Skip to content

Commit

Permalink
Merge pull request #2417 from GNS3/backport-aux-console-support
Browse files Browse the repository at this point in the history
Backport auxiliary console support for Qemu, Docker and Dynamips nodes
  • Loading branch information
grossmj authored Sep 22, 2024
2 parents 1f09a3e + 74782d4 commit f1294cf
Show file tree
Hide file tree
Showing 25 changed files with 390 additions and 156 deletions.
182 changes: 115 additions & 67 deletions gns3server/compute/base_node.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,16 @@ class BaseNode:
:param node_id: Node instance identifier
:param project: Project instance
:param manager: parent node manager
:param console: TCP console port
:param aux: TCP aux console port
:param allocate_aux: Boolean if true will allocate an aux console port
:param console: console TCP port
:param console_type: console type
:param aux: auxiliary console TCP port
:param aux_type: auxiliary console type
:param linked_clone: The node base image is duplicate/overlay (Each node data are independent)
:param wrap_console: The console is wrapped using AsyncioTelnetServer
:param wrap_aux: The auxiliary console is wrapped using AsyncioTelnetServer
"""

def __init__(self, name, node_id, project, manager, console=None, console_type="telnet", aux=None, allocate_aux=False, linked_clone=True, wrap_console=False):
def __init__(self, name, node_id, project, manager, console=None, console_type="telnet", aux=None, aux_type="none", linked_clone=True, wrap_console=False, wrap_aux=False):

self._name = name
self._usage = ""
Expand All @@ -68,22 +70,25 @@ def __init__(self, name, node_id, project, manager, console=None, console_type="
self._console = console
self._aux = aux
self._console_type = console_type
self._aux_type = aux_type
self._temporary_directory = None
self._hw_virtualization = False
self._ubridge_hypervisor = None
self._closed = False
self._node_status = "stopped"
self._command_line = ""
self._allocate_aux = allocate_aux
self._wrap_console = wrap_console
self._wrapper_telnet_server = None
self._wrap_aux = wrap_aux
self._wrapper_telnet_servers = []
self._wrap_console_reader = None
self._wrap_console_writer = None
self._internal_console_port = None
self._internal_aux_port = None
self._custom_adapters = []
self._ubridge_require_privileged_access = False

if self._console is not None:
# use a previously allocated console port
if console_type == "vnc":
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
self._console = self._manager.port_manager.reserve_tcp_port(
Expand All @@ -97,25 +102,45 @@ def __init__(self, name, node_id, project, manager, console=None, console_type="
else:
self._console = self._manager.port_manager.reserve_tcp_port(self._console, self._project)

# We need to allocate aux before giving a random console port
if self._aux is not None:
self._aux = self._manager.port_manager.reserve_tcp_port(self._aux, self._project)
# use a previously allocated auxiliary console port
if aux_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._aux = self._manager.port_manager.reserve_tcp_port(
self._aux, self._project, port_range_start=5900, port_range_end=6000
)
elif aux_type == "none":
self._aux = None
else:
self._aux = self._manager.port_manager.reserve_tcp_port(self._aux, self._project)

if self._console is None:
# allocate a new console
if console_type == "vnc":
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
self._console = self._manager.port_manager.get_free_tcp_port(
self._project,
port_range_start=vnc_console_start_port_range,
port_range_end=vnc_console_end_port_range)
port_range_end=vnc_console_end_port_range,
)
elif console_type != "none":
self._console = self._manager.port_manager.get_free_tcp_port(self._project)

if self._aux is None:
# allocate a new auxiliary console
if aux_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._aux = self._manager.port_manager.get_free_tcp_port(
self._project, port_range_start=5900, port_range_end=6000
)
elif aux_type != "none":
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)

if self._wrap_console:
self._internal_console_port = self._manager.port_manager.get_free_tcp_port(self._project)

if self._aux is None and allocate_aux:
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)
if self._wrap_aux:
self._internal_aux_port = self._manager.port_manager.get_free_tcp_port(self._project)

log.debug("{module}: {name} [{id}] initialized. Console port {console}".format(module=self.manager.module_name,
name=self.name,
Expand Down Expand Up @@ -343,6 +368,9 @@ async def close(self):
if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project)
self._aux = None
if self._wrap_aux:
self._manager.port_manager.release_tcp_port(self._internal_aux_port, self._project)
self._internal_aux_port = None

self._closed = True
return True
Expand All @@ -366,56 +394,49 @@ def _get_vnc_console_port_range(self):

return vnc_console_start_port_range, vnc_console_end_port_range

async def start_wrap_console(self):
"""
Start a telnet proxy for the console allowing multiple telnet clients
to be connected at the same time
"""
async def _wrap_telnet_proxy(self, internal_port, external_port):

if not self._wrap_console or self._console_type != "telnet":
return
remaining_trial = 60
while True:
try:
(self._wrap_console_reader, self._wrap_console_writer) = await asyncio.open_connection(
host="127.0.0.1",
port=self._internal_console_port
)
(reader, writer) = await asyncio.open_connection(host="127.0.0.1", port=internal_port)
break
except (OSError, ConnectionRefusedError) as e:
if remaining_trial <= 0:
raise e
await asyncio.sleep(0.1)
remaining_trial -= 1
await AsyncioTelnetServer.write_client_intro(self._wrap_console_writer, echo=True)
server = AsyncioTelnetServer(
reader=self._wrap_console_reader,
writer=self._wrap_console_writer,
binary=True,
echo=True
)
await AsyncioTelnetServer.write_client_intro(writer, echo=True)
server = AsyncioTelnetServer(reader=reader, writer=writer, binary=True, echo=True)
# warning: this will raise OSError exception if there is a problem...
self._wrapper_telnet_server = await asyncio.start_server(
server.run,
self._manager.port_manager.console_host,
self.console
)
telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, external_port)
self._wrapper_telnet_servers.append(telnet_server)

async def start_wrap_console(self):
"""
Start a Telnet proxy servers for the console and auxiliary console allowing multiple telnet clients
to be connected at the same time
"""

if self._wrap_console and self._console_type == "telnet":
await self._wrap_telnet_proxy(self._internal_console_port, self.console)
log.info("New Telnet proxy server for console started (internal port = {}, external port = {})".format(self._internal_console_port,
self.console))

if self._wrap_aux and self._aux_type == "telnet":
await self._wrap_telnet_proxy(self._internal_aux_port, self.aux)
log.info("New Telnet proxy server for auxiliary console started (internal port = {}, external port = {})".format(self._internal_aux_port,
self.aux))

async def stop_wrap_console(self):
"""
Stops the telnet proxy.
Stops the telnet proxy servers.
"""

if self._wrapper_telnet_server:
self._wrap_console_writer.close()
if sys.version_info >= (3, 7, 0):
try:
await self._wrap_console_writer.wait_closed()
except ConnectionResetError:
pass
self._wrapper_telnet_server.close()
await self._wrapper_telnet_server.wait_closed()
self._wrapper_telnet_server = None
for telnet_proxy_server in self._wrapper_telnet_servers:
telnet_proxy_server.close()
await telnet_proxy_server.wait_closed()
self._wrapper_telnet_servers = []

async def reset_wrap_console(self):
"""
Expand Down Expand Up @@ -492,22 +513,6 @@ async def telnet_forward(telnet_reader):

