diff --git a/gns3server/modules/docker/docker_vm.py b/gns3server/modules/docker/docker_vm.py index 5b280e1a..f62a0cac 100644 --- a/gns3server/modules/docker/docker_vm.py +++ b/gns3server/modules/docker/docker_vm.py @@ -67,7 +67,7 @@ class DockerVM(BaseVM): self._ethernet_adapters = [] self._ubridge_hypervisor = None self._temporary_directory = None - self._telnet_server = None + self._telnet_servers = [] if adapters is None: self.adapters = 1 @@ -255,8 +255,28 @@ class DockerVM(BaseVM): if self.console_type == "telnet": yield from self._start_console() + if self.allocate_aux: + yield from self._start_aux() + self.status = "started" - log.info("Docker container '{name}' [{image}] started listen for telnet on {console}".format(name=self._name, image=self._image, console=self._console)) + log.info("Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(name=self._name, image=self._image, console=self.console, console_type=self.console_type)) + + @asyncio.coroutine + def _start_aux(self): + """ + Start an auxilary console + """ + + # We can not use the API because docker doesn't expose a websocket api for exec + # https://github.com/GNS3/gns3-gui/issues/1039 + process = yield from asyncio.subprocess.create_subprocess_exec( + "docker", "exec", "-i", self._cid, "/bin/sh", "-i", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + stdin=asyncio.subprocess.PIPE) + server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=False, echo=False) + self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux))) + log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux) @asyncio.coroutine def _start_vnc(self): @@ -295,8 +315,8 @@ class DockerVM(BaseVM): output_stream = asyncio.StreamReader() input_stream = InputStream() - telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream) - self._telnet_server = yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self._console) + telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True) + self._telnet_servers.append((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console))) ws = yield from self.manager.websocket_query("containers/{}/attach/ws?stream=1&stdin=1&stdout=1&stderr=1".format(self._cid)) input_stream.ws = ws @@ -345,10 +365,11 @@ class DockerVM(BaseVM): """Stops this Docker container.""" try: - if self._telnet_server: - self._telnet_server.close() - yield from self._telnet_server.wait_closed() - self._telnet_server = None + if len(self._telnet_servers) > 0: + for telnet_server in self._telnet_servers: + telnet_server.close() + yield from telnet_server.wait_closed() + self._telnet_servers = [] if self._ubridge_hypervisor and self._ubridge_hypervisor.is_running(): yield from self._ubridge_hypervisor.stop() diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index cdc7fcd2..9bb1ee6c 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -58,7 +58,7 @@ READ_SIZE = 1024 class AsyncioTelnetServer: - def __init__(self, reader=None, writer=None): + def __init__(self, reader=None, writer=None, binary=True, echo=False): self._reader = reader self._writer = writer self._clients = set() @@ -66,6 +66,12 @@ class AsyncioTelnetServer: self._reader_process = None self._current_read = None + self._binary = binary + # If echo is true when the client send data + # the data is echo on his terminal by telnet otherwise + # it's our job (or the wrapped app) to send back the data + self._echo = echo + @asyncio.coroutine def run(self, network_reader, network_writer): # Keep track of connected clients @@ -73,10 +79,24 @@ class AsyncioTelnetServer: try: # Send initial telnet session opening - network_writer.write(bytes([IAC, WILL, ECHO, - IAC, WILL, SGA, - IAC, WILL, BINARY, - IAC, DO, BINARY])) + if self._echo: + network_writer.write(bytes([IAC, WILL, ECHO])) + else: + network_writer.write(bytes([ + IAC, WONT, ECHO, + IAC, DONT, ECHO])) + + if self._binary: + network_writer.write(bytes([ + IAC, WILL, SGA, + IAC, WILL, BINARY, + IAC, DO, BINARY])) + else: + network_writer.write(bytes([ + IAC, WONT, SGA, + IAC, DONT, SGA, + IAC, WONT, BINARY, + IAC, DONT, BINARY])) yield from network_writer.drain() yield from self._process(network_reader, network_writer) @@ -128,7 +148,6 @@ class AsyncioTelnetServer: return_when=asyncio.FIRST_COMPLETED) for coro in done: data = coro.result() - # Console is closed if len(data) == 0: raise ConnectionResetError() @@ -138,11 +157,18 @@ class AsyncioTelnetServer: if IAC in data: data = yield from self._IAC_parser(data, network_reader, network_writer) + if len(data) == 0: + continue + + if not self._binary: + data = data.replace(b"\r\n", b"\n") + if self._writer: self._writer.write(data) yield from self._writer.drain() elif coro == reader_read: reader_read = yield from self._get_reader(network_reader) + # Replicate the output on all clients for writer in self._clients: writer.write(data) @@ -199,9 +225,27 @@ class AsyncioTelnetServer: buf.extend(d) iac_cmd.append(buf[iac_loc + 2]) # We do ECHO, SGA, and BINARY. Period. - if iac_cmd[1] == DO and iac_cmd[2] not in [ECHO, SGA, BINARY]: - network_writer.write(bytes([IAC, WONT, iac_cmd[2]])) - log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + if iac_cmd[1] == DO: + if iac_cmd[2] not in [ECHO, SGA, BINARY]: + network_writer.write(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + else: + if iac_cmd[2] == SGA: + if self._binary: + network_writer.write(bytes([IAC, WILL, iac_cmd[2]])) + else: + network_writer.write(bytes([IAC, WONT, iac_cmd[2]])) + log.debug("Telnet WON'T {:#x}".format(iac_cmd[2])) + + elif iac_cmd[1] == DONT: + log.debug("Unhandled DONT telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + elif iac_cmd[1] == WILL: + log.debug("Unhandled WILL telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) + elif iac_cmd[1] == WONT: + log.debug("Unhandled WONT telnet command: " + "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) else: log.debug("Unhandled telnet command: " "{0:#x} {1:#x} {2:#x}".format(*iac_cmd)) @@ -215,15 +259,16 @@ class AsyncioTelnetServer: return buf if __name__ == '__main__': + logging.basicConfig(level=logging.DEBUG) loop = asyncio.get_event_loop() - process = loop.run_until_complete(asyncio.async(asyncio.subprocess.create_subprocess_exec("bash", + process = loop.run_until_complete(asyncio.async(asyncio.subprocess.create_subprocess_exec("/bin/sh", "-i", stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT, stdin=asyncio.subprocess.PIPE))) - server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin) + server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=False, echo=False) - coro = asyncio.start_server(server.run, '127.0.0.1', 2222, loop=loop) + coro = asyncio.start_server(server.run, '127.0.0.1', 4444, loop=loop) s = loop.run_until_complete(coro) try: diff --git a/tests/modules/docker/test_docker_vm.py b/tests/modules/docker/test_docker_vm.py index 1d399356..0c5a93ab 100644 --- a/tests/modules/docker/test_docker_vm.py +++ b/tests/modules/docker/test_docker_vm.py @@ -19,7 +19,7 @@ import pytest import uuid import asyncio import os -from tests.utils import asyncio_patch +from tests.utils import asyncio_patch, AsyncioMagicMock from gns3server.ubridge.ubridge_error import UbridgeNamespaceError from gns3server.modules.docker.docker_vm import DockerVM @@ -42,6 +42,7 @@ def manager(port_manager): def vm(project, manager): vm = DockerVM("test", str(uuid.uuid4()), project, manager, "ubuntu") vm._cid = "e90e34656842" + vm.allocate_aux = False return vm @@ -308,21 +309,26 @@ def test_start(loop, vm, manager, free_console_port): assert vm.status != "started" vm.adapters = 1 + vm.allocate_aux = True + vm._start_aux = AsyncioMagicMock() + + vm._get_container_state = AsyncioMagicMock(return_value="stopped") + vm._start_ubridge = AsyncioMagicMock() + vm._get_namespace = AsyncioMagicMock(return_value=42) + vm._add_ubridge_connection = AsyncioMagicMock() + vm._start_console = AsyncioMagicMock() + nio = manager.create_nio(0, {"type": "nio_udp", "lport": free_console_port, "rport": free_console_port, "rhost": "127.0.0.1"}) loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, nio))) - with asyncio_patch("gns3server.modules.docker.DockerVM._get_container_state", return_value="stopped"): - with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: - with asyncio_patch("gns3server.modules.docker.DockerVM._start_ubridge") as mock_start_ubridge: - with asyncio_patch("gns3server.modules.docker.DockerVM._get_namespace", return_value=42) as mock_namespace: - with asyncio_patch("gns3server.modules.docker.DockerVM._add_ubridge_connection") as mock_add_ubridge_connection: - with asyncio_patch("gns3server.modules.docker.DockerVM._start_console") as mock_start_console: - loop.run_until_complete(asyncio.async(vm.start())) + with asyncio_patch("gns3server.modules.docker.Docker.query") as mock_query: + loop.run_until_complete(asyncio.async(vm.start())) mock_query.assert_called_with("POST", "containers/e90e34656842/start") - mock_add_ubridge_connection.assert_called_once_with(nio, 0, 42) - assert mock_start_ubridge.called - assert mock_start_console.called + vm._add_ubridge_connection.assert_called_once_with(nio, 0, 42) + assert vm._start_ubridge.called + assert vm._start_console.called + assert vm._start_aux.called assert vm.status == "started" @@ -759,3 +765,9 @@ def test_start_vnc(vm, loop): def test_start_vnc_xvfb_missing(vm, loop): with pytest.raises(DockerError): loop.run_until_complete(asyncio.async(vm._start_vnc())) + + +def test_start_aux(vm, loop): + + with asyncio_patch("asyncio.subprocess.create_subprocess_exec", return_value=MagicMock()) as mock_exec: + loop.run_until_complete(asyncio.async(vm._start_aux())) diff --git a/tests/utils.py b/tests/utils.py index cb3b4f39..507f35a3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -16,7 +16,7 @@ # along with this program. If not, see . import asyncio -from unittest.mock import patch +from unittest.mock import patch, MagicMock class _asyncio_patch: @@ -62,3 +62,15 @@ class _asyncio_patch: def asyncio_patch(function, *args, **kwargs): return _asyncio_patch(function, *args, **kwargs) + + +class AsyncioMagicMock(MagicMock): + """ + Magic mock returning coroutine + """ + def __init__(self, return_value=None, **kwargs): + if return_value: + future = asyncio.Future() + future.set_result(return_value) + kwargs["return_value"] = future + super().__init__(**kwargs)