mirror of
https://github.com/GNS3/gns3-server
synced 2025-01-12 09:00:57 +00:00
Merge 2.2
This commit is contained in:
commit
27d9063e56
12
CHANGELOG
12
CHANGELOG
@ -1,5 +1,17 @@
|
||||
# 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
|
||||
|
||||
* Add web-ui v3.0.0a3
|
||||
|
0
README.rst
Normal file
0
README.rst
Normal file
@ -247,6 +247,12 @@
|
||||
}
|
||||
],
|
||||
"versions": [
|
||||
{
|
||||
"name": "5.3.1",
|
||||
"images": {
|
||||
"hda_disk_image": "cumulus-linux-5.3.1-vx-amd64-qemu.qcow2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "5.1.0",
|
||||
"images": {
|
||||
|
59
gns3server/appliances/windows-11-dev-env.gns3a
Normal file
59
gns3server/appliances/windows-11-dev-env.gns3a
Normal file
@ -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_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 = []
|
||||
@ -375,7 +377,6 @@ class BaseNode:
|
||||
if self._wrap_console:
|
||||
self._manager.port_manager.release_tcp_port(self._internal_console_port, self._project)
|
||||
self._internal_console_port = None
|
||||
|
||||
if self._aux:
|
||||
self._manager.port_manager.release_tcp_port(self._aux, self._project)
|
||||
self._aux = None
|
||||
@ -415,15 +416,23 @@ class BaseNode:
|
||||
remaining_trial = 60
|
||||
while True:
|
||||
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
|
||||
except (OSError, ConnectionRefusedError) as e:
|
||||
if remaining_trial <= 0:
|
||||
raise e
|
||||
await asyncio.sleep(0.1)
|
||||
remaining_trial -= 1
|
||||
await AsyncioTelnetServer.write_client_intro(writer, echo=True)
|
||||
server = AsyncioTelnetServer(reader=reader, writer=writer, binary=True, echo=True)
|
||||
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
|
||||
)
|
||||
# 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)
|
||||
self._wrapper_telnet_servers.append(telnet_server)
|
||||
@ -453,14 +462,17 @@ class BaseNode:
|
||||
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:
|
||||
telnet_proxy_server.close()
|
||||
await telnet_proxy_server.wait_closed()
|
||||
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()
|
||||
|
@ -87,5 +87,13 @@ done
|
||||
ifup -a -f
|
||||
|
||||
# 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" "$@"
|
||||
|
@ -278,8 +278,12 @@ class Dynamips(BaseManager):
|
||||
if not working_dir:
|
||||
working_dir = tempfile.gettempdir()
|
||||
|
||||
# FIXME: hypervisor should always listen to 127.0.0.1
|
||||
if not sys.platform.startswith("win"):
|
||||
# Hypervisor should always listen to 127.0.0.1
|
||||
# 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:
|
||||
@ -305,6 +309,8 @@ class Dynamips(BaseManager):
|
||||
await hypervisor.connect()
|
||||
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}")
|
||||
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
|
||||
|
||||
|
@ -95,7 +95,9 @@ class DynamipsHypervisor:
|
||||
try:
|
||||
version = await self.send("hypervisor version")
|
||||
self._version = version[0].split("-", 1)[0]
|
||||
log.info("Dynamips version {} detected".format(self._version))
|
||||
except IndexError:
|
||||
log.warning("Dynamips version could not be detected")
|
||||
self._version = "Unknown"
|
||||
|
||||
# this forces to send the working dir to Dynamips
|
||||
|
@ -197,11 +197,9 @@ class Hypervisor(DynamipsHypervisor):
|
||||
command = [self._path]
|
||||
command.extend(["-N1"]) # use instance IDs for filenames
|
||||
command.extend(["-l", f"dynamips_i{self._id}_log.txt"]) # log file
|
||||
# Dynamips cannot listen for hypervisor commands and for console connections on
|
||||
# 2 different IP addresses.
|
||||
# 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}"])
|
||||
if not sys.platform.startswith("win"):
|
||||
command.extend(["-H", f"{self._host}:{self._port}", "--console-binding-addr", self._console_host])
|
||||
else:
|
||||
command.extend(["-H", str(self._port)])
|
||||
|
||||
return command
|
||||
|
@ -1012,11 +1012,8 @@ class Router(BaseNode):
|
||||
if self.console_type != console_type:
|
||||
status = await self.get_status()
|
||||
if status == "running":
|
||||
raise DynamipsError(
|
||||
'"{name}" must be stopped to change the console type to {console_type}'.format(
|
||||
name=self._name, console_type=console_type
|
||||
)
|
||||
)
|
||||
raise DynamipsError('"{name}" must be stopped to change the console type to {console_type}'.format(name=self._name,
|
||||
console_type=console_type))
|
||||
|
||||
self.console_type = console_type
|
||||
|
||||
@ -1033,6 +1030,13 @@ class Router(BaseNode):
|
||||
self.aux = 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):
|
||||
"""
|
||||
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/>.
|
||||
|
||||
import socket
|
||||
import ipaddress
|
||||
from fastapi import HTTPException, status
|
||||
from gns3server.config import Config
|
||||
|
||||
@ -145,13 +146,19 @@ class PortManager:
|
||||
@console_host.setter
|
||||
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
|
||||
if remote_console_connections:
|
||||
log.warning("Remote console connections are allowed")
|
||||
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:
|
||||
self._console_host = new_host
|
||||
|
||||
|
@ -107,6 +107,7 @@ class QemuVM(BaseNode):
|
||||
self._monitor_host = manager.config.settings.Qemu.monitor_host
|
||||
self._process = None
|
||||
self._cpulimit_process = None
|
||||
self._swtpm_process = None
|
||||
self._monitor = None
|
||||
self._stdout_file = ""
|
||||
self._qemu_img_stdout_file = ""
|
||||
@ -151,6 +152,7 @@ class QemuVM(BaseNode):
|
||||
self._kernel_image = ""
|
||||
self._kernel_command_line = ""
|
||||
self._replicate_network_connection_state = True
|
||||
self._tpm = False
|
||||
self._create_config_disk = False
|
||||
self._on_close = "power_off"
|
||||
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)
|
||||
|
||||
:param replicate_network_connection_state: boolean
|
||||
:param create_config_disk: boolean
|
||||
"""
|
||||
|
||||
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}')
|
||||
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
|
||||
def options(self):
|
||||
"""
|
||||
@ -1039,11 +1065,8 @@ class QemuVM(BaseNode):
|
||||
"""
|
||||
|
||||
if self._cpulimit_process and self._cpulimit_process.returncode is None:
|
||||
self._cpulimit_process.kill()
|
||||
try:
|
||||
self._process.wait(3)
|
||||
except subprocess.TimeoutExpired:
|
||||
log.error(f"Could not kill cpulimit process {self._cpulimit_process.pid}")
|
||||
self._cpulimit_process.terminate()
|
||||
self._cpulimit_process = None
|
||||
|
||||
def _set_cpu_throttling(self):
|
||||
"""
|
||||
@ -1054,10 +1077,13 @@ class QemuVM(BaseNode):
|
||||
return
|
||||
|
||||
try:
|
||||
subprocess.Popen(
|
||||
["cpulimit", "--lazy", f"--pid={self._process.pid}", f"--limit={self._cpu_throttling}"],
|
||||
cwd=self.working_dir,
|
||||
)
|
||||
if sys.platform.startswith("win") and hasattr(sys, "frozen"):
|
||||
cpulimit_exec = os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "cpulimit", "cpulimit.exe")
|
||||
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}%")
|
||||
except FileNotFoundError:
|
||||
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()
|
||||
if self._cpu_throttling:
|
||||
self._set_cpu_throttling()
|
||||
|
||||
if self._tpm:
|
||||
self._start_swtpm()
|
||||
if "-enable-kvm" in command_string or "-enable-hax" in command_string:
|
||||
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')
|
||||
self._process = None
|
||||
self._stop_cpulimit()
|
||||
self._stop_swtpm()
|
||||
if self.on_close != "save_vm_state":
|
||||
await self._clear_save_vm_stated()
|
||||
await self._export_config()
|
||||
@ -1746,6 +1774,14 @@ class QemuVM(BaseNode):
|
||||
self._process = None
|
||||
return False
|
||||
|
||||
async def reset_console(self):
|
||||
"""
|
||||
Reset console
|
||||
"""
|
||||
|
||||
if self.is_running():
|
||||
await self.reset_wrap_console()
|
||||
|
||||
def command(self):
|
||||
"""
|
||||
Returns the QEMU command line.
|
||||
@ -2243,6 +2279,60 @@ class QemuVM(BaseNode):
|
||||
|
||||
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):
|
||||
|
||||
network_options = []
|
||||
@ -2495,6 +2585,8 @@ class QemuVM(BaseNode):
|
||||
command.extend(await self._saved_state_option())
|
||||
if self._console_type == "telnet":
|
||||
command.extend(await self._disable_graphics())
|
||||
if self._tpm:
|
||||
command.extend(self._tpm_options())
|
||||
if additional_options:
|
||||
try:
|
||||
command.extend(shlex.split(additional_options))
|
||||
|
@ -347,6 +347,14 @@ class VPCSVM(BaseNode):
|
||||
return True
|
||||
return False
|
||||
|
||||
async def reset_console(self):
|
||||
"""
|
||||
Reset console
|
||||
"""
|
||||
|
||||
if self.is_running():
|
||||
await self.reset_wrap_console()
|
||||
|
||||
@BaseNode.console_type.setter
|
||||
def console_type(self, new_console_type):
|
||||
"""
|
||||
|
@ -18,13 +18,19 @@
|
||||
import os
|
||||
import sys
|
||||
import uuid
|
||||
import socket
|
||||
import shutil
|
||||
import asyncio
|
||||
import random
|
||||
|
||||
try:
|
||||
import importlib_resources
|
||||
except ImportError:
|
||||
from importlib import resources as importlib_resources
|
||||
|
||||
|
||||
from ..config import Config
|
||||
from ..utils import parse_version
|
||||
|
||||
from .project import Project
|
||||
from .appliance import Appliance
|
||||
from .appliance_manager import ApplianceManager
|
||||
@ -62,7 +68,7 @@ class Controller:
|
||||
async def start(self, computes=None):
|
||||
|
||||
log.info("Controller is starting")
|
||||
self._load_base_files()
|
||||
self._install_base_configs()
|
||||
server_config = Config.instance().settings.Server
|
||||
Config.instance().listen_for_config_changes(self._update_config)
|
||||
name = server_config.name
|
||||
@ -282,6 +288,10 @@ class Controller:
|
||||
except OSError as 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.load_appliances()
|
||||
self._config_loaded = True
|
||||
@ -307,13 +317,14 @@ class Controller:
|
||||
except OSError as 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
|
||||
them to customize it
|
||||
"""
|
||||
|
||||
dst_path = self.configs_path()
|
||||
log.info(f"Installing base configs in '{dst_path}'")
|
||||
try:
|
||||
if hasattr(sys, "frozen") and sys.platform.startswith("win"):
|
||||
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "configs"))
|
||||
|
@ -20,9 +20,14 @@ import os
|
||||
import json
|
||||
import asyncio
|
||||
import aiofiles
|
||||
import importlib_resources
|
||||
import shutil
|
||||
|
||||
try:
|
||||
import importlib_resources
|
||||
except ImportError:
|
||||
from importlib import resources as importlib_resources
|
||||
|
||||
|
||||
from typing import Tuple, List
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
|
||||
@ -94,13 +99,15 @@ class ApplianceManager:
|
||||
os.makedirs(appliances_path, exist_ok=True)
|
||||
return appliances_path
|
||||
|
||||
def _builtin_appliances_path(self):
|
||||
def _builtin_appliances_path(self, delete_first=False):
|
||||
"""
|
||||
Get the built-in appliance storage directory
|
||||
"""
|
||||
|
||||
config = Config.instance()
|
||||
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)
|
||||
return appliances_dir
|
||||
|
||||
@ -109,17 +116,17 @@ class ApplianceManager:
|
||||
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:
|
||||
if hasattr(sys, "frozen") and sys.platform.startswith("win"):
|
||||
resource_path = os.path.normpath(os.path.join(os.path.dirname(sys.executable), "appliances"))
|
||||
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))
|
||||
else:
|
||||
for entry in importlib_resources.files('gns3server.appliances').iterdir():
|
||||
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}")
|
||||
shutil.copy(str(entry), os.path.join(dst_path, entry.name))
|
||||
except OSError as e:
|
||||
|
@ -39,7 +39,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
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 = [
|
||||
"x",
|
||||
"y",
|
||||
|
File diff suppressed because one or more lines are too long
@ -200,6 +200,7 @@ class AsyncioTelnetServer:
|
||||
except ConnectionError:
|
||||
async with self._lock:
|
||||
network_writer.close()
|
||||
await network_writer.wait_closed()
|
||||
if self._reader_process == network_reader:
|
||||
self._reader_process = None
|
||||
# Cancel current read from this reader
|
||||
@ -214,6 +215,8 @@ class AsyncioTelnetServer:
|
||||
try:
|
||||
writer.write_eof()
|
||||
await writer.drain()
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except (AttributeError, ConnectionError):
|
||||
continue
|
||||
|
||||
|
@ -18,7 +18,12 @@ import atexit
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
try:
|
||||
import importlib_resources
|
||||
except ImportError:
|
||||
from importlib import resources as importlib_resources
|
||||
|
||||
|
||||
from contextlib import ExitStack
|
||||
resource_manager = ExitStack()
|
||||
|
@ -176,7 +176,7 @@ async def test_termination_callback(vm):
|
||||
await vm._termination_callback(0)
|
||||
assert vm.status == "stopped"
|
||||
|
||||
await queue.get(1) # Ping
|
||||
await queue.get(1) # Ping
|
||||
|
||||
(action, event, kwargs) = await queue.get(1)
|
||||
assert action == "node.updated"
|
||||
@ -405,6 +405,17 @@ async def test_spice_option(vm, fake_qemu_img_binary):
|
||||
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
|
||||
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
|
||||
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)
|
||||
with open(str(tmpdir / 'iou_l2_base_startup-config.txt'), 'w+') as f:
|
||||
f.write('test')
|
||||
|
||||
controller._load_base_files()
|
||||
controller._install_base_configs()
|
||||
assert os.path.exists(str(tmpdir / 'iou_l3_base_startup-config.txt'))
|
||||
|
||||
# Check is the file has not been overwritten
|
||||
@ -379,8 +379,9 @@ def test_appliances(controller, config, tmpdir):
|
||||
with open(str(tmpdir / "my_appliance2.gns3a"), 'w+') as 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()
|
||||
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
|
||||
for appliance in controller.appliance_manager.appliances.values():
|
||||
|
Loading…
Reference in New Issue
Block a user