return ws

@property
def allocate_aux(self):
"""
:returns: Boolean allocate or not an aux console
"""

return self._allocate_aux

@allocate_aux.setter
def allocate_aux(self, allocate_aux):
"""
:returns: Boolean allocate or not an aux console
"""

self._allocate_aux = allocate_aux

@property
def aux(self):
"""
Expand All @@ -526,18 +531,25 @@ def aux(self, aux):
:params aux: Console port (integer) or None to free the port
"""

if aux == self._aux:
if aux == self._aux or self._aux_type == "none":
return

if self._aux_type == "vnc" and aux is not None and aux < 5900:
raise NodeError("VNC auxiliary console require a port superior or equal to 5900, current port is {}".format(aux))

if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project)
self._aux = None
if aux is not None:
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project)
log.info("{module}: '{name}' [{id}]: aux port set to {port}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
port=aux))
if self._aux_type == "vnc":
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project, port_range_start=5900, port_range_end=6000)
else:
self._aux = self._manager.port_manager.reserve_tcp_port(aux, self._project)

log.info("{module}: '{name}' [{id}]: auxiliary console port set to {port}".format(module=self.manager.module_name,
name=self.name,
id=self.id,
port=aux))

@property
def console(self):
Expand Down Expand Up @@ -625,6 +637,42 @@ def console_type(self, console_type):
console_type=console_type,
console=self.console))

@property
def aux_type(self):
"""
Returns the auxiliary console type for this node.
:returns: aux type (string)
"""

return self._aux_type

@aux_type.setter
def aux_type(self, aux_type):
"""
Sets the auxiliary console type for this node.
:param aux_type: console type (string)
"""

