mirror of
https://github.com/GNS3/gns3-server
synced 2025-01-27 00:11:07 +00:00
Merge 2.2
This commit is contained in:
commit
27d9063e56
12
CHANGELOG
12
CHANGELOG
@ -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
|
||||||
|
0
README.rst
Normal file
0
README.rst
Normal file
@ -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": {
|
||||||
|
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_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…
Reference in New Issue
Block a user