pull/2160/head
grossmj 1 year ago
commit 27d9063e56

@ -1,5 +1,17 @@
# Change Log # Change Log
## 2.2.36 04/01/2023
* Install web-ui v2.2.36
* Add Trusted Platform Module (TPM) support for Qemu VMs
* Require Dynamips 0.2.23 and bind Dynamips hypervisor on 127.0.0.1
* Delete the built-in appliance directory before installing updated files
* Use a stock BusyBox for the Docker integration
* Overwrite built-in appliance files when starting a more recent version of the server
* Fix reset console. Fixes #1619
* Only use importlib_resources for Python <= 3.9. Fixes #2147
* Support when the user field defined in Docker container is an ID. Fixes #2134
## 3.0.0a3 27/12/2022 ## 3.0.0a3 27/12/2022
* Add web-ui v3.0.0a3 * Add web-ui v3.0.0a3

@ -247,6 +247,12 @@
} }
], ],
"versions": [ "versions": [
{
"name": "5.3.1",
"images": {
"hda_disk_image": "cumulus-linux-5.3.1-vx-amd64-qemu.qcow2"
}
},
{ {
"name": "5.1.0", "name": "5.1.0",
"images": { "images": {

@ -0,0 +1,59 @@
{
"appliance_id": "f3b6a3ac-7be5-4bb0-b204-da3712fb646c",
"name": "Windows-11-Dev-Env",
"category": "guest",
"description": "Windows 11 Developer Environment Virtual Machine.",
"vendor_name": "Microsoft",
"vendor_url": "https://www.microsoft.com",
"documentation_url": "https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/",
"product_name": "Windows 11 Development Environment",
"product_url": "https://developer.microsoft.com/en-us/windows/downloads/virtual-machines/",
"registry_version": 4,
"status": "experimental",
"availability": "free",
"maintainer": "Ean Towne",
"maintainer_email": "eantowne@gmail.com",
"usage": "Uses SPICE not VNC\nHighly recommended to install the SPICE-agent from: https://www.spice-space.org/download/windows/spice-guest-tools/spice-guest-tools-latest.exe to be able to change resolution and increase performance.\nThis is an evaluation virtual machine (90 days) and includes:\n* Window 11 Enterprise (Evaluation)\n* Visual Studio 2022 Community Edition with UWP .NET Desktop, Azure, and Windows App SDK for C# workloads enabled\n* Windows Subsystem for Linux 2 enabled with Ubuntu installed\n* Windows Terminal installed\n* Developer mode enabled",
"symbol": "microsoft.svg",
"first_port_name": "Network Adapter 1",
"port_name_format": "Network Adapter {0}",
"qemu": {
"adapter_type": "e1000",
"adapters": 1,
"ram": 4096,
"cpus": 4,
"hda_disk_interface": "sata",
"arch": "x86_64",
"console_type": "spice",
"boot_priority": "c",
"kvm": "require"
},
"images": [
{
"filename": "WinDev2212Eval-disk1.vmdk",
"version": "2212",
"md5sum": "c79f393a067b92e01a513a118d455ac8",
"filesize": 24620493824,
"download_url": "https://aka.ms/windev_VM_vmware",
"compression": "zip"
},
{
"filename": "OVMF-20160813.fd",
"version": "16.08.13",
"md5sum": "8ff0ef1ec56345db5b6bda1a8630e3c6",
"filesize": 2097152,
"download_url": "",
"direct_download_url": "https://sourceforge.net/projects/gns-3/files/Qemu%20Appliances/OVMF-20160813.fd.zip/download",
"compression": "zip"
}
],
"versions": [
{
"images": {
"bios_image": "OVMF-20160813.fd",
"hda_disk_image": "WinDev2212Eval-disk1.vmdk"
},
"name": "2212"
}
]
}

@ -92,6 +92,8 @@ class BaseNode:
self._wrap_console = wrap_console self._wrap_console = wrap_console
self._wrap_aux = wrap_aux self._wrap_aux = wrap_aux
self._wrapper_telnet_servers = [] self._wrapper_telnet_servers = []
self._wrap_console_reader = None
self._wrap_console_writer = None
self._internal_console_port = None self._internal_console_port = None
self._internal_aux_port = None self._internal_aux_port = None
self._custom_adapters = [] self._custom_adapters = []
@ -375,7 +377,6 @@ class BaseNode:
if self._wrap_console: if self._wrap_console:
self._manager.port_manager.release_tcp_port(self._internal_console_port, self._project) self._manager.port_manager.release_tcp_port(self._internal_console_port, self._project)
self._internal_console_port = None self._internal_console_port = None
if self._aux: if self._aux:
self._manager.port_manager.release_tcp_port(self._aux, self._project) self._manager.port_manager.release_tcp_port(self._aux, self._project)
self._aux = None self._aux = None
@ -415,15 +416,23 @@ class BaseNode:
remaining_trial = 60 remaining_trial = 60
while True: while True:
try: try:
(reader, writer) = await asyncio.open_connection(host="127.0.0.1", port=internal_port) (self._wrap_console_reader, self._wrap_console_writer) = await asyncio.open_connection(
host="127.0.0.1",
port=self._internal_console_port
)
break break
except (OSError, ConnectionRefusedError) as e: except (OSError, ConnectionRefusedError) as e:
if remaining_trial <= 0: if remaining_trial <= 0:
raise e raise e
await asyncio.sleep(0.1) await asyncio.sleep(0.1)
remaining_trial -= 1 remaining_trial -= 1
await AsyncioTelnetServer.write_client_intro(writer, echo=True) await AsyncioTelnetServer.write_client_intro(self._wrap_console_writer, echo=True)
server = AsyncioTelnetServer(reader=reader, writer=writer, binary=True, echo=True) server = AsyncioTelnetServer(
reader=self._wrap_console_reader,
writer=self._wrap_console_writer,
binary=True,
echo=True
)
# warning: this will raise OSError exception if there is a problem... # warning: this will raise OSError exception if there is a problem...
telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, external_port) telnet_server = await asyncio.start_server(server.run, self._manager.port_manager.console_host, external_port)
self._wrapper_telnet_servers.append(telnet_server) self._wrapper_telnet_servers.append(telnet_server)
@ -453,14 +462,17 @@ class BaseNode:
Stops the telnet proxy servers. Stops the telnet proxy servers.
""" """
if self._wrap_console_writer:
self._wrap_console_writer.close()
await self._wrap_console_writer.wait_closed()
for telnet_proxy_server in self._wrapper_telnet_servers: for telnet_proxy_server in self._wrapper_telnet_servers:
telnet_proxy_server.close() telnet_proxy_server.close()
await telnet_proxy_server.wait_closed() await telnet_proxy_server.wait_closed()
self._wrapper_telnet_servers = [] self._wrapper_telnet_servers = []
async def reset_console(self): async def reset_wrap_console(self):
""" """
Reset console Reset the wrap console (restarts the Telnet proxy)
""" """
await self.stop_wrap_console() await self.stop_wrap_console()

@ -87,5 +87,13 @@ done
ifup -a -f ifup -a -f
# continue normal docker startup # continue normal docker startup
eval HOME=$(echo ~${GNS3_USER-root}) case "$GNS3_USER" in
[1-9][0-9]*)
# for when the user field defined in the Docker container is an ID
export GNS3_USER=$(cat /etc/passwd | grep ${GNS3_USER-root} | awk -F: '{print $1}')
;;
*)
;;
esac
eval HOME="$(echo ~${GNS3_USER-root})"
exec su ${GNS3_USER-root} -p -- /gns3/run-cmd.sh "$OLD_PATH" "$@" exec su ${GNS3_USER-root} -p -- /gns3/run-cmd.sh "$OLD_PATH" "$@"

@ -278,9 +278,13 @@ class Dynamips(BaseManager):
if not working_dir: if not working_dir:
working_dir = tempfile.gettempdir() working_dir = tempfile.gettempdir()
# FIXME: hypervisor should always listen to 127.0.0.1 if not sys.platform.startswith("win"):
# See https://github.com/GNS3/dynamips/issues/62 # Hypervisor should always listen to 127.0.0.1
server_host = self.config.settings.Server.host # See https://github.com/GNS3/dynamips/issues/62
# This was fixed in Dynamips v0.2.23 which hasn't been built for Windows
server_host = "127.0.0.1"
else:
server_host = self.config.settings.Server.host
try: try:
info = socket.getaddrinfo(server_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE) info = socket.getaddrinfo(server_host, 0, socket.AF_UNSPEC, socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
@ -305,6 +309,8 @@ class Dynamips(BaseManager):
await hypervisor.connect() await hypervisor.connect()
if parse_version(hypervisor.version) < parse_version("0.2.11"): if parse_version(hypervisor.version) < parse_version("0.2.11"):
raise DynamipsError(f"Dynamips version must be >= 0.2.11, detected version is {hypervisor.version}") raise DynamipsError(f"Dynamips version must be >= 0.2.11, detected version is {hypervisor.version}")
if not sys.platform.startswith("win") and parse_version(hypervisor.version) < parse_version('0.2.23'):
raise DynamipsError(f"Dynamips version must be >= 0.2.23 on Linux/macOS, detected version is {hypervisor.version}")
return hypervisor return hypervisor

@ -95,7 +95,9 @@ class DynamipsHypervisor:
try: try:
version = await self.send("hypervisor version") version = await self.send("hypervisor version")
self._version = version[0].split("-", 1)[0] self._version = version[0].split("-", 1)[0]
log.info("Dynamips version {} detected".format(self._version))
except IndexError: except IndexError:
log.warning("Dynamips version could not be detected")
self._version = "Unknown" self._version = "Unknown"
# this forces to send the working dir to Dynamips # this forces to send the working dir to Dynamips

@ -197,11 +197,9 @@ class Hypervisor(DynamipsHypervisor):
command = [self._path] command = [self._path]
command.extend(["-N1"]) # use instance IDs for filenames command.extend(["-N1"]) # use instance IDs for filenames
command.extend(["-l", f"dynamips_i{self._id}_log.txt"]) # log file command.extend(["-l", f"dynamips_i{self._id}_log.txt"]) # log file
# Dynamips cannot listen for hypervisor commands and for console connections on if not sys.platform.startswith("win"):
# 2 different IP addresses. command.extend(["-H", f"{self._host}:{self._port}", "--console-binding-addr", self._console_host])
# See https://github.com/GNS3/dynamips/issues/62
if self._console_host != "0.0.0.0" and self._console_host != "::":
command.extend(["-H", f"{self._host}:{self._port}"])
else: else:
command.extend(["-H", str(self._port)]) command.extend(["-H", str(self._port)])
return command return command

@ -1012,11 +1012,8 @@ class Router(BaseNode):
if self.console_type != console_type: if self.console_type != console_type:
status = await self.get_status() status = await self.get_status()
if status == "running": if status == "running":
raise DynamipsError( raise DynamipsError('"{name}" must be stopped to change the console type to {console_type}'.format(name=self._name,
'"{name}" must be stopped to change the console type to {console_type}'.format( console_type=console_type))
name=self._name, console_type=console_type
)
)
self.console_type = console_type self.console_type = console_type
@ -1033,6 +1030,13 @@ class Router(BaseNode):
self.aux = aux self.aux = aux
await self._hypervisor.send(f'vm set_aux_tcp_port "{self._name}" {aux}') await self._hypervisor.send(f'vm set_aux_tcp_port "{self._name}" {aux}')
async def reset_console(self):
"""
Reset console
"""
pass # reset console is not supported with Dynamips
async def get_cpu_usage(self, cpu_id=0): async def get_cpu_usage(self, cpu_id=0):
""" """
Shows cpu usage in seconds, "cpu_id" is ignored. Shows cpu usage in seconds, "cpu_id" is ignored.

@ -15,6 +15,7 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import socket import socket
import ipaddress
from fastapi import HTTPException, status from fastapi import HTTPException, status
from gns3server.config import Config from gns3server.config import Config
@ -145,13 +146,19 @@ class PortManager:
@console_host.setter @console_host.setter
def console_host(self, new_host): def console_host(self, new_host):
""" """
Bind console host to 0.0.0.0 if remote connections are allowed. Bind console host to 0.0.0.0 or :: if remote connections are allowed.
""" """
remote_console_connections = Config.instance().settings.Server.allow_remote_console remote_console_connections = Config.instance().settings.Server.allow_remote_console
if remote_console_connections: if remote_console_connections:
log.warning("Remote console connections are allowed") log.warning("Remote console connections are allowed")
self._console_host = "0.0.0.0" self._console_host = "0.0.0.0"
try:
ip = ipaddress.ip_address(new_host)
if isinstance(ip, ipaddress.IPv6Address):
self._console_host = "::"
except ValueError:
log.warning("Could not determine IP address type for console host")
else: else:
self._console_host = new_host self._console_host = new_host

@ -107,6 +107,7 @@ class QemuVM(BaseNode):
self._monitor_host = manager.config.settings.Qemu.monitor_host self._monitor_host = manager.config.settings.Qemu.monitor_host
self._process = None self._process = None
self._cpulimit_process = None self._cpulimit_process = None
self._swtpm_process = None
self._monitor = None self._monitor = None
self._stdout_file = "" self._stdout_file = ""
self._qemu_img_stdout_file = "" self._qemu_img_stdout_file = ""
@ -151,6 +152,7 @@ class QemuVM(BaseNode):
self._kernel_image = "" self._kernel_image = ""
self._kernel_command_line = "" self._kernel_command_line = ""
self._replicate_network_connection_state = True self._replicate_network_connection_state = True
self._tpm = False
self._create_config_disk = False self._create_config_disk = False
self._on_close = "power_off" self._on_close = "power_off"
self._cpu_throttling = 0 # means no CPU throttling self._cpu_throttling = 0 # means no CPU throttling
@ -728,7 +730,7 @@ class QemuVM(BaseNode):
""" """
Sets whether a config disk is automatically created on HDD disk interface (secondary slave) Sets whether a config disk is automatically created on HDD disk interface (secondary slave)
:param replicate_network_connection_state: boolean :param create_config_disk: boolean
""" """
if create_config_disk: if create_config_disk:
@ -874,6 +876,30 @@ class QemuVM(BaseNode):
log.info(f'QEMU VM "{self._name}" [{self._id}] has set maximum number of hotpluggable vCPUs to {maxcpus}') log.info(f'QEMU VM "{self._name}" [{self._id}] has set maximum number of hotpluggable vCPUs to {maxcpus}')
self._maxcpus = maxcpus self._maxcpus = maxcpus
@property
def tpm(self):
"""
Returns whether TPM is activated for this QEMU VM.
:returns: boolean
"""
return self._tpm
@tpm.setter
def tpm(self, tpm):
"""
Sets whether TPM is activated for this QEMU VM.
:param tpm: boolean
"""
if tpm:
log.info(f'QEMU VM "{self._name}" [{self._id}] has enabled the Trusted Platform Module (TPM)')
else:
log.info(f'QEMU VM "{self._name}" [{self._id}] has disabled the Trusted Platform Module (TPM)')
self._tpm = tpm
@property @property
def options(self): def options(self):
""" """
@ -1039,11 +1065,8 @@ class QemuVM(BaseNode):
""" """
if self._cpulimit_process and self._cpulimit_process.returncode is None: if self._cpulimit_process and self._cpulimit_process.returncode is None:
self._cpulimit_process.kill() self._cpulimit_process.terminate()
try: self._cpulimit_process = None
self._process.wait(3)
except subprocess.TimeoutExpired:
log.error(f"Could not kill cpulimit process {self._cpulimit_process.pid}")
def _set_cpu_throttling(self): def _set_cpu_throttling(self):
""" """
@ -1054,10 +1077,13 @@ class QemuVM(BaseNode):
return return
try: try:
subprocess.Popen( if sys.platform.startswith("win") and hasattr(sys, "frozen"):
["cpulimit", "--lazy", f"--pid={self._process.pid}", f"--limit={self._cpu_throttling}"], cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe")
cwd=self.working_dir, else:
) cpulimit_exec = "cpulimit"
command = [cpulimit_exec, "--lazy", "--pid={}".format(self._process.pid), "--limit={}".format(self._cpu_throttling)]
self._cpulimit_process = subprocess.Popen(command, cwd=self.working_dir)
log.info(f"CPU throttled to {self._cpu_throttling}%") log.info(f"CPU throttled to {self._cpu_throttling}%")
except FileNotFoundError: except FileNotFoundError:
raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling") raise QemuError("cpulimit could not be found, please install it or deactivate CPU throttling")
@ -1134,7 +1160,8 @@ class QemuVM(BaseNode):
await self._set_process_priority() await self._set_process_priority()
if self._cpu_throttling: if self._cpu_throttling:
self._set_cpu_throttling() self._set_cpu_throttling()
if self._tpm:
self._start_swtpm()
if "-enable-kvm" in command_string or "-enable-hax" in command_string: if "-enable-kvm" in command_string or "-enable-hax" in command_string:
self._hw_virtualization = True self._hw_virtualization = True
@ -1219,6 +1246,7 @@ class QemuVM(BaseNode):
log.warning(f'QEMU VM "{self._name}" PID={self._process.pid} is still running') log.warning(f'QEMU VM "{self._name}" PID={self._process.pid} is still running')
self._process = None self._process = None
self._stop_cpulimit() self._stop_cpulimit()
self._stop_swtpm()
if self.on_close != "save_vm_state": if self.on_close != "save_vm_state":
await self._clear_save_vm_stated() await self._clear_save_vm_stated()
await self._export_config() await self._export_config()
@ -1746,6 +1774,14 @@ class QemuVM(BaseNode):
self._process = None self._process = None
return False return False
async def reset_console(self):
"""
Reset console
"""
if self.is_running():
await self.reset_wrap_console()
def command(self): def command(self):
""" """
Returns the QEMU command line. Returns the QEMU command line.
@ -2243,6 +2279,60 @@ class QemuVM(BaseNode):
return options return options
def _start_swtpm(self):
"""
Start swtpm (TPM emulator)
"""
if sys.platform.startswith("win"):
raise QemuError("swtpm (TPM emulator) is not supported on Windows")
tpm_dir = os.path.join(self.working_dir, "tpm")
os.makedirs(tpm_dir, exist_ok=True)
tpm_sock = os.path.join(self.temporary_directory, "swtpm.sock")
swtpm = shutil.which("swtpm")
if not swtpm:
raise QemuError("Could not find swtpm (TPM emulator)")
try:
command = [
swtpm,
"socket",
"--tpm2",
'--tpmstate', "dir={}".format(tpm_dir),
"--ctrl",
"type=unixio,path={},terminate".format(tpm_sock)
]
command_string = " ".join(shlex.quote(s) for s in command)
log.info("Starting swtpm (TPM emulator) with: {}".format(command_string))
self._swtpm_process = subprocess.Popen(command, cwd=self.working_dir)
log.info("swtpm (TPM emulator) has started")
except (OSError, subprocess.SubprocessError) as e:
raise QemuError("Could not start swtpm (TPM emulator): {}".format(e))
def _stop_swtpm(self):
"""
Stop swtpm (TPM emulator)
"""
if self._swtpm_process and self._swtpm_process.returncode is None:
self._swtpm_process.terminate()
self._swtpm_process = None
def _tpm_options(self):
"""
Return the TPM options for Qemu.
"""
tpm_sock = os.path.join(self.temporary_directory, "swtpm.sock")
options = [
"-chardev",
"socket,id=chrtpm,path={}".format(tpm_sock),
"-tpmdev",
"emulator,id=tpm0,chardev=chrtpm",
"-device",
"tpm-tis,tpmdev=tpm0"
]
return options
async def _network_options(self): async def _network_options(self):
network_options = [] network_options = []
@ -2495,6 +2585,8 @@ class QemuVM(BaseNode):
command.extend(await self._saved_state_option()) command.extend(await self._saved_state_option())
if self._console_type == "telnet": if self._console_type == "telnet":
command.extend(await self._disable_graphics()) command.extend(await self._disable_graphics())
if self._tpm:
command.extend(self._tpm_options())
if additional_options: if additional_options:
try: try:
command.extend(shlex.split(additional_options)) command.extend(shlex.split(additional_options))

@ -347,6 +347,14 @@ class VPCSVM(BaseNode):
return True return True
return False return False
async def reset_console(self):
"""
Reset console
"""
if self.is_running():
await self.reset_wrap_console()
@BaseNode.console_type.setter @BaseNode.console_type.setter
def console_type(self, new_console_type): def console_type(self, new_console_type):
""" """

@ -18,13 +18,19 @@
import os import os
import sys import sys
import uuid import uuid
import socket
import shutil import shutil
import asyncio import asyncio
import random import random
import importlib_resources
try:
import importlib_resources
except ImportError:
from importlib import resources as importlib_resources
from ..config import Config from ..config import Config
from ..utils import parse_version
from .project import Project from .project import Project
from .appliance import Appliance from .appliance import Appliance
from .appliance_manager import ApplianceManager from .appliance_manager import ApplianceManager
@ -62,7 +68,7 @@ class Controller:
async def start(self, computes=None): async def start(self, computes=None):
log.info("Controller is starting") log.info("Controller is starting")
self._load_base_files() self._install_base_configs()
server_config = Config.instance().settings.Server server_config = Config.instance().settings.Server
Config.instance().listen_for_config_changes(self._update_config) Config.instance().listen_for_config_changes(self._update_config)
name = server_config.name name = server_config.name
@ -282,6 +288,10 @@ class Controller:
except OSError as e: except OSError as e:
log.error(f"Cannot read Etag appliance file '{etag_appliances_path}': {e}") log.error(f"Cannot read Etag appliance file '{etag_appliances_path}': {e}")
# FIXME
#if parse_version(__version__) > parse_version(controller_settings.get("version", "")):
# self._appliance_manager.install_builtin_appliances()
self._appliance_manager.install_builtin_appliances() self._appliance_manager.install_builtin_appliances()
self._appliance_manager.load_appliances() self._appliance_manager.load_appliances()
self._config_loaded = True self._config_loaded = True
@ -307,13 +317,14 @@ class Controller:
except OSError as e: except OSError as e:
log.error(str(e)) log.error(str(e))
def _load_base_files(self): def _install_base_configs(self):
""" """
At startup we copy base file to the user location to allow At startup we copy base file to the user location to allow
them to customize it them to customize it
""" """
dst_path = self.configs_path() dst_path = self.configs_path()
log.info(f"Installing base configs in '{dst_path}'")
try: try:
if hasattr(sys, "frozen") and sys.platform.startswith("win"): if hasattr(sys, "frozen") and sys.platform.startswith("win"):
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "configs")) resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "configs"))