if aux_type != self._aux_type:
# get a new port if the aux type change
if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project)
if aux_type == "none":
# no need to allocate a port when the auxiliary console type is none
self._aux = None
elif aux_type == "vnc":
# VNC is a special case and the range must be 5900-6000
self._aux = self._manager.port_manager.get_free_tcp_port(self._project, 5900, 6000)
else:
self._aux = self._manager.port_manager.get_free_tcp_port(self._project)

self._aux_type = aux_type
log.info("{module}: '{name}' [{id}]: console type set to {aux_type} (auxiliary console port is {aux})".format(module=self.manager.module_name,
name=self.name,
id=self.id,
aux_type=aux_type,
aux=self.aux))

@property
def ubridge(self):
"""
Expand Down
10 changes: 6 additions & 4 deletions gns3server/compute/docker/docker_vm.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,9 @@ class DockerVM(BaseNode):
:param manager: Manager instance
:param image: Docker image
:param console: TCP console port
:param console_type: Console type
:param console_type: console type
:param aux: TCP aux console port
:param aux_type: auxiliary console type
:param console_resolution: Resolution of the VNC display
:param console_http_port: Port to redirect HTTP queries
:param console_http_path: Url part with the path of the web interface
Expand All @@ -70,10 +71,10 @@ class DockerVM(BaseNode):
"""

def __init__(self, name, node_id, project, manager, image, console=None, aux=None, start_command=None,
adapters=None, environment=None, console_type="telnet", console_resolution="1024x768",
adapters=None, environment=None, console_type="telnet", aux_type="none", console_resolution="1024x768",
console_http_port=80, console_http_path="/", extra_hosts=None, extra_volumes=[]):

super().__init__(name, node_id, project, manager, console=console, aux=aux, allocate_aux=True, console_type=console_type)
super().__init__(name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type)

# force the latest image if no version is specified
if ":" not in image:
Expand Down Expand Up @@ -129,6 +130,7 @@ def __json__(self):
"console_http_port": self.console_http_port,
"console_http_path": self.console_http_path,
"aux": self.aux,
"aux_type": self.aux_type,
"start_command": self.start_command,
"status": self.status,
"environment": self.environment,
Expand Down Expand Up @@ -546,7 +548,7 @@ async def start(self):
elif self.console_type == "http" or self.console_type == "https":
await self._start_http()

if self.allocate_aux:
if self.aux_type != "none":
await self._start_aux()

self._permissions_fixed = False
Expand Down
6 changes: 4 additions & 2 deletions gns3server/compute/dynamips/nodes/c1700.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ class C1700(Router):
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param console_type: console type
:param aux: auxiliary console port
:param aux_type: auxiliary console type
:param chassis: chassis for this router:
1720, 1721, 1750, 1751 or 1760 (default = 1720).
1710 is not supported.
"""

def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, chassis="1720"):
def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis="1720"):

super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, platform="c1700")
super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c1700")

# Set default values for this platform (must be the same as Dynamips)
self._ram = 64
Expand Down
6 changes: 4 additions & 2 deletions gns3server/compute/dynamips/nodes/c2600.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ class C2600(Router):
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param console_type: console type
:param aux: auxiliary console port
:param aux_type: auxiliary console type
:param chassis: chassis for this router:
2610, 2611, 2620, 2621, 2610XM, 2611XM
2620XM, 2621XM, 2650XM or 2651XM (default = 2610).
Expand All @@ -61,9 +63,9 @@ class C2600(Router):
"2650XM": C2600_MB_1FE,
"2651XM": C2600_MB_2FE}

def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, chassis="2610"):
def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis="2610"):

super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, platform="c2600")
super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2600")

# Set default values for this platform (must be the same as Dynamips)
self._ram = 64
Expand Down
6 changes: 4 additions & 2 deletions gns3server/compute/dynamips/nodes/c2691.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,14 @@ class C2691(Router):
:param manager: Parent VM Manager
:param dynamips_id: ID to use with Dynamips
:param console: console port
:param console_type: console type
:param aux: auxiliary console port
:param aux_type: auxiliary console type
"""

def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, chassis=None):
def __init__(self, name, node_id, project, manager, dynamips_id, console=None, console_type="telnet", aux=None, aux_type="none", chassis=None):

super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, platform="c2691")
super().__init__(name, node_id, project, manager, dynamips_id, console, console_type, aux, aux_type, platform="c2691")

# Set default values for this platform (must be the same as Dynamips)
self._ram = 128
Expand Down
Loading

0 comments on commit f1294cf

Please sign in to comment.