diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 38fdc9c0..a0ee4a61 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -89,6 +89,7 @@ class DockerVM(BaseNode): self._console_http_port = console_http_port self._console_websocket = None self._extra_hosts = extra_hosts + self._permissions_fixed = False self._display = None self._closing = False @@ -459,6 +460,7 @@ class DockerVM(BaseNode): if self.allocate_aux: await self._start_aux() + self._permissions_fixed = False self.status = "started" log.info("Docker container '{name}' [{image}] started listen for {console_type} on {console}".format(name=self._name, image=self._image, @@ -485,7 +487,7 @@ class DockerVM(BaseNode): self._telnet_servers.append((await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.aux))) except OSError as e: raise DockerError("Could not start Telnet server on socket {}:{}: {}".format(self._manager.port_manager.console_host, self.aux, e)) - log.debug("Docker container '%s' started listen for auxilary telnet on %d", self.name, self.aux) + log.debug("Docker container '%s' started listen for auxiliary telnet on %d", self.name, self.aux) async def _fix_permissions(self): """ @@ -494,6 +496,7 @@ class DockerVM(BaseNode): """ state = await self._get_container_state() + log.info("Docker container '{name}' fix ownership, state = {state}".format(name=self._name, state=state)) if state == "stopped" or state == "exited": # We need to restart it to fix permissions await self.manager.query("POST", "containers/{}/start".format(self._cid)) @@ -521,12 +524,17 @@ class DockerVM(BaseNode): except OSError as e: raise DockerError("Could not fix permissions for {}: {}".format(volume, e)) await process.wait() + self._permissions_fixed = True async def _start_vnc_process(self, restart=False): """ Starts the VNC process. """ + self._display = self._get_free_display_port() + if not (shutil.which("Xtigervnc") or shutil.which("Xvfb") and shutil.which("x11vnc")): + raise DockerError("Please install tigervnc-standalone-server (recommended) or Xvfb + x11vnc before using VNC support") + if shutil.which("Xtigervnc"): with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd: self._vnc_process = await asyncio.create_subprocess_exec("Xtigervnc", @@ -607,6 +615,19 @@ class DockerVM(BaseNode): ]) self._telnet_servers.append((await asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console))) + async def _window_size_changed_callback(self, columns, rows): + """ + Called when the console window size has been changed. + (when naws is enabled in the Telnet server) + + :param columns: number of columns + :param rows: number of rows + """ + + # resize the container TTY. + await self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns)) + + async def _start_console(self): """ Starts streaming the console via telnet @@ -627,8 +648,7 @@ class DockerVM(BaseNode): output_stream = asyncio.StreamReader() input_stream = InputStream() - - telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True) + telnet = AsyncioTelnetServer(reader=output_stream, writer=input_stream, echo=True, naws=True, window_size_changed_callback=self._window_size_changed_callback) try: self._telnet_servers.append((await asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console))) except OSError as e: @@ -716,14 +736,15 @@ class DockerVM(BaseNode): if state == "paused": await self.unpause() - await self._fix_permissions() + if not self._permissions_fixed: + await self._fix_permissions() + state = await self._get_container_state() if state != "stopped" or state != "exited": # t=5 number of seconds to wait before killing the container try: await self.manager.query("POST", "containers/{}/stop".format(self._cid), params={"t": 5}) - log.info("Docker container '{name}' [{image}] stopped".format( - name=self._name, image=self._image)) + log.info("Docker container '{name}' [{image}] stopped".format(name=self._name, image=self._image)) except DockerHttp304Error: # Container is already stopped pass diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index a2f5e436..439e7412 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -59,10 +59,11 @@ READ_SIZE = 1024 class TelnetConnection(object): """Default implementation of telnet connection which may but may not be used.""" - def __init__(self, reader, writer): + def __init__(self, reader, writer, window_size_changed_callback=None): self.is_closing = False self._reader = reader self._writer = writer + self._window_size_changed_callback = window_size_changed_callback @property def reader(self): @@ -80,10 +81,12 @@ class TelnetConnection(object): """Method called when client is disconnecting""" pass - def window_size_changed(self, columns, rows): + async def window_size_changed(self, columns, rows): """Method called when window size changed, only can occur when `naws` flag is enable in server configuration.""" - pass + + if self._window_size_changed_callback: + await self._window_size_changed_callback(columns, rows) async def feed(self, data): """ @@ -110,7 +113,7 @@ class TelnetConnection(object): class AsyncioTelnetServer: MAX_NEGOTIATION_READ = 10 - def __init__(self, reader=None, writer=None, binary=True, echo=False, naws=False, connection_factory=None): + def __init__(self, reader=None, writer=None, binary=True, echo=False, naws=False, window_size_changed_callback=None, connection_factory=None): """ Initializes telnet server :param naws when True make a window size negotiation @@ -125,6 +128,7 @@ class AsyncioTelnetServer: self._lock = asyncio.Lock() self._reader_process = None self._current_read = None + self._window_size_changed_callback = window_size_changed_callback self._binary = binary # If echo is true when the client send data @@ -133,8 +137,8 @@ class AsyncioTelnetServer: self._echo = echo self._naws = naws - def default_connection_factory(reader, writer): - return TelnetConnection(reader, writer) + def default_connection_factory(reader, writer, window_size_changed_callback): + return TelnetConnection(reader, writer, window_size_changed_callback) if connection_factory is None: connection_factory = default_connection_factory @@ -181,7 +185,7 @@ class AsyncioTelnetServer: async def run(self, network_reader, network_writer): # Keep track of connected clients - connection = self._connection_factory(network_reader, network_writer) + connection = self._connection_factory(network_reader, network_writer, self._window_size_changed_callback) self._connections[network_writer] = connection try: @@ -293,14 +297,14 @@ class AsyncioTelnetServer: cmd.append(buffer[location]) return op - def _negotiate(self, data, connection): + async def _negotiate(self, data, connection): """ Performs negotiation commands""" command, payload = data[0], data[1:] if command == NAWS: if len(payload) == 4: columns, rows = struct.unpack(str('!HH'), bytes(payload)) - connection.window_size_changed(columns, rows) + await connection.window_size_changed(columns, rows) else: log.warning('Wrong number of NAWS bytes') else: @@ -358,7 +362,7 @@ class AsyncioTelnetServer: break # SE command is followed by IAC, remove the last two operations from stack - self._negotiate(negotiation[0:-2], connection) + await self._negotiate(negotiation[0:-2], connection) # This must be a 3-byte TELNET command else: diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index ae34281a..3740594f 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -970,7 +970,7 @@ def test_mount_binds(vm, tmpdir): def test_start_vnc(vm, loop): vm.console_resolution = "1280x1024" - with patch("shutil.which", return_value="/bin/x"): + with patch("shutil.which", return_value="/bin/Xtigervnc"): with asyncio_patch("gns3server.compute.docker.docker_vm.wait_for_file_creation") as mock_wait: with asyncio_patch("asyncio.create_subprocess_exec") as mock_exec: loop.run_until_complete(asyncio.ensure_future(vm._start_vnc()))