@ -20,9 +20,14 @@ import os
import json import json
import asyncio import asyncio
import aiofiles import aiofiles
import importlib_resources
import shutil import shutil
try:
import importlib_resources
except ImportError:
from importlib import resources as importlib_resources
from typing import Tuple, List from typing import Tuple, List
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
@ -94,13 +99,15 @@ class ApplianceManager:
os.makedirs(appliances_path, exist_ok=True) os.makedirs(appliances_path, exist_ok=True)
return appliances_path return appliances_path
def _builtin_appliances_path(self): def _builtin_appliances_path(self, delete_first=False):
""" """
Get the built-in appliance storage directory Get the built-in appliance storage directory
""" """
config = Config.instance() config = Config.instance()
appliances_dir = os.path.join(config.config_dir, "appliances") appliances_dir = os.path.join(config.config_dir, "appliances")
if delete_first:
shutil.rmtree(appliances_dir, ignore_errors=True)
os.makedirs(appliances_dir, exist_ok=True) os.makedirs(appliances_dir, exist_ok=True)
return appliances_dir return appliances_dir
@ -109,17 +116,17 @@ class ApplianceManager:
At startup we copy the built-in appliances files. At startup we copy the built-in appliances files.
""" """
dst_path = self._builtin_appliances_path() dst_path = self._builtin_appliances_path(delete_first=True)
log.info(f"Installing built-in appliances in '{dst_path}'")
try: try:
if hasattr(sys, "frozen") and sys.platform.startswith("win"): if hasattr(sys, "frozen") and sys.platform.startswith("win"):
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "appliances")) resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "appliances"))
for filename in os.listdir(resource_path): for filename in os.listdir(resource_path):
if not os.path.exists(os.path.join(dst_path, filename)): shutil.copy(os.path.join(resource_path, filename), os.path.join(dst_path, filename))
shutil.copy(os.path.join(resource_path, filename), os.path.join(dst_path, filename))
else: else:
for entry in importlib_resources.files('gns3server.appliances').iterdir(): for entry in importlib_resources.files('gns3server.appliances').iterdir():
full_path = os.path.join(dst_path, entry.name) full_path = os.path.join(dst_path, entry.name)
if entry.is_file() and not os.path.exists(full_path): if entry.is_file():
log.debug(f"Installing built-in appliance file {entry.name} to {full_path}") log.debug(f"Installing built-in appliance file {entry.name} to {full_path}")
shutil.copy(str(entry), os.path.join(dst_path, entry.name)) shutil.copy(str(entry), os.path.join(dst_path, entry.name))
except OSError as e: except OSError as e:

