From 029344da14c63d5a75704d7297cb4185ac45af65 Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 11 Nov 2018 22:33:58 +0800 Subject: [PATCH 1/6] Only require Xtigervnc or Xvfb+x11vnc for Docker with vnc console. Ref #1438 --- gns3server/compute/docker/docker_vm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index b6c9b4da..04d2401b 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -519,7 +519,7 @@ class DockerVM(BaseNode): """ self._display = self._get_free_display_port() - if shutil.which("Xtigervnc") is None or shutil.which("Xvfb") is None or shutil.which("x11vnc") is None: + 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"): From e7b8309a800228d9c73779c0ece60654b50e8b4e Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 11 Nov 2018 23:25:23 +0800 Subject: [PATCH 2/6] Fix Docker VNC tests. Ref #1438 --- tests/compute/docker/test_docker_vm.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/compute/docker/test_docker_vm.py b/tests/compute/docker/test_docker_vm.py index bb0e9779..72263866 100644 --- a/tests/compute/docker/test_docker_vm.py +++ b/tests/compute/docker/test_docker_vm.py @@ -971,13 +971,12 @@ 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())) assert vm._display is not None - mock_exec.assert_any_call("Xvfb", "-nolisten", "tcp", ":{}".format(vm._display), "-screen", "0", "1280x1024x16") - mock_exec.assert_any_call("x11vnc", "-forever", "-nopw", "-shared", "-geometry", "1280x1024", "-display", "WAIT:{}".format(vm._display), "-rfbport", str(vm.console), "-rfbportv6", str(vm.console), "-noncache", "-listen", "127.0.0.1") + assert mock_exec.call_args[0] == ("Xtigervnc", "-geometry", vm.console_resolution, "-depth", "16", "-interface", "127.0.0.1", "-rfbport", str(vm.console), "-AlwaysShared", "-SecurityTypes", "None", ":{}".format(vm._display)) mock_wait.assert_called_with("/tmp/.X11-unix/X{}".format(vm._display)) From df3baffd9b69cc794df8e26a1e0ad1acf8ab4d1f Mon Sep 17 00:00:00 2001 From: grossmj Date: Sat, 24 Nov 2018 19:56:29 +0700 Subject: [PATCH 3/6] Fix "None is not of type 'integer'" when opening project containing a Qemu VM. Fixes #2610. --- gns3server/schemas/qemu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index d1157de9..7579616d 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -537,7 +537,7 @@ QEMU_OBJECT_SCHEMA = { "description": "Console TCP port", "minimum": 1, "maximum": 65535, - "type": "integer" + "type": ["integer", "null"] }, "console_type": { "description": "Console type", From a3044ede77359cc1ba1c7a9e59627c178dd1a81f Mon Sep 17 00:00:00 2001 From: grossmj Date: Sun, 25 Nov 2018 17:11:42 +0700 Subject: [PATCH 4/6] Fix _fix_permissions() garbles permissions in Docker VM. Ref #1428 --- gns3server/compute/docker/docker_vm.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 04d2401b..a5ec22ce 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -462,11 +462,10 @@ class DockerVM(BaseNode): # We can not use the API because docker doesn't expose a websocket api for exec # https://github.com/GNS3/gns3-gui/issues/1039 try: - process = yield from asyncio.subprocess.create_subprocess_exec( - "docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "script", "-qfc", "while true; do TERM=vt100 /gns3/bin/busybox sh; done", "/dev/null", - stdout=asyncio.subprocess.PIPE, - stderr=asyncio.subprocess.STDOUT, - stdin=asyncio.subprocess.PIPE) + process = yield from asyncio.subprocess.create_subprocess_exec("docker", "exec", "-i", self._cid, "/gns3/bin/busybox", "script", "-qfc", "while true; do TERM=vt100 /gns3/bin/busybox sh; done", "/dev/null", + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.STDOUT, + stdin=asyncio.subprocess.PIPE) except OSError as e: raise DockerError("Could not start auxiliary console process: {}".format(e)) server = AsyncioTelnetServer(reader=process.stdout, writer=process.stdin, binary=True, echo=True) @@ -474,7 +473,7 @@ class DockerVM(BaseNode): self._telnet_servers.append((yield from 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) @asyncio.coroutine def _fix_permissions(self): @@ -484,6 +483,7 @@ class DockerVM(BaseNode): """ state = yield from 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 yield from self.manager.query("POST", "containers/{}/start".format(self._cid)) @@ -697,14 +697,15 @@ class DockerVM(BaseNode): if state == "paused": yield from self.unpause() - yield from self._fix_permissions() + if state == "running": + yield from self._fix_permissions() + state = yield from self._get_container_state() if state != "stopped" or state != "exited": # t=5 number of seconds to wait before killing the container try: yield from 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 From c0a0a13bdd40ce1ac2c205a4328b5e6c6bcf17cc Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 26 Nov 2018 15:53:24 +0700 Subject: [PATCH 5/6] Avoid _fix_permissions() to be called twice when stopping Docker VM. Ref #1428 --- gns3server/compute/docker/docker_vm.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index a5ec22ce..3790c5f6 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -90,6 +90,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._volumes = [] # Keep a list of created bridge @@ -447,6 +448,7 @@ class DockerVM(BaseNode): if self.allocate_aux: yield from 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, @@ -511,6 +513,7 @@ class DockerVM(BaseNode): except OSError as e: raise DockerError("Could not fix permissions for {}: {}".format(volume, e)) yield from process.wait() + self._permissions_fixed = True @asyncio.coroutine def _start_vnc(self): @@ -697,7 +700,7 @@ class DockerVM(BaseNode): if state == "paused": yield from self.unpause() - if state == "running": + if not self._permissions_fixed: yield from self._fix_permissions() state = yield from self._get_container_state() From 60ac6d2dfeecef9fbf59018ba69d3e4e9b9673de Mon Sep 17 00:00:00 2001 From: grossmj Date: Tue, 27 Nov 2018 15:06:56 +0700 Subject: [PATCH 6/6] Telnet console resize support for Docker VM. --- gns3server/compute/docker/docker_vm.py | 18 ++++++++++++++--- gns3server/utils/asyncio/telnet_server.py | 24 ++++++++++++++--------- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/gns3server/compute/docker/docker_vm.py b/gns3server/compute/docker/docker_vm.py index 3790c5f6..a8625895 100644 --- a/gns3server/compute/docker/docker_vm.py +++ b/gns3server/compute/docker/docker_vm.py @@ -442,6 +442,7 @@ class DockerVM(BaseNode): if self.console_type == "telnet": yield from self._start_console() + elif self.console_type == "http" or self.console_type == "https": yield from self._start_http() @@ -592,6 +593,19 @@ class DockerVM(BaseNode): ]) self._telnet_servers.append((yield from asyncio.start_server(server.run, self._manager.port_manager.console_host, self.console))) + @asyncio.coroutine + 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. + yield from self._manager.query("POST", "containers/{}/resize?h={}&w={}".format(self._cid, rows, columns)) + @asyncio.coroutine def _start_console(self): """ @@ -614,8 +628,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((yield from asyncio.start_server(telnet.run, self._manager.port_manager.console_host, self.console))) except OSError as e: @@ -625,7 +638,6 @@ class DockerVM(BaseNode): input_stream.ws = self._console_websocket output_stream.feed_data(self.name.encode() + b" console is now available... Press RETURN to get started.\r\n") - asyncio_ensure_future(self._read_console_output(self._console_websocket, output_stream)) @asyncio.coroutine diff --git a/gns3server/utils/asyncio/telnet_server.py b/gns3server/utils/asyncio/telnet_server.py index 9223fc16..1548d6b1 100644 --- a/gns3server/utils/asyncio/telnet_server.py +++ b/gns3server/utils/asyncio/telnet_server.py @@ -15,7 +15,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -import re + import asyncio import asyncio.subprocess import struct @@ -62,10 +62,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): @@ -85,10 +86,13 @@ class TelnetConnection(object): """Method called when client is disconnecting""" pass + @asyncio.coroutine 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: + yield from self._window_size_changed_callback(columns, rows) @asyncio.coroutine def feed(self, data): @@ -116,7 +120,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 @@ -131,6 +135,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 @@ -139,8 +144,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 @@ -190,7 +195,7 @@ class AsyncioTelnetServer: @asyncio.coroutine 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: @@ -307,6 +312,7 @@ class AsyncioTelnetServer: cmd.append(buffer[location]) return op + @asyncio.coroutine def _negotiate(self, data, connection): """ Performs negotiation commands""" @@ -314,7 +320,7 @@ class AsyncioTelnetServer: if command == NAWS: if len(payload) == 4: columns, rows = struct.unpack(str('!HH'), bytes(payload)) - connection.window_size_changed(columns, rows) + yield from connection.window_size_changed(columns, rows) else: log.warning('Wrong number of NAWS bytes') else: @@ -373,7 +379,7 @@ class AsyncioTelnetServer: break # SE command is followed by IAC, remove the last two operations from stack - self._negotiate(negotiation[0:-2], connection) + yield from self._negotiate(negotiation[0:-2], connection) # This must be a 3-byte TELNET command else: