mirror of
https://github.com/GNS3/gns3-server
synced 2024-12-23 23:38:21 +00:00
Aux console for Docker
Fix https://github.com/GNS3/gns3-gui/issues/1039
This commit is contained in:
parent
03ffce0a75
commit
dab1b26569
@ -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()
|
||||
|
@ -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:
|
||||
|
@ -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()))
|
||||
|
@ -16,7 +16,7 @@
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
|
Loading…
Reference in New Issue
Block a user