@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
class Node: class Node:
# This properties are used only on controller and are not forwarded to the compute # These properties are used only on controller and are not forwarded to the compute
CONTROLLER_ONLY_PROPERTIES = [ CONTROLLER_ONLY_PROPERTIES = [
"x", "x",
"y", "y",

File diff suppressed because one or more lines are too long

@ -200,6 +200,7 @@ class AsyncioTelnetServer:
except ConnectionError: except ConnectionError:
async with self._lock: async with self._lock:
network_writer.close() network_writer.close()
await network_writer.wait_closed()
if self._reader_process == network_reader: if self._reader_process == network_reader:
self._reader_process = None self._reader_process = None
# Cancel current read from this reader # Cancel current read from this reader
@ -214,6 +215,8 @@ class AsyncioTelnetServer:
try: try:
writer.write_eof() writer.write_eof()
await writer.drain() await writer.drain()
writer.close()
await writer.wait_closed()
except (AttributeError, ConnectionError): except (AttributeError, ConnectionError):
continue continue

@ -18,7 +18,12 @@ import atexit
import logging import logging
import os import os
import sys import sys
import importlib_resources
try:
import importlib_resources
except ImportError:
from importlib import resources as importlib_resources
from contextlib import ExitStack from contextlib import ExitStack
resource_manager = ExitStack() resource_manager = ExitStack()

@ -176,7 +176,7 @@ async def test_termination_callback(vm):
await vm._termination_callback(0) await vm._termination_callback(0)
assert vm.status == "stopped" assert vm.status == "stopped"
await queue.get(1) #  Ping await queue.get(1) # Ping
(action, event, kwargs) = await queue.get(1) (action, event, kwargs) = await queue.get(1)
assert action == "node.updated" assert action == "node.updated"
@ -405,6 +405,17 @@ async def test_spice_option(vm, fake_qemu_img_binary):
assert '-vga qxl' in ' '.join(options) assert '-vga qxl' in ' '.join(options)
@pytest.mark.asyncio
async def test_tpm_option(vm, tmpdir, fake_qemu_img_binary):
vm.manager.get_qemu_version = AsyncioMagicMock(return_value="3.1.0")
vm._tpm = True
tpm_sock = os.path.join(vm.temporary_directory, "swtpm.sock")
options = await vm._build_command()
assert '-chardev socket,id=chrtpm,path={}'.format(tpm_sock) in ' '.join(options)
assert '-tpmdev emulator,id=tpm0,chardev=chrtpm' in ' '.join(options)
assert '-device tpm-tis,tpmdev=tpm0' in ' '.join(options)
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary): async def test_disk_options_multiple_disk(vm, tmpdir, fake_qemu_img_binary):

@ -350,13 +350,13 @@ async def test_get_free_project_name(controller):
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_load_base_files(controller, config, tmpdir): async def test_install_base_configs(controller, config, tmpdir):
config.settings.Server.configs_path = str(tmpdir) config.settings.Server.configs_path = str(tmpdir)
with open(str(tmpdir / 'iou_l2_base_startup-config.txt'), 'w+') as f: with open(str(tmpdir / 'iou_l2_base_startup-config.txt'), 'w+') as f:
f.write('test') f.write('test')
controller._load_base_files() controller._install_base_configs()
assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt')) assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt'))
# Check is the file has not been overwritten # Check is the file has not been overwritten
@ -379,9 +379,10 @@ def test_appliances(controller, config, tmpdir):
with open(str(tmpdir / "my_appliance2.gns3a"), 'w+') as f: with open(str(tmpdir / "my_appliance2.gns3a"), 'w+') as f:
json.dump(my_appliance, f) json.dump(my_appliance, f)
config.settings.Server.appliances_path = str(tmpdir) #config.settings.Server.appliances_path = str(tmpdir)
controller.appliance_manager.install_builtin_appliances() controller.appliance_manager.install_builtin_appliances()
controller.appliance_manager.load_appliances() with patch("gns3server.config.Config.get_section_config", return_value={"appliances_path": str(tmpdir)}):
controller.appliance_manager.load_appliances()
assert len(controller.appliance_manager.appliances) > 0 assert len(controller.appliance_manager.appliances) > 0
for appliance in controller.appliance_manager.appliances.values(): for appliance in controller.appliance_manager.appliances.values():
assert appliance.asdict()["status"] != "broken" assert appliance.asdict()["status"] != "broken"

Loading…
Cancel
Save