refactor: ruff autofixes

This commit is contained in:
Mark Paronyan committed 2026-09-27 15:55:10 +03:00
1 parent 2ac637c662
commit a0450b6bce
455 files changed
+12322 -14762

No files matched your search

+1
View File
@@ -34,5 +34,6 @@ if (
# IOU & Docker only runs on Linux but test suite works on UNIX platform
from .docker import Docker
from .iou import IOU
MODULES.append(Docker)
MODULES.append(IOU)
-1
View File
@@ -16,7 +16,6 @@
class Adapter:
"""
Base class for adapters.
@@ -18,7 +18,6 @@ from .adapter import Adapter
class EthernetAdapter(Adapter):
"""
Ethernet adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class SerialAdapter(Adapter):
"""
Serial adapter.
"""
+2 -4
View File
@@ -50,7 +50,6 @@ CHUNK_SIZE = 1024 * 8 # 8KB
class BaseManager:
"""
Base class for all Manager classes.
Responsible of management of a node pool of the same type.
@@ -222,7 +221,7 @@ class BaseManager:
if hasattr(source_node, "status") and source_node.status != "stopped":
raise ComputeError("Cannot duplicate node data while the node is running")
destination_dir = destination_node.working_dir
try:
shutil.rmtree(destination_dir)
@@ -431,7 +430,6 @@ class BaseManager:
)
if not os.path.isabs(orig_path):
for directory in valid_directory_prefices:
log.debug(f"Searching for image '{orig_path}' in '{directory}'")
path = self._recursive_search_file_in_directory(directory, orig_path)
@@ -466,7 +464,7 @@ class BaseManager:
for root, dirs, files in os.walk(directory):
for file in files:
if s[1] == file and (s[0] == '' or root == os.path.join(directory, s[0])):
if s[1] == file and (s[0] == "" or root == os.path.join(directory, s[0])):
path = os.path.normpath(os.path.join(root, s[1]))
if os.path.exists(path):
return path
+29 -34
View File
@@ -46,7 +46,6 @@ log = logging.getLogger(__name__)
class BaseNode:
"""
Base node implementation.
@@ -460,8 +459,7 @@ class BaseNode:
while True:
try:
(self._wrap_console_reader, self._wrap_console_writer) = await asyncio.open_connection(
host="127.0.0.1",
port=internal_port
host="127.0.0.1", port=internal_port
)
break
except (OSError, ConnectionRefusedError) as e:
@@ -472,10 +470,7 @@ class BaseNode:
if console_type == "telnet":
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
reader=self._wrap_console_reader, writer=self._wrap_console_writer, binary=True, echo=True
)
elif console_type == "ssh":
server = AsyncioSSHServer(reader=self._wrap_console_reader, writer=self._wrap_console_writer)
@@ -543,8 +538,7 @@ class BaseNode:
"""
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute"
f" console WebSocket"
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute console WebSocket"
)
if self.status != "started":
@@ -555,8 +549,7 @@ class BaseNode:
if self._console_type not in ("telnet", "ssh", "docker_exec"):
await websocket.close(code=1000)
log.warning(
f"Cannot open console WebSocket: node {self.name} console type '{self._console_type}' "
f"is not supported"
f"Cannot open console WebSocket: node {self.name} console type '{self._console_type}' is not supported"
)
return
@@ -609,8 +602,10 @@ class BaseNode:
return None
cols, rows = message.get("cols"), message.get("rows")
if (
isinstance(cols, int) and not isinstance(cols, bool)
and isinstance(rows, int) and not isinstance(rows, bool)
isinstance(cols, int)
and not isinstance(cols, bool)
and isinstance(rows, int)
and not isinstance(rows, bool)
and 2 <= cols <= 5000
and 2 <= rows <= 100000
):
@@ -720,8 +715,7 @@ class BaseNode:
"""
log.info(
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute "
f"VNC console WebSocket"
f"New client {websocket.client.host}:{websocket.client.port} has connected to compute VNC console WebSocket"
)
if self.status != "started":
@@ -730,15 +724,13 @@ class BaseNode:
return
if self._console_type != "vnc":
await websocket.close(code=1000)
log.warning(
f"Cannot open VNC WebSocket: node {self.name} console type '{self._console_type}' is not vnc"
)
log.warning(f"Cannot open VNC WebSocket: node {self.name} console type '{self._console_type}' is not vnc")
return
try:
vnc_reader, vnc_writer = await asyncio.open_connection(
self._manager.port_manager.console_host,
self.console # VNC port
self.console, # VNC port
)
log.info(f"Connected to VNC server {self._manager.port_manager.console_host}:{self.console}")
except ConnectionError as e:
@@ -909,9 +901,7 @@ class BaseNode:
elif console_type == "vnc":
vnc_console_start_port_range, vnc_console_end_port_range = self._get_vnc_console_port_range()
self._console = self._manager.port_manager.get_free_tcp_port(
self._project,
vnc_console_start_port_range,
vnc_console_end_port_range
self._project, vnc_console_start_port_range, vnc_console_end_port_range
)
else:
self._console = self._manager.port_manager.get_free_tcp_port(self._project)
@@ -1044,9 +1034,7 @@ class BaseNode:
log.debug(f"Starting new uBridge hypervisor at {self._ubridge_hypervisor.endpoint}")
await self._ubridge_hypervisor.start()
if self._ubridge_hypervisor:
log.info(
f"Hypervisor at {self._ubridge_hypervisor.endpoint} has successfully started"
)
log.info(f"Hypervisor at {self._ubridge_hypervisor.endpoint} has successfully started")
await self._ubridge_hypervisor.connect()
# Tell this uBridge where to send MARK signals and which node id to
# tag them with. Marker is opt-in and inert until a `mark` filter is
@@ -1181,7 +1169,7 @@ class BaseNode:
"""
i = 0
for (filter_type, values) in filters.items():
for filter_type, values in filters.items():
if isinstance(values[0], str):
for line in values[0].split("\n"):
line = line.strip()
@@ -1217,7 +1205,9 @@ class BaseNode:
dlt = dlt[4:]
return None if dlt == "EN10MB" else dlt
async def _ubridge_add_marker_filter(self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None, data_link_type=None):
async def _ubridge_add_marker_filter(
self, bridge_name, name, bpf, pcap_path, tag=None, link_id=None, direction=None, data_link_type=None
):
"""
Attach a `mark` packet filter to a uBridge bridge for traffic insight.
@@ -1246,9 +1236,7 @@ class BaseNode:
# so allow up to 48 here.
if not _MARKER_NAME_RE.match(name) or len(name) > 48:
raise UbridgeError(f"Invalid marker name: {name!r}")
cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format(
bridge=bridge_name, name=name, bpf=bpf
)
cmd = 'bridge add_packet_filter {bridge} {name} mark "{bpf}"'.format(bridge=bridge_name, name=name, bpf=bpf)
if tag is not None:
cmd += f" tag {tag}"
# Per-link attribution (contract §3.2): when one ubridge bridge serves
@@ -1353,7 +1341,7 @@ class BaseNode:
"""
from gns3server.compute.marker.marker_manager import MarkerManager
markers = nio.markers if hasattr(nio, 'markers') else {}
markers = nio.markers if hasattr(nio, "markers") else {}
manager = MarkerManager.instance()
markers_dir = self.project.markers_working_directory()
desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()}
@@ -1404,9 +1392,16 @@ class BaseNode:
continue # unchanged
pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap")
try:
await self._ubridge_add_marker_filter(bridge_name, name, bpf, pcap_path, tag, link_id,
direction=spec.get("direction"),
data_link_type=spec.get("data_link_type"))
await self._ubridge_add_marker_filter(
bridge_name,
name,
bpf,
pcap_path,
tag,
link_id,
direction=spec.get("direction"),
data_link_type=spec.get("data_link_type"),
)
except UbridgeError as e:
# Swallow BPF compile errors (warn + skip) so a single bad
# expression can't break link creation / node restart — mirrors
-2
View File
@@ -18,7 +18,6 @@
Builtin nodes server module.
"""
from ..base_manager import BaseManager
from .builtin_node_factory import BuiltinNodeFactory, BUILTIN_NODES
@@ -28,7 +27,6 @@ log = logging.getLogger(__name__)
class Builtin(BaseManager):
_NODE_CLASS = BuiltinNodeFactory
def __init__(self):
@@ -29,7 +29,6 @@ BUILTIN_NODES = {"cloud": Cloud, "nat": Nat, "ethernet_hub": EthernetHub, "ether
class BuiltinNodeFactory:
"""
Factory to create an builtin object based on the node type.
"""
+1 -6
View File
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
class Cloud(BaseNode):
"""
Cloud.
@@ -317,7 +316,6 @@ class Cloud(BaseNode):
await self._ubridge_apply_filters(bridge_name, nio.filters)
await self._ubridge_apply_markers(bridge_name, nio)
if port_info["type"] in ("ethernet", "tap"):
if not self.manager.has_privileged_access(self.ubridge_path):
raise NodeError(
"uBridge requires root access or the capability to interact with Ethernet and TAP adapters"
@@ -339,9 +337,7 @@ class Cloud(BaseNode):
elif port_info["type"] == "tap":
await self._ubridge_send(
'bridge add_nio_tap {name} "{interface}"'.format(
name=bridge_name, interface=port_info["interface"]
)
'bridge add_nio_tap {name} "{interface}"'.format(name=bridge_name, interface=port_info["interface"])
)
elif port_info["type"] == "udp":
@@ -367,7 +363,6 @@ class Cloud(BaseNode):
interface = port_info["interface"]
if gns3server.utils.interfaces.is_interface_bridge(interface):
network_interfaces = [interface["name"] for interface in self._interfaces()]
i = 0
while True:
@@ -24,7 +24,6 @@ log = logging.getLogger(__name__)
class EthernetHub(BaseNode):
"""
Ethernet hub
@@ -40,12 +39,7 @@ class EthernetHub(BaseNode):
def asdict(self):
return {
"name": self.name,
"usage": self.usage,
"node_id": self.id,
"project_id": self.project.id
}
return {"name": self.name, "usage": self.usage, "node_id": self.id, "project_id": self.project.id}
async def create(self):
"""
@@ -55,7 +55,6 @@ _QINQ_ETHERTYPE = "0x88a8"
class EthernetSwitch(BaseNode):
"""
Ethernet switch.
@@ -74,8 +73,8 @@ class EthernetSwitch(BaseNode):
self._ubridge_require_privileged_access = True
self._nios = {}
self._tap_by_port = {} # port_number -> kernel TAP enslaved to the bridge
self._bridge_name = None # kernel bridge interface name (allocated on start)
self._tap_by_port = {} # port_number -> kernel TAP enslaved to the bridge
self._bridge_name = None # kernel bridge interface name (allocated on start)
self._bridge_created = False
self._bridge_proto_set = False # whether ``brctl setvlanproto`` has been applied
# Idempotency flag for start(). Decoupled from ``status`` so the node can
+9 -5
View File
@@ -38,14 +38,16 @@ class Nat(Cloud):
allowed_interfaces = Config.instance().settings.Server.allowed_interfaces
if allowed_interfaces and isinstance(allowed_interfaces, str):
allowed_interfaces = allowed_interfaces.split(',')
allowed_interfaces = allowed_interfaces.split(",")
if sys.platform.startswith("linux"):
nat_interface = Config.instance().settings.Server.default_nat_interface
if not nat_interface:
nat_interface = "virbr0"
if allowed_interfaces and nat_interface not in allowed_interfaces:
raise NodeError("NAT interface {} is not allowed be used on this server. "
"Please check the server configuration file.".format(nat_interface))
raise NodeError(
"NAT interface {} is not allowed be used on this server. "
"Please check the server configuration file.".format(nat_interface)
)
if nat_interface not in [interface["name"] for interface in gns3server.utils.interfaces.interfaces()]:
raise NodeError(f"NAT interface {nat_interface} is missing, please install libvirt")
interface = nat_interface
@@ -54,8 +56,10 @@ class Nat(Cloud):
if not nat_interface:
nat_interface = "vmnet8"
if allowed_interfaces and nat_interface not in allowed_interfaces:
raise NodeError("NAT interface {} is not allowed be used on this server. "
"Please check the server configuration file.".format(nat_interface))
raise NodeError(
"NAT interface {} is not allowed be used on this server. "
"Please check the server configuration file.".format(nat_interface)
)
interfaces = list(
filter(
lambda x: nat_interface in x.lower(),
+22 -18
View File
@@ -34,7 +34,12 @@ from gns3server.compute.base_manager import BaseManager
from gns3server.compute.docker.docker_vm import DockerVM
from gns3server.compute.docker.vendor_docker_vm import VendorDockerVM
from gns3server.compute.docker.iol_docker_vm import IOLDockerVM
from gns3server.compute.docker.docker_error import DockerError, DockerHttp304Error, DockerHttp404Error, DockerHttp409Error
from gns3server.compute.docker.docker_error import (
DockerError,
DockerHttp304Error,
DockerHttp404Error,
DockerHttp409Error,
)
log = logging.getLogger(__name__)
@@ -47,7 +52,6 @@ CHUNK_SIZE = 1024 * 8 # 8KB
class Docker(BaseManager):
_NODE_CLASS = DockerVM
def __init__(self):
@@ -97,10 +101,7 @@ class Docker(BaseManager):
# check that busybox is statically linked
# (dynamically linked busybox will fail to run in a container)
proc = await asyncio.create_subprocess_exec(
"ldd",
busybox_path,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.DEVNULL
"ldd", busybox_path, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL
)
stdout, _ = await proc.communicate()
if proc.returncode == 1 or "static" in busybox_exec:
@@ -111,11 +112,15 @@ class Docker(BaseManager):
shutil.copy2(busybox_path, dst_busybox, follow_symlinks=True)
return
else:
log.warning(f"Busybox '{busybox_path}' is dynamically linked\n"
f"{stdout.decode('utf-8', errors='ignore').strip()}")
log.warning(
f"Busybox '{busybox_path}' is dynamically linked\n"
f"{stdout.decode('utf-8', errors='ignore').strip()}"
)
except OSError as e:
raise DockerError(f"Could not install busybox: {e}")
raise DockerError("No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH")
raise DockerError(
"No busybox executable could be found, please install busybox (apt install busybox-static on Debian/Ubuntu) and make sure it is in your PATH"
)
@staticmethod
def resources_path():
@@ -142,6 +147,7 @@ class Docker(BaseManager):
dst_path = self.resources_path()
log.info(f"Installing Docker resources in '{dst_path}'")
from gns3server.controller import Controller
await Controller.instance().install_resource_files(dst_path, "compute/docker/resources")
await self.install_busybox(dst_path)
except OSError as e:
@@ -157,12 +163,12 @@ class Docker(BaseManager):
self._connected = False
raise DockerError("Can't connect to Docker daemon")
api_version = parse_version(docker_info['ApiVersion'])
api_version = parse_version(docker_info["ApiVersion"])
version = docker_info["Version"]
if api_version < parse_version(DOCKER_MINIMUM_API_VERSION):
raise DockerError(f"Docker version is {version}. "
f"GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
raise DockerError(
f"Docker version is {version}. GNS3 requires a minimum version of {DOCKER_MINIMUM_VERSION}"
)
preferred_api_version = parse_version(DOCKER_PREFERRED_API_VERSION)
@@ -170,7 +176,7 @@ class Docker(BaseManager):
self._api_version = DOCKER_PREFERRED_API_VERSION
else:
# use the Min API version supported by the daemon
self._api_version = docker_info['MinAPIVersion']
self._api_version = docker_info["MinAPIVersion"]
log.warning("Using Docker client with the minimum API version {}".format(self._api_version))
log.info("Connected to Docker daemon version {} using API version {}".format(version, self._api_version))
@@ -264,7 +270,7 @@ class Docker(BaseManager):
response = await self.http_query(method, path, data=data, params=params)
body = await response.read()
response.close()
if response.headers.get('CONTENT-TYPE') == 'application/json':
if response.headers.get("CONTENT-TYPE") == "application/json":
body = json.loads(body.decode("utf-8", errors="ignore"))
else:
body = body.decode("utf-8", errors="ignore")
@@ -292,7 +298,7 @@ class Docker(BaseManager):
if timeout is None:
timeout = 60 * 60 * 24 * 31 # One month timeout
if path == 'version':
if path == "version":
url = "http://docker/" + path
else:
await self._check_connection() # version is use by check connection
@@ -380,9 +386,7 @@ class Docker(BaseManager):
f"Disconnected while pulling Docker image '{image}' from Docker repository"
) from e
except asyncio.TimeoutError as e:
raise DockerError(
f"Timeout while pulling Docker image '{image}' from Docker repository"
) from e
raise DockerError(f"Timeout while pulling Docker image '{image}' from Docker repository") from e
if not chunk:
break
content += chunk.decode("utf-8")
+120 -91
View File
@@ -236,7 +236,9 @@ class DockerVM(BaseNode):
"""
if not is_rfc1123_hostname_valid(new_name):
raise DockerError(f"'{new_name}' is an invalid name to rename Docker container '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen.")
raise DockerError(
f"'{new_name}' is an invalid name to rename Docker container '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen."
)
super(DockerVM, DockerVM).name.__set__(self, new_name)
@property
@@ -275,10 +277,10 @@ class DockerVM(BaseNode):
else:
self._mac_address = mac_address
log.debug('Docker container "{name}" [{id}]: MAC address changed to {mac_addr}'.format(
name=self._name,
id=self._id,
mac_addr=self._mac_address)
log.debug(
'Docker container "{name}" [{id}]: MAC address changed to {mac_addr}'.format(
name=self._name, id=self._id, mac_addr=self._mac_address
)
)
@property
@@ -453,12 +455,7 @@ class DockerVM(BaseNode):
raise DockerError(f"Cannot access resources: {e}")
log.debug(f'Mount resources from "{resources_path}"')
binds = [{
"Type": "bind",
"Source": resources_path,
"Target": "/gns3",
"ReadOnly": True
}]
binds = [{"Type": "bind", "Source": resources_path, "Target": "/gns3", "ReadOnly": True}]
self._image_id = image_info.get("Id") or self._image_id
# We mount our own etc/network
@@ -471,11 +468,7 @@ class DockerVM(BaseNode):
for volume in self._volumes:
source = os.path.join(self.working_dir, os.path.relpath(volume, "/"))
os.makedirs(source, exist_ok=True)
binds.append({
"Type": "bind",
"Source": source,
"Target": "/gns3volumes{}".format(volume)
})
binds.append({"Type": "bind", "Source": source, "Target": "/gns3volumes{}".format(volume)})
# Inject extra config files: write each to the node working directory and
# bind-mount it read-only at its target path. Single-file binds are applied
@@ -485,9 +478,7 @@ class DockerVM(BaseNode):
target = cfg["target"] if isinstance(cfg, dict) else cfg.target
content = cfg["content"] if isinstance(cfg, dict) else cfg.content
if not target.startswith("/") or target.endswith("/") or ".." in target.split("/"):
raise DockerError(
f"Extra config target '{target}' must be an absolute file path and not contain '..'."
)
raise DockerError(f"Extra config target '{target}' must be an absolute file path and not contain '..'.")
for volume in self._volumes:
# A single-file bind gets covered by the volume's bind mount at
# start (init.sh or the vendor volume bridge), so the injected
@@ -497,18 +488,22 @@ class DockerVM(BaseNode):
log.warning(
"Extra config target '%s' on container '%s' is shadowed by persisted volume '%s' "
"and will not take effect; pick a target outside persisted volumes.",
target, self._name, volume,
target,
self._name,
volume,
)
host_path = os.path.join(self.working_dir, "configs", target.lstrip("/"))
os.makedirs(os.path.dirname(host_path), exist_ok=True)
with open(host_path, "w") as f:
f.write(content)
binds.append({
"Type": "bind",
"Source": host_path,
"Target": target,
"ReadOnly": True,
})
binds.append(
{
"Type": "bind",
"Source": host_path,
"Target": target,
"ReadOnly": True,
}
)
return binds
@@ -554,7 +549,8 @@ class DockerVM(BaseNode):
#auto eth{adapter}
#iface eth{adapter} inet dhcp
#\thostname {hostname}
""".format(adapter=adapter, hostname=self._name))
""".format(adapter=adapter, hostname=self._name)
)
return path
def _prepare_init_and_interface_env(self, params):
@@ -573,9 +569,11 @@ class DockerVM(BaseNode):
"""
if ":" in os.path.splitdrive(self.working_dir)[1]:
raise DockerError("Cannot create a Docker container with a project directory containing a colon character (':')")
raise DockerError(
"Cannot create a Docker container with a project directory containing a colon character (':')"
)
#await self.manager.install_resources()
# await self.manager.install_resources()
try:
image_infos = await self._get_image_information()
@@ -601,8 +599,7 @@ class DockerVM(BaseNode):
available_cpus = psutil.cpu_count(logical=True)
if self._cpus > available_cpus:
raise DockerError(
f"You have allocated too many CPUs for the Docker container "
f"(max available is {available_cpus} CPUs)"
f"You have allocated too many CPUs for the Docker container (max available is {available_cpus} CPUs)"
)
# Prepare persistent volume content before the container and its
@@ -622,7 +619,7 @@ class DockerVM(BaseNode):
"Mounts": self._mount_binds(image_infos),
"Memory": self._memory * (1024 * 1024), # convert memory to bytes
"NanoCpus": int(self._cpus * 1e9), # convert cpus to nano cpus
"UsernsMode": "host"
"UsernsMode": "host",
},
"Volumes": {},
"Env": ["container=docker"], # Systemd compliant: https://github.com/GNS3/gns3-server/issues/573
@@ -651,8 +648,11 @@ class DockerVM(BaseNode):
devices = self._format_devices(line.split("=", 1)[1])
if devices:
params["HostConfig"]["Devices"] = devices
elif line.startswith("GNS3_MASK_UDEV=") and \
line.split("=", 1)[1].strip().lower() in ("1", "true", "yes"):
elif line.startswith("GNS3_MASK_UDEV=") and line.split("=", 1)[1].strip().lower() in (
"1",
"true",
"yes",
):
# A privileged systemd-based NOS container (e.g. Cisco XRd)
# runs systemd-udevd, which coldplugs every device it can see
# -- and in privileged mode that includes the HOST's USB/input/
@@ -660,24 +660,28 @@ class DockerVM(BaseNode):
# XRd doesn't need udev (interfaces are pre-created by GNS3), so
# bind /dev/null over the udev units to keep it from running.
for target in [f"/etc/systemd/system/{u}" for u in self._UDEV_UNITS] + list(self._UDEVADM_PATHS):
params["HostConfig"]["Mounts"].append({
"Type": "bind",
"Source": "/dev/null",
"Target": target,
"ReadOnly": True,
})
params["HostConfig"]["Mounts"].append(
{
"Type": "bind",
"Source": "/dev/null",
"Target": target,
"ReadOnly": True,
}
)
elif line.startswith("GNS3_MASK_SYSTEMD="):
# Generic form: comma/semicolon-separated unit names to mask
# the same way (bind /dev/null over /etc/systemd/system/<unit>).
for unit in line.split("=", 1)[1].replace(";", ",").split(","):
unit = unit.strip()
if unit and "/" not in unit and ".." not in unit:
params["HostConfig"]["Mounts"].append({
"Type": "bind",
"Source": "/dev/null",
"Target": f"/etc/systemd/system/{unit}",
"ReadOnly": True,
})
params["HostConfig"]["Mounts"].append(
{
"Type": "bind",
"Source": "/dev/null",
"Target": f"/etc/systemd/system/{unit}",
"ReadOnly": True,
}
)
# Overlapping bind targets (GNS3_MASK_UDEV together with a
# GNS3_MASK_SYSTEMD entry for the same unit, an extra_configs target
@@ -754,12 +758,14 @@ class DockerVM(BaseNode):
"QT_GRAPHICSSYSTEM=native"
) # To fix a Qt issue: https://github.com/GNS3/gns3-server/issues/556
params["Env"].append(f"DISPLAY=:{self._display}")
params["HostConfig"]["Mounts"].append({
"Type": "bind",
"Source": f"/tmp/.X11-unix/X{self._display}",
"Target": f"/tmp/.X11-unix/X{self._display}",
"ReadOnly": True
})
params["HostConfig"]["Mounts"].append(
{
"Type": "bind",
"Source": f"/tmp/.X11-unix/X{self._display}",
"Target": f"/tmp/.X11-unix/X{self._display}",
"ReadOnly": True,
}
)
if self._extra_hosts:
extra_hosts = self._format_extra_hosts(self._extra_hosts)
@@ -772,7 +778,9 @@ class DockerVM(BaseNode):
except DockerHttp409Error:
# Container name already exists. This can happen when the server crashes
# and leaves containers behind. Try to remove the conflicting container.
log.warning(f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container...")
log.warning(
f"Container name '{self.docker_name}' is already in use, attempting to clean up the stale container..."
)
try:
# Try to get and remove the conflicting container
try:
@@ -850,11 +858,13 @@ class DockerVM(BaseNode):
on_host, in_container, permissions = parts
else:
continue
formatted.append({
"PathOnHost": on_host,
"PathInContainer": in_container,
"CgroupPermissions": permissions,
})
formatted.append(
{
"PathOnHost": on_host,
"PathInContainer": in_container,
"CgroupPermissions": permissions,
}
)
return formatted
async def update(self):
@@ -897,7 +907,6 @@ class DockerVM(BaseNode):
await self._start_interface_monitor()
return
else:
if self._console_type == "vnc" and not self._vnc_process:
# restart the vnc process in case it had previously crashed
await self._start_vnc_process(restart=True)
@@ -1042,7 +1051,9 @@ class DockerVM(BaseNode):
stderr = (await process.stderr.read()).decode(errors="replace").strip()
log.error(
"Failed to fix permissions on '%s' for container '%s': %s",
volume, self._name, stderr or f"exit code {process.returncode}"
volume,
self._name,
stderr or f"exit code {process.returncode}",
)
else:
self._permissions_fixed = True
@@ -1102,7 +1113,9 @@ class DockerVM(BaseNode):
log.warning(f"Docker container '{self._name}': cannot access resources to reclaim '{directory}': {e}")
return False
log.info(f"Docker container '{self._name}': reclaiming root-owned files under '{directory}' via a one-shot container")
log.info(
f"Docker container '{self._name}': reclaiming root-owned files under '{directory}' via a one-shot container"
)
# Prefer the image's own chown over the static busybox one: busybox's
# chown dlopens NSS modules from the image, which mismatch the static
# glibc and abort on NOS images whose glibc differs (same reasoning
@@ -1116,12 +1129,24 @@ class DockerVM(BaseNode):
image_ref = self._image_id or self._image
try:
process = await asyncio.subprocess.create_subprocess_exec(
"docker", "run", "--rm", "--network", "none", "--pull", "never", "--user", "0:0",
"--entrypoint", "/gns3/bin/busybox",
"-v", f"{resources_path}:/gns3:ro",
"-v", f"{directory}:/target",
"docker",
"run",
"--rm",
"--network",
"none",
"--pull",
"never",
"--user",
"0:0",
"--entrypoint",
"/gns3/bin/busybox",
"-v",
f"{resources_path}:/gns3:ro",
"-v",
f"{directory}:/target",
image_ref,
"sh", "-c",
"sh",
"-c",
"/gns3/bin/busybox chmod -R u+rwX /target"
f" && ( command -v chown >/dev/null 2>&1 && chown {uid}:{gid} -R /target"
f" || /gns3/bin/busybox chown {uid}:{gid} -R /target )",
@@ -1159,17 +1184,27 @@ class DockerVM(BaseNode):
if tigervnc_path:
with open(os.path.join(self.working_dir, "vnc.log"), "w") as fd:
self._vnc_process = await asyncio.create_subprocess_exec(tigervnc_path,
"-extension", "MIT-SHM",
"-geometry", self._console_resolution,
"-depth", "16",
"-interface", self._manager.port_manager.console_host,
"-rfbport", str(self.console),
"-AlwaysShared",
"-SecurityTypes", "None",
"-desktop", self.name,
":{}".format(self._display),
stdout=fd, stderr=subprocess.STDOUT)
self._vnc_process = await asyncio.create_subprocess_exec(
tigervnc_path,
"-extension",
"MIT-SHM",
"-geometry",
self._console_resolution,
"-depth",
"16",
"-interface",
self._manager.port_manager.console_host,
"-rfbport",
str(self.console),
"-AlwaysShared",
"-SecurityTypes",
"None",
"-desktop",
self.name,
":{}".format(self._display),
stdout=fd,
stderr=subprocess.STDOUT,
)
async def _start_vnc(self):
"""
@@ -1576,8 +1611,8 @@ class DockerVM(BaseNode):
script = (
"while :; do "
"for ifname do "
"flags=$(/gns3/bin/busybox cat \"/sys/class/net/$ifname/flags\" 2>/dev/null) || continue; "
"/gns3/bin/busybox printf '%s=%s\\n' \"$ifname\" \"$flags\"; "
'flags=$(/gns3/bin/busybox cat "/sys/class/net/$ifname/flags" 2>/dev/null) || continue; '
'/gns3/bin/busybox printf \'%s=%s\\n\' "$ifname" "$flags"; '
"done; "
"/gns3/bin/busybox sleep 1; "
"done"
@@ -1621,7 +1656,7 @@ class DockerVM(BaseNode):
asyncio.IncompleteReadError,
asyncio.TimeoutError,
) as e:
if 'writer' in locals():
if "writer" in locals():
await self._close_interface_monitor_writer(writer)
log.warning("Could not monitor interfaces for Docker container '%s': %s", self.name, e)
return
@@ -1658,8 +1693,7 @@ class DockerVM(BaseNode):
"""Read ``ifname=flags`` records and emit changed adapter states."""
interfaces = {
self._get_container_ifname(adapter_number): adapter_number
for adapter_number in range(self.adapters)
self._get_container_ifname(adapter_number): adapter_number for adapter_number in range(self.adapters)
}
try:
while True:
@@ -1747,9 +1781,7 @@ class DockerVM(BaseNode):
await self._ubridge_send(f"bridge create {bridge_name}")
self._bridges.add(bridge_name)
await self._ubridge_send(
"bridge add_nio_tap {bridge_name} {hostif} off".format(
bridge_name=bridge_name, hostif=adapter.host_ifc
)
"bridge add_nio_tap {bridge_name} {hostif} off".format(bridge_name=bridge_name, hostif=adapter.host_ifc)
)
mac_address = int_to_macaddress(macaddress_to_int(self._mac_address) + adapter_number)
@@ -1759,17 +1791,14 @@ class DockerVM(BaseNode):
mac_address = custom_mac_address
try:
await self._ubridge_send('docker set_mac_addr {ifc} {mac}'.format(ifc=adapter.host_ifc, mac=mac_address))
await self._ubridge_send("docker set_mac_addr {ifc} {mac}".format(ifc=adapter.host_ifc, mac=mac_address))
except UbridgeError:
log.warning(f"Could not set MAC address {mac_address} on interface {adapter.host_ifc}")
ifname = self._get_container_ifname(adapter_number)
log.debug(f"Move container {self.name} adapter {adapter.host_ifc} -> {ifname} in ns {self._namespace}")
try:
await self._ubridge_send(
f"docker move_to_ns {adapter.host_ifc} {self._namespace} {ifname}"
)
await self._ubridge_send(f"docker move_to_ns {adapter.host_ifc} {self._namespace} {ifname}")
except UbridgeError as e:
raise UbridgeNamespaceError(e)
else:
@@ -2061,6 +2090,6 @@ class DockerVM(BaseNode):
raise ComputeError(
f"Could not delete the node directory '{self.working_dir}': files left owned by "
f"another user could not be reclaimed ({e}). Reclaim them manually with: "
f"docker run --rm --user 0:0 -v \"{self.working_dir}\":/target --entrypoint /bin/sh "
f'docker run --rm --user 0:0 -v "{self.working_dir}":/target --entrypoint /bin/sh '
f"{self._image} -c 'chown -R {os.getuid()}:{os.getgid()} /target'"
)
+2 -1
View File
@@ -247,7 +247,8 @@ class IOLDockerVM(VendorDockerVM):
log.debug(
"IOL container '%s': number of 4-port Ethernet adapters set to %d",
self._name, adapters,
self._name,
adapters,
)
def _persistent_volume_list(self, image_info, include_network_config=True):
+45 -28
View File
@@ -113,9 +113,7 @@ class VendorDockerVM(DockerVM):
if _line.startswith("GNS3_SKIP_INIT="):
self._gns3_init = _line.split("=", 1)[1].strip().lower() not in ("1", "true", "yes")
elif _line.startswith("GNS3_INTERFACE_NAMES="):
self._interface_names = [
n.strip() for n in _line.split("=", 1)[1].split(",") if n.strip()
]
self._interface_names = [n.strip() for n in _line.split("=", 1)[1].split(",") if n.strip()]
elif _line.startswith("GNS3_CONSOLE_CMD="):
self._console_cmd = _line.split("=", 1)[1].strip()
elif _line.startswith("GNS3_CONSOLE_RESIZE="):
@@ -172,19 +170,20 @@ class VendorDockerVM(DockerVM):
if self._unix_socket_nio:
socket_dir = self._unix_socket_dir.rstrip("/")
if not any(
v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/")
for v in self._volumes
v.rstrip("/") == socket_dir or socket_dir.startswith(v.rstrip("/") + "/") for v in self._volumes
):
# The image's network agent drops privileges before using the
# socket directory, so it must be writable by the server user:
# bind a per-node directory from the runtime directory (see
# _unix_socket_host_dir).
binds.append({
"Type": "bind",
"Source": self._unix_socket_host_dir(),
"Target": socket_dir,
"BindOptions": {"Propagation": "rprivate"},
})
binds.append(
{
"Type": "bind",
"Source": self._unix_socket_host_dir(),
"Target": socket_dir,
"BindOptions": {"Propagation": "rprivate"},
}
)
if self._gns3_init:
return binds
binds = [b for b in binds if b.get("Target") != "/gns3volumes/etc/network"]
@@ -198,7 +197,7 @@ class VendorDockerVM(DockerVM):
for bind in binds:
target = bind.get("Target", "")
if target.startswith("/gns3volumes"):
volume = target[len("/gns3volumes"):]
volume = target[len("/gns3volumes") :]
if volume in self._volumes:
bind = {**bind, "Target": volume}
retargeted.append(bind)
@@ -249,8 +248,11 @@ class VendorDockerVM(DockerVM):
try:
process = await asyncio.subprocess.create_subprocess_exec(
"docker", "create", self._image,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
"docker",
"create",
self._image,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except OSError as e:
raise DockerError(f"Could not seed persistent volumes for '{self._name}': {e}")
@@ -271,8 +273,13 @@ class VendorDockerVM(DockerVM):
try:
process = await asyncio.subprocess.create_subprocess_exec(
"docker", "cp", "-a", f"{seed_cid}:{volume}/.", host_dir + "/",
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
"docker",
"cp",
"-a",
f"{seed_cid}:{volume}/.",
host_dir + "/",
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except OSError as e:
raise DockerError(f"Could not seed persistent volume '{volume}' for '{self._name}': {e}")
@@ -283,7 +290,10 @@ class VendorDockerVM(DockerVM):
# init.sh's first copy (cp -a ... 2>/dev/null).
log.info(
"Persistent volume '%s' on '%s' not seedable from image '%s' (%s); starting empty",
volume, self._name, self._image, stderr.decode(errors="replace").strip(),
volume,
self._name,
self._image,
stderr.decode(errors="replace").strip(),
)
return
log.info("Seeded persistent volume '%s' for '%s' from image '%s'", volume, self._name, self._image)
@@ -295,8 +305,12 @@ class VendorDockerVM(DockerVM):
try:
process = await asyncio.subprocess.create_subprocess_exec(
"docker", "rm", "-f", seed_cid,
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
"docker",
"rm",
"-f",
seed_cid,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except OSError:
return
@@ -348,9 +362,7 @@ class VendorDockerVM(DockerVM):
try:
os.makedirs(host_dir, mode=0o700, exist_ok=True)
except OSError as e:
raise DockerError(
f"Could not create unix-socket directory '{host_dir}' for container '{self._name}': {e}"
)
raise DockerError(f"Could not create unix-socket directory '{host_dir}' for container '{self._name}': {e}")
return host_dir
def _remove_unix_socket_host_dir(self):
@@ -448,7 +460,11 @@ class VendorDockerVM(DockerVM):
adapter.host_ifc = local_sock # bookkeeping / removal logging only
log.debug(
"Adapter %d port %d of container '%s' wired via unix sockets %s <-> %s",
adapter_number, port_number, self._name, local_sock, remote_sock,
adapter_number,
port_number,
self._name,
local_sock,
remote_sock,
)
if nio:
@@ -564,7 +580,8 @@ class VendorDockerVM(DockerVM):
if state == "stopped" or state == "exited":
log.info(
"Container '%s' is %s, skipping permission fix (next start will fix)",
self._name, state,
self._name,
state,
)
return
@@ -601,7 +618,9 @@ class VendorDockerVM(DockerVM):
stderr = (await process.stderr.read()).decode(errors="replace").strip()
log.error(
"Failed to fix permissions on '%s' for container '%s': %s",
volume, self._name, stderr or f"exit code {process.returncode}",
volume,
self._name,
stderr or f"exit code {process.returncode}",
)
else:
self._permissions_fixed = True
@@ -640,9 +659,7 @@ class VendorDockerVM(DockerVM):
allow_resize=self._console_resize,
)
try:
self._telnet_servers.append(
await telnet.start(self._manager.port_manager.console_host, self.console)
)
self._telnet_servers.append(await telnet.start(self._manager.port_manager.console_host, self.console))
except OSError as e:
raise DockerError(
f"Could not start console server on socket {self._manager.port_manager.console_host}:{self.console}: {e}"
+16 -7
View File
@@ -110,7 +110,6 @@ PLATFORMS_DEFAULT_RAM = {
class Dynamips(BaseManager):
_NODE_CLASS = DynamipsFactory
_NODE_TYPE = "dynamips"
_ghost_ios_lock = None
@@ -253,7 +252,9 @@ class Dynamips(BaseManager):
dynamips_path = self.config.settings.Dynamips.dynamips_path
if not os.path.isabs(dynamips_path):
if sys.platform.startswith("win") and hasattr(sys, "frozen"):
dynamips_dir = os.path.normpath(os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "dynamips"))
dynamips_dir = os.path.normpath(
os.path.join(os.path.dirname(os.path.abspath(sys.executable)), "dynamips")
)
os.environ["PATH"] = os.pathsep.join(dynamips_dir) + os.pathsep + os.environ.get("PATH", "")
dynamips_path = shutil.which(dynamips_path)
@@ -305,14 +306,14 @@ class Dynamips(BaseManager):
bind_console_host = False
dynamips_version = await self.dynamips_version(self.dynamips_path)
if parse_version(dynamips_version) < parse_version('0.2.11'):
if parse_version(dynamips_version) < parse_version("0.2.11"):
raise DynamipsError("Dynamips version must be >= 0.2.11, detected version is {}".format(dynamips_version))
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
if parse_version(dynamips_version) >= parse_version('0.2.23'):
if parse_version(dynamips_version) >= parse_version("0.2.23"):
server_host = "127.0.0.1"
bind_console_host = True
@@ -331,7 +332,9 @@ class Dynamips(BaseManager):
raise DynamipsError(f"Could not find free port for the Dynamips hypervisor: {e}")
port_manager = PortManager.instance()
hypervisor = Hypervisor(self._dynamips_path, working_dir, server_host, port, port_manager.console_host, bind_console_host)
hypervisor = Hypervisor(
self._dynamips_path, working_dir, server_host, port, port_manager.console_host, bind_console_host
)
log.debug(f"Creating new hypervisor {hypervisor.host}:{hypervisor.port} with working directory {working_dir}")
await hypervisor.start()
@@ -482,7 +485,9 @@ class Dynamips(BaseManager):
if vm.slots[slot_id].removable():
await vm.slot_remove_binding(slot_id)
else:
log.warning(f"Slot {slot_id} on router '{vm.name}' has a non-removable adapter, skipping replacement")
log.warning(
f"Slot {slot_id} on router '{vm.name}' has a non-removable adapter, skipping replacement"
)
continue
if not isinstance(vm.slots[slot_id], type(adapter)):
await vm.slot_add_binding(slot_id, adapter)
@@ -500,7 +505,11 @@ class Dynamips(BaseManager):
wic_name = value
wic = WIC_MATRIX[wic_name]()
try:
if vm.slots[0] and vm.slots[0].wics[wic_slot_id] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
if (
vm.slots[0]
and vm.slots[0].wics[wic_slot_id]
and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic))
):
await vm.uninstall_wic(wic_slot_id)
if vm.slots[0] and not isinstance(vm.slots[0].wics[wic_slot_id], type(wic)):
await vm.install_wic(wic_slot_id, wic)
@@ -16,7 +16,6 @@
class Adapter:
"""
Base class for adapters.
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C1700_MB_1FE(Adapter):
"""
Integrated 1 port FastEthernet adapter for c1700 platform.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C1700_MB_WIC1(Adapter):
"""
Fake module to provide a placeholder for slot 1 interfaces when WICs
are inserted into WIC slot 1.
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C2600_MB_1E(Adapter):
"""
Integrated 1 port Ethernet adapter for the c2600 platform.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C2600_MB_1FE(Adapter):
"""
Integrated 1 port FastEthernet adapter for the c2600 platform.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C2600_MB_2E(Adapter):
"""
Integrated 2 port Ethernet adapter for the c2600 platform.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C2600_MB_2FE(Adapter):
"""
Integrated 2 port FastEthernet adapter for the c2600 platform.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C7200_IO_2FE(Adapter):
"""
C7200-IO-2FE FastEthernet Input/Ouput controller.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C7200_IO_FE(Adapter):
"""
C7200-IO-FE FastEthernet Input/Ouput controller.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class C7200_IO_GE_E(Adapter):
"""
C7200-IO-GE-E GigabitEthernet Input/Ouput controller.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class Leopard_2FE(Adapter):
"""
Integrated 2 port FastEthernet adapter for c3660 router.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class NM_16ESW(Adapter):
"""
NM-16ESW FastEthernet network module.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class NM_1E(Adapter):
"""
NM-1E Ethernet network module.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class NM_1FE_TX(Adapter):
"""
NM-1FE-TX FastEthernet network module.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class NM_4E(Adapter):
"""
NM-4E Ethernet network module.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class NM_4T(Adapter):
"""
NM-4T Serial network module.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_2FE_TX(Adapter):
"""
PA-2FE-TX FastEthernet port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_4E(Adapter):
"""
PA-4E Ethernet port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_4T(Adapter):
"""
PA-4T+ Serial port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_8E(Adapter):
"""
PA-8E Ethernet port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_8T(Adapter):
"""
PA-8T Serial port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_A1(Adapter):
"""
PA-A1 ATM port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_FE_TX(Adapter):
"""
PA-FE-TX FastEthernet port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_GE(Adapter):
"""
PA-GE GigabitEthernet port adapter.
"""
@@ -18,7 +18,6 @@ from .adapter import Adapter
class PA_POS_OC3(Adapter):
"""
PA-POS-OC3 port adapter.
"""
@@ -16,7 +16,6 @@
class WIC_1ENET:
"""
WIC-1ENET Ethernet
"""
@@ -16,7 +16,6 @@
class WIC_1T:
"""
WIC-1T Serial
"""
@@ -16,7 +16,6 @@
class WIC_2T:
"""
WIC-2T Serial
"""
@@ -22,5 +22,4 @@ from ..error import NodeError
class DynamipsError(NodeError):
pass
@@ -52,7 +52,6 @@ DEVICES = {
class DynamipsFactory:
"""
Factory to create an Router object based on the correct platform.
"""
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class DynamipsHypervisor:
"""
Creates a new connection to a Dynamips server (also called hypervisor)
@@ -90,7 +89,9 @@ class DynamipsHypervisor:
if not connection_success:
raise DynamipsError(f"Couldn't connect to hypervisor on {host}:{self._port} :{last_exception}")
else:
log.debug(f"Connected to Dynamips hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds")
log.debug(
f"Connected to Dynamips hypervisor on {host}:{self._port} after {time.time() - begin:.4f} seconds"
)
try:
version = await self.send("hypervisor version")
@@ -33,7 +33,6 @@ log = logging.getLogger(__name__)
class Hypervisor(DynamipsHypervisor):
"""
Hypervisor.
-1
View File
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIO:
"""
Base NIO class
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIOGenericEthernet(NIO):
"""
Dynamips generic Ethernet NIO.
@@ -67,7 +66,4 @@ class NIOGenericEthernet(NIO):
def asdict(self):
return {
"type": "nio_generic_ethernet",
"ethernet_device": self._ethernet_device
}
return {"type": "nio_generic_ethernet", "ethernet_device": self._ethernet_device}
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIOLinuxEthernet(NIO):
"""
Dynamips Linux Ethernet NIO.
@@ -66,7 +65,4 @@ class NIOLinuxEthernet(NIO):
def asdict(self):
return {
"type": "nio_linux_ethernet",
"ethernet_device": self._ethernet_device
}
return {"type": "nio_linux_ethernet", "ethernet_device": self._ethernet_device}
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIONull(NIO):
"""
Dynamips NULL NIO.
+1 -5
View File
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIOTAP(NIO):
"""
Dynamips TAP NIO.
@@ -60,7 +59,4 @@ class NIOTAP(NIO):
def asdict(self):
return {
"type": "nio_tap",
"tap_device": self._tap_device
}
return {"type": "nio_tap", "tap_device": self._tap_device}
+1 -7
View File
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
class NIOUDP(NIO):
"""
Dynamips UDP NIO.
@@ -130,9 +129,4 @@ class NIOUDP(NIO):
def asdict(self):
return {
"type": "nio_udp",
"lport": self._lport,
"rport": self._rport,
"rhost": self._rhost
}
return {"type": "nio_udp", "lport": self._lport, "rport": self._rport, "rhost": self._rhost}
+1 -6
View File
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIOUNIX(NIO):
"""
Dynamips UNIX NIO.
@@ -81,8 +80,4 @@ class NIOUNIX(NIO):
def asdict(self):
return {
"type": "nio_unix",
"local_file": self._local_file,
"remote_file": self._remote_file
}
return {"type": "nio_unix", "local_file": self._local_file, "remote_file": self._remote_file}
+1 -6
View File
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class NIOVDE(NIO):
"""
Dynamips VDE NIO.
@@ -81,8 +80,4 @@ class NIOVDE(NIO):
def asdict(self):
return {
"type": "nio_vde",
"local_file": self._local_file,
"control_file": self._control_file
}
return {"type": "nio_vde", "local_file": self._local_file, "control_file": self._control_file}
@@ -32,7 +32,6 @@ log = logging.getLogger(__name__)
class ATMSwitch(Device):
"""
Dynamips ATM switch.
@@ -24,7 +24,6 @@ from .device import Device
class Bridge(Device):
"""
Dynamips bridge.
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class C1700(Router):
"""
Dynamips c1700 router.
@@ -32,7 +32,6 @@ log = logging.getLogger(__name__)
class C2600(Router):
"""
Dynamips c2600 router.
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class C2691(Router):
"""
Dynamips c2691 router.
@@ -29,7 +29,6 @@ log = logging.getLogger(__name__)
class C3600(Router):
"""
Dynamips c3600 router.
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class C3725(Router):
"""
Dynamips c3725 router.
@@ -30,7 +30,6 @@ log = logging.getLogger(__name__)
class C3745(Router):
"""
Dynamips c3745 router.
@@ -32,7 +32,6 @@ log = logging.getLogger(__name__)
class C7200(Router):
"""
Dynamips c7200 router (model is 7206).
@@ -16,7 +16,6 @@
class Device:
"""
Base device for switches and hubs
@@ -29,7 +29,6 @@ log = logging.getLogger(__name__)
class EthernetHub(Bridge):
"""
Dynamips Ethernet hub (based on Bridge)
@@ -68,7 +68,6 @@ log = logging.getLogger(__name__)
class EthernetSwitch(Device):
"""
Dynamips Ethernet switch.
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
class FrameRelaySwitch(Device):
"""
Dynamips Frame Relay switch.
+23 -13
View File
@@ -42,7 +42,6 @@ from gns3server.utils.images import md5sum
class Router(BaseNode):
"""
Dynamips router implementation.
@@ -80,7 +79,14 @@ class Router(BaseNode):
raise DynamipsError(f"{name} is an invalid name to create a Dynamips node")
super().__init__(
name, node_id, project, manager, console=console, console_type=console_type, aux=aux, aux_type=aux_type,
name,
node_id,
project,
manager,
console=console,
console_type=console_type,
aux=aux,
aux_type=aux_type,
wrap_console=(console_type == "ssh"),
wrap_aux=(aux_type == "ssh"),
)
@@ -242,7 +248,6 @@ class Router(BaseNode):
)
if not self._ghost_flag:
log.debug(
'Router {platform} "{name}" [{id}] has been created'.format(
name=self._name, platform=self._platform, id=self._id
@@ -252,7 +257,9 @@ class Router(BaseNode):
if self._console is not None:
# For SSH console, tell Dynamips to listen on the internal port so that
# the AsyncioSSHServer proxy can wrap it on the external console port.
con_port = self._internal_console_port if self._wrap_console and self._internal_console_port else self._console
con_port = (
self._internal_console_port if self._wrap_console and self._internal_console_port else self._console
)
await self._hypervisor.send(f'vm set_con_tcp_port "{self._name}" {con_port}')
if self.aux is not None:
@@ -287,7 +294,6 @@ class Router(BaseNode):
if status == "suspended":
await self.resume()
elif status == "inactive":
if not os.path.isfile(self._image) or not os.path.exists(self._image):
if os.path.islink(self._image):
raise DynamipsError(
@@ -1026,8 +1032,11 @@ 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
@@ -1068,9 +1077,10 @@ class Router(BaseNode):
if self.aux_type != aux_type:
status = await self.get_status()
if status == "running":
raise DynamipsError('"{name}" must be stopped to change the auxiliary console type to {aux_type}'.format(
name=self._name,
aux_type=aux_type)
raise DynamipsError(
'"{name}" must be stopped to change the auxiliary console type to {aux_type}'.format(
name=self._name, aux_type=aux_type
)
)
self.aux_type = aux_type
@@ -1228,7 +1238,6 @@ class Router(BaseNode):
# Generate an OIR event if the router is running
if is_running:
await self._hypervisor.send(
'vm slot_oir_start "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number)
)
@@ -1274,7 +1283,6 @@ class Router(BaseNode):
# Generate an OIR event if the router is running
if is_running:
await self._hypervisor.send(
'vm slot_oir_stop "{name}" {slot_number} 0'.format(name=self._name, slot_number=slot_number)
)
@@ -1722,7 +1730,9 @@ class Router(BaseNode):
"""
if not is_ios_hostname_valid(new_name):
raise DynamipsError(f"{new_name} is an invalid name to rename router '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer.")
raise DynamipsError(
f"{new_name} is an invalid name to rename router '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer."
)
await self._hypervisor.send(f'vm rename "{self._name}" "{new_name}"')
-1
View File
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
class IOU(BaseManager):
_NODE_CLASS = IOUVM
_NODE_TYPE = "iou"
+16 -19
View File
@@ -338,13 +338,11 @@ class IOUVM(BaseNode):
if elf_header_start[4] == 1:
# 32-bit loader
loader = os.path.join(self._lib_base, "lib", "ld-linux.so.2")
lib_path = (os.path.join(self._lib_base, "lib"),
os.path.join(self._lib_base, "lib", "i386-linux-gnu"))
lib_path = (os.path.join(self._lib_base, "lib"), os.path.join(self._lib_base, "lib", "i386-linux-gnu"))
else:
# 64-bit loader
loader = os.path.join(self._lib_base, "lib64", "ld-linux-x86-64.so.2")
lib_path = (os.path.join(self._lib_base, "lib64"),
os.path.join(self._lib_base, "lib", "x86_64-linux-gnu"))
lib_path = (os.path.join(self._lib_base, "lib64"), os.path.join(self._lib_base, "lib", "x86_64-linux-gnu"))
self._loader = []
if os.path.isfile(loader):
try:
@@ -475,7 +473,9 @@ class IOUVM(BaseNode):
"""
if not is_ios_hostname_valid(new_name):
raise IOUError(f"'{new_name}' is an invalid name to rename IOU node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer.")
raise IOUError(
f"'{new_name}' is an invalid name to rename IOU node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name must start with a letter, end with a letter or digit, and be 63 characters or fewer."
)
if self.startup_config_file:
content = self.startup_config_content
content = re.sub(r"hostname .+$", "hostname " + new_name, content, flags=re.MULTILINE)
@@ -613,7 +613,7 @@ class IOUVM(BaseNode):
raise IOUError(f"Invalid hostid detected: {hostid}")
for x in hostname:
ioukey += ord(x)
pad1 = b"\x4B\x58\x21\x81\x56\x7B\x0D\xF3\x21\x43\x9B\x7E\xAC\x1D\xE6\x8A"
pad1 = b"\x4b\x58\x21\x81\x56\x7b\x0d\xf3\x21\x43\x9b\x7e\xac\x1d\xe6\x8a"
pad2 = b"\x80" + 39 * b"\0"
ioukey = hashlib.md5(pad1 + pad2 + struct.pack("!I", ioukey) + pad1).hexdigest()[:16]
if ioukey != user_ioukey:
@@ -668,7 +668,6 @@ class IOUVM(BaseNode):
await self._check_requirements()
if not self.is_running():
await self._library_check()
try:
@@ -709,7 +708,7 @@ class IOUVM(BaseNode):
if len(iou_image_path) > 63:
# IOU file basename length must be <= 63 chars
iou_file_name, iou_file_ext = os.path.splitext(iou_image_path)
iou_image_path = iou_file_name[:63 - len(iou_file_ext)] + iou_file_ext
iou_image_path = iou_file_name[: 63 - len(iou_file_ext)] + iou_file_ext
symlink = os.path.join(self.working_dir, iou_image_path)
if os.path.islink(symlink):
os.unlink(symlink)
@@ -729,7 +728,8 @@ class IOUVM(BaseNode):
log.debug(f"Starting IOU: {command}")
self.command_line = " ".join(command)
self._iou_process = await asyncio.create_subprocess_exec(
*self._loader, *command,
*self._loader,
*command,
stdout=asyncio.subprocess.PIPE,
stdin=asyncio.subprocess.PIPE,
stderr=subprocess.STDOUT,
@@ -743,7 +743,9 @@ class IOUVM(BaseNode):
gns3server.utils.asyncio.monitor_process(self._iou_process, callback)
except FileNotFoundError as e:
self._stop_l1_keepalive_responder()
raise IOUError(f"Could not start IOU: {e}: 32-bit binary support is probably not installed, it is recommended to use a 64-bit image instead")
raise IOUError(
f"Could not start IOU: {e}: 32-bit binary support is probably not installed, it is recommended to use a 64-bit image instead"
)
except (OSError, subprocess.SubprocessError) as e:
self._stop_l1_keepalive_responder()
iou_stdout = self.read_iou_stdout()
@@ -775,8 +777,7 @@ class IOUVM(BaseNode):
await self.stop()
raise IOUError(
"Could not start {} server on socket {}:{}: {}".format(
error_prefix,
self._manager.port_manager.console_host, self.console, e
error_prefix, self._manager.port_manager.console_host, self.console, e
)
)
@@ -1281,13 +1282,11 @@ class IOUVM(BaseNode):
"""
from gns3server.compute.marker.marker_manager import MarkerManager
markers = nio.markers if hasattr(nio, 'markers') else {}
markers = nio.markers if hasattr(nio, "markers") else {}
manager = MarkerManager.instance()
markers_dir = self.project.markers_working_directory()
bridge_name = f"IOL-BRIDGE-{self.application_id + 512}"
location = "{bridge_name} {bay} {unit}".format(
bridge_name=bridge_name, bay=adapter_number, unit=port_number
)
location = "{bridge_name} {bay} {unit}".format(bridge_name=bridge_name, bay=adapter_number, unit=port_number)
desired = {(name, spec.get("link_id", "")): spec for name, spec in markers.items()}
# 1. Remove installed markers that are no longer desired.
@@ -1331,9 +1330,7 @@ class IOUVM(BaseNode):
continue
pcap_path = os.path.join(markers_dir, f"{self._id}_{link_id}_{name}.pcap")
# iol_bridge add_packet_filter {br} {bay} {unit} {name} mark "{bpf}" [tag {id}] pcap "{path}"
cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format(
loc=location, name=name, bpf=bpf
)
cmd = 'iol_bridge add_packet_filter {loc} {name} mark "{bpf}"'.format(loc=location, name=name, bpf=bpf)
if tag is not None:
cmd += f" tag {tag}"
if link_id:
+1 -3
View File
@@ -103,9 +103,7 @@ class MarkerListener(asyncio.DatagramProtocol):
project_id, link_id, registered_tag = self._manager.lookup(node_id, filter_name)
if project_id is None:
log.warning(
"MARK signal for unregistered node=%s filter=%s, dropping", node_id, filter_name
)
log.warning("MARK signal for unregistered node=%s filter=%s, dropping", node_id, filter_name)
return
# `link=` is the authoritative per-link id (opaque, set by gns3server at
+6 -10
View File
@@ -86,24 +86,18 @@ class MarkerManager:
sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 8 * 1024 * 1024)
try:
self._transport, _ = await loop.create_datagram_endpoint(
lambda: self._listener, local_addr=(host, port)
)
self._transport, _ = await loop.create_datagram_endpoint(lambda: self._listener, local_addr=(host, port))
_configure_transport(self._transport)
except OSError:
if port != 0:
log.warning(
"Marker listener: port %s unavailable, falling back to OS-assigned port", port
)
log.warning("Marker listener: port %s unavailable, falling back to OS-assigned port", port)
try:
self._transport, _ = await loop.create_datagram_endpoint(
lambda: self._listener, local_addr=(host, 0)
)
_configure_transport(self._transport)
except OSError as e:
log.error(
"Marker listener startup failed: %s. Traffic insight signals are unavailable.", e
)
log.error("Marker listener startup failed: %s. Traffic insight signals are unavailable.", e)
self._listener = None
return
else:
@@ -132,7 +126,9 @@ class MarkerManager:
if received:
log.info(
"marker sink: %d matches (%.0f/s), %d errors in last 10s",
received, received / 10.0, errors,
received,
received / 10.0,
errors,
)
async def stop(self):
-1
View File
@@ -20,7 +20,6 @@ Base interface for NIOs.
class NIO:
"""
IOU NIO.
"""
+1 -5
View File
@@ -22,7 +22,6 @@ from .nio import NIO
class NIOEthernet(NIO):
"""
Generic Ethernet NIO.
@@ -50,7 +49,4 @@ class NIOEthernet(NIO):
def asdict(self):
return {
"type": "nio_ethernet",
"ethernet_device": self._ethernet_device
}
return {"type": "nio_ethernet", "ethernet_device": self._ethernet_device}
+1 -5
View File
@@ -22,7 +22,6 @@ from .nio import NIO
class NIOTAP(NIO):
"""
TAP NIO.
@@ -50,7 +49,4 @@ class NIOTAP(NIO):
def asdict(self):
return {
"type": "nio_tap",
"tap_device": self._tap_device
}
return {"type": "nio_tap", "tap_device": self._tap_device}
+1 -2
View File
@@ -22,7 +22,6 @@ from .nio import NIO
class NIOUDP(NIO):
"""
UDP NIO.
@@ -81,5 +80,5 @@ class NIOUDP(NIO):
"rhost": self._rhost,
"suspend": self._suspended,
"filters": self._filters,
"markers": self._markers
"markers": self._markers,
}
+7 -5
View File
@@ -95,7 +95,6 @@ BANNED_PORTS = {
class PortManager:
"""
:param host: IP address to bind for console connections
"""
@@ -318,7 +317,9 @@ class PortManager:
if port in self._used_tcp_ports:
old_port = port
port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end)
msg = f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}"
msg = (
f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}"
)
log.debug(msg)
return port
if port < port_range_start or port > port_range_end:
@@ -335,7 +336,9 @@ class PortManager:
except OSError:
old_port = port
port = self.get_free_tcp_port(project, port_range_start=port_range_start, port_range_end=port_range_end)
msg = f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}"
msg = (
f"TCP port {old_port} already in use on host {self._console_host}. Port has been replaced by {port}"
)
log.debug(msg)
return port
@@ -395,8 +398,7 @@ class PortManager:
if port < self._udp_port_range[0] or port > self._udp_port_range[1]:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"UDP port {port} is outside the range "
f"{self._udp_port_range[0]}-{self._udp_port_range[1]}",
detail=f"UDP port {port} is outside the range {self._udp_port_range[0]}-{self._udp_port_range[1]}",
)
self._used_udp_ports.add(port)
project.record_udp_port(port)
+44 -46
View File
@@ -37,7 +37,6 @@ log = logging.getLogger(__name__)
class Project:
"""
A project contains a list of nodes.
In theory nodes are isolated project/project.
@@ -82,11 +81,7 @@ class Project:
def asdict(self):
return {
"name": self._name,
"project_id": self._id,
"variables": self._variables
}
return {"name": self._name, "project_id": self._id, "variables": self._variables}
@property
def id(self):
@@ -461,15 +456,14 @@ class Project:
# Security check: ensure the path is within the node directory
if not os.path.commonpath([target_path, node_full_path]) == node_full_path:
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN,
detail="Path is outside the node directory")
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Path is outside the node directory")
if not os.path.exists(target_path):
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND,
detail="Path not found")
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Path not found")
if recursive:
return await self._list_node_files_recursive(node_full_path, target_path,
node_full_path if subpath else None)
return await self._list_node_files_recursive(
node_full_path, target_path, node_full_path if subpath else None
)
# Non-recursive: list only the current directory level
files = []
@@ -492,20 +486,20 @@ class Project:
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
files.append({
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": "directory"
})
files.append(
{
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": "directory",
}
)
else:
if name.endswith(".ghost"):
continue
try:
file_type = await wait_run_in_executor(
lambda e=entry: magic.from_file(e.path, mime=False)
)
file_type = await wait_run_in_executor(lambda e=entry: magic.from_file(e.path, mime=False))
except Exception as e:
log.warning(f"Error getting file type for '{rel_path}': {e}")
file_type = ""
@@ -514,13 +508,15 @@ class Project:
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
files.append({
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": file_type
})
files.append(
{
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": file_type,
}
)
except OSError:
continue
return files
@@ -543,13 +539,15 @@ class Project:
modified_at = datetime.datetime.fromtimestamp(stat_info.st_mtime).isoformat()
except (OSError, OverflowError, ValueError):
created_at = modified_at = ""
files.append({
"path": os.path.relpath(dir_full_path, base_path),
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": "directory"
})
files.append(
{
"path": os.path.relpath(dir_full_path, base_path),
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": "directory",
}
)
except OSError:
continue
@@ -561,9 +559,7 @@ class Project:
try:
stat_info = await wait_run_in_executor(os.stat, file_path)
try:
file_type = await wait_run_in_executor(
lambda fp=file_path: magic.from_file(fp, mime=False)
)
file_type = await wait_run_in_executor(lambda fp=file_path: magic.from_file(fp, mime=False))
except Exception as e:
log.warning(f"Error getting file type for '{rel_path}': {e}")
file_type = ""
@@ -573,13 +569,15 @@ class Project:
except (OSError, OverflowError, ValueError) as e:
log.warning(f"Invalid timestamp for '{rel_path}': {e}")
created_at = modified_at = ""
files.append({
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": file_type
})
files.append(
{
"path": rel_path,
"size": stat_info.st_size,
"created_at": created_at,
"modified_at": modified_at,
"file_type": file_type,
}
)
except OSError as e:
log.warning(f"Error getting metadata for file '{rel_path}': {e}")
continue
+1 -2
View File
@@ -28,7 +28,6 @@ log = logging.getLogger(__name__)
class ProjectManager:
"""
This singleton keeps track of available projects.
"""
@@ -106,7 +105,7 @@ class ProjectManager:
if project_id is not None and project_id in self._projects:
return self._projects[project_id]
project = Project(name=name, project_id=project_id, path=path, variables=variables)
#self._check_available_disk_space(project) # FIXME: disabled for now
# self._check_available_disk_space(project) # FIXME: disabled for now
self._projects[project.id] = project
return project
+7 -4
View File
@@ -42,7 +42,6 @@ log = logging.getLogger(__name__)
class Qemu(BaseManager):
_NODE_CLASS = QemuVM
_NODE_TYPE = "qemu"
@@ -252,17 +251,21 @@ class Qemu(BaseManager):
:returns: HAXM version number. Returns None if HAXM is not installed.
"""
assert(sys.platform.startswith("win"))
assert sys.platform.startswith("win")
import winreg
hkey = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products")
hkey = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, r"SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products"
)
version = None
for index in range(winreg.QueryInfoKey(hkey)[0]):
product_id = winreg.EnumKey(hkey, index)
try:
product_key = winreg.OpenKey(hkey, r"{}\InstallProperties".format(product_id))
try:
if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith("Hardware Accelerated Execution Manager"):
if winreg.QueryValueEx(product_key, "DisplayName")[0].endswith(
"Hardware Accelerated Execution Manager"
):
version = winreg.QueryValueEx(product_key, "DisplayVersion")[0]
break
finally:
-1
View File
@@ -22,5 +22,4 @@ from ..error import NodeError
class QemuError(NodeError):
pass
+81 -45
View File
@@ -57,10 +57,8 @@ import logging
log = logging.getLogger(__name__)
# forbidden additional options
FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd",
"-fsdev", "-virtfs", "-nic", "-netdev"}
FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS
if opt.startswith("-") and not opt.startswith("--")}
FORBIDDEN_OPTIONS = {"-blockdev", "-drive", "-hda", "-hdb", "-hdc", "-hdd", "-fsdev", "-virtfs", "-nic", "-netdev"}
FORBIDDEN_OPTIONS |= {"-" + opt for opt in FORBIDDEN_OPTIONS if opt.startswith("-") and not opt.startswith("--")}
class QemuVM(BaseNode):
@@ -132,8 +130,10 @@ class QemuVM(BaseNode):
except QemuError:
# If the binary is not found for topologies 1.4 and later
# search via the platform otherwise use the binary name
log.warning(f"Could not find the QEMU binary {qemu_path} on this system, "
f"trying to find one using platform {platform}")
log.warning(
f"Could not find the QEMU binary {qemu_path} on this system, "
f"trying to find one using platform {platform}"
)
if platform:
self.platform = platform
else:
@@ -198,7 +198,9 @@ class QemuVM(BaseNode):
"""
if not is_rfc1123_hostname_valid(new_name):
raise QemuError(f"'{new_name}' is an invalid name to rename Qemu node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen.")
raise QemuError(
f"'{new_name}' is an invalid name to rename Qemu node '{self._name}'. Allowed characters: letters (a-z, A-Z), digits (0-9), and hyphens (-). The name cannot start or end with a hyphen."
)
super(QemuVM, QemuVM).name.__set__(self, new_name)
@property
@@ -267,7 +269,7 @@ class QemuVM(BaseNode):
if qemu_bin == "qemu":
self._platform = "i386"
else:
self._platform = re.sub(r'^qemu-system-(\w+).*$', r'\1', qemu_bin, flags=re.IGNORECASE)
self._platform = re.sub(r"^qemu-system-(\w+).*$", r"\1", qemu_bin, flags=re.IGNORECASE)
if self._platform.split(".")[0] not in list(QemuPlatform):
raise QemuError(f"Platform {self._platform} is unknown")
log.debug(f'QEMU VM "{self._name}" [{self._name}] has set the QEMU path to {qemu_path}')
@@ -1082,9 +1084,7 @@ class QemuVM(BaseNode):
else:
priority = 0
try:
process = await asyncio.create_subprocess_exec(
"renice", "-n", str(priority), "-p", str(self._process.pid)
)
process = await asyncio.create_subprocess_exec("renice", "-n", str(priority), "-p", str(self._process.pid))
await process.wait()
except (OSError, subprocess.SubprocessError) as e:
log.error(f'Could not change process priority for QEMU VM "{self._name}": {e}')
@@ -1108,11 +1108,18 @@ class QemuVM(BaseNode):
try:
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")
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)]
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.debug(f"CPU throttled to {self._cpu_throttling}%")
except FileNotFoundError:
@@ -1251,10 +1258,12 @@ class QemuVM(BaseNode):
if "TPM result for CMD_INIT" in qemu_stdout:
partition = self._find_partition_for_path(self.project.path)
if partition and partition.mountpoint != "/":
qemu_stdout += "\nTPM error: the project directory is not on the same partition as the root directory which can be a problem when using AppArmor.\n" \
"Please try to execute the following commands on the server:\n\n" \
"echo 'owner {}/** rwk,' | sudo tee /etc/apparmor.d/local/usr.bin.swtpm > /dev/null\n" \
"sudo service apparmor restart".format(os.path.dirname(self.project.path))
qemu_stdout += (
"\nTPM error: the project directory is not on the same partition as the root directory which can be a problem when using AppArmor.\n"
"Please try to execute the following commands on the server:\n\n"
"echo 'owner {}/** rwk,' | sudo tee /etc/apparmor.d/local/usr.bin.swtpm > /dev/null\n"
"sudo service apparmor restart".format(os.path.dirname(self.project.path))
)
self.project.emit(
"log.error",
{"message": f"QEMU process has stopped, return code: {returncode}\n{qemu_stdout}"},
@@ -1272,7 +1281,6 @@ class QemuVM(BaseNode):
if self.is_running():
log.debug(f'Stopping QEMU VM "{self._name}" PID={self._process.pid}')
try:
if self.on_close == "save_vm_state":
await self._control_vm("stop")
await self._control_vm("savevm GNS3_SAVED_STATE")
@@ -1399,7 +1407,6 @@ class QemuVM(BaseNode):
"""
if self.is_running() and self._monitor:
reader, writer = await self._open_qemu_monitor_connection_vm()
if reader is None and writer is None:
return
@@ -1729,7 +1736,9 @@ class QemuVM(BaseNode):
retcode = await self._qemu_img_exec(command)
if retcode:
stdout = self.read_qemu_img_stdout()
raise QemuError(f"Could not create '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}")
raise QemuError(
f"Could not create '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}"
)
else:
log.debug(f"QEMU VM '{self.name}' [{self.id}]: Qemu disk image'{disk_name}' created")
except (OSError, subprocess.SubprocessError) as e:
@@ -1757,7 +1766,9 @@ class QemuVM(BaseNode):
retcode = await self._qemu_img_exec(command)
if retcode:
stdout = self.read_qemu_img_stdout()
raise QemuError(f"Could not update '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}")
raise QemuError(
f"Could not update '{disk_name}' disk image: qemu-img returned with {retcode}\n{stdout}"
)
else:
log.debug(f"QEMU VM '{self.name}' [{self.id}]: Qemu disk image '{disk_name}' extended by {extend} MB")
except (OSError, subprocess.SubprocessError) as e:
@@ -1894,9 +1905,7 @@ class QemuVM(BaseNode):
console_host = "::"
except OSError as e:
raise QemuError("Could not check if IPv6 is enabled: {}".format(e))
return ["-spice",
f"addr={console_host},port={port},disable-ticketing",
"-vga", "qxl"]
return ["-spice", f"addr={console_host},port={port},disable-ticketing", "-vga", "qxl"]
else:
return []
@@ -2015,11 +2024,31 @@ class QemuVM(BaseNode):
if base_qcow2 and base_qcow2.crypt_method:
# Workaround for https://gitlab.com/qemu-project/qemu/-/issues/441
# (we have to pass -u and the size). Also embed secret name.
command = [qemu_img_path, "create", "-b", backing_options,
"-F", backing_file_format, "-f", "qcow2", "-u", disk, str(base_qcow2.size)]
command = [
qemu_img_path,
"create",
"-b",
backing_options,
"-F",
backing_file_format,
"-f",
"qcow2",
"-u",
disk,
str(base_qcow2.size),
]
else:
command = [qemu_img_path, "create", "-o", "backing_file={}".format(disk_image),
"-F", backing_file_format, "-f", "qcow2", disk]
command = [
qemu_img_path,
"create",
"-o",
"backing_file={}".format(disk_image),
"-F",
backing_file_format,
"-f",
"qcow2",
disk,
]
retcode = await self._qemu_img_exec(command)
if retcode:
@@ -2186,8 +2215,7 @@ class QemuVM(BaseNode):
if not os.path.isfile(disk_image) or not os.path.exists(disk_image):
if os.path.islink(disk_image):
raise QemuError(
f"'{disk_name}' disk image linked to "
f"'{os.path.realpath(disk_image)}' is not accessible"
f"'{disk_name}' disk image linked to '{os.path.realpath(disk_image)}' is not accessible"
)
else:
raise QemuError(f"'{disk_image}' is not accessible")
@@ -2203,7 +2231,7 @@ class QemuVM(BaseNode):
if await self._qemu_img_exec([qemu_img_path, "check", "-r", "leaks", f"{disk_image}"]) == 3:
self.project.emit(
"log.warning",
{"message": f"Disk image '{disk_image}' has leaked clusters and could not be fixed"}
{"message": f"Disk image '{disk_image}' has leaked clusters and could not be fixed"},
)
elif retcode == 2:
# image is corrupted, let's try to fix it
@@ -2211,15 +2239,14 @@ class QemuVM(BaseNode):
if await self._qemu_img_exec([qemu_img_path, "check", "-r", "all", f"{disk_image}"]) == 2:
self.project.emit(
"log.warning",
{"message": f"Disk image '{disk_image}' is corrupted and could not be fixed"}
{"message": f"Disk image '{disk_image}' is corrupted and could not be fixed"},
)
except (OSError, subprocess.SubprocessError) as e:
stdout = self.read_qemu_img_stdout()
raise QemuError(f"Could not check '{disk_name}' disk image: {e}\n{stdout}")
if self.linked_clone and os.path.dirname(disk_image) != self.working_dir:
#cloned_disk_image = os.path.splitext(os.path.basename(disk_image))
# cloned_disk_image = os.path.splitext(os.path.basename(disk_image))
disk = os.path.join(self.working_dir, f"{disk_name}_disk.qcow2")
if not os.path.exists(disk):
# create the disk
@@ -2305,8 +2332,9 @@ class QemuVM(BaseNode):
else:
# Use a manual case-insensitive search instead
try:
system_ovmf_firmware_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd")
if f.name.lower() == "ovmf_code_4m.fd"), None)
system_ovmf_firmware_path = next(
(f for f in system_ovmf_firmware_dir.glob("*.fd") if f.name.lower() == "ovmf_code_4m.fd"), None
)
except (FileNotFoundError, StopIteration):
system_ovmf_firmware_path = None
@@ -2321,8 +2349,9 @@ class QemuVM(BaseNode):
# try to use the UEFI variables store from the system first
try:
system_ovmf_vars_path = next((f for f in system_ovmf_firmware_dir.glob("*.fd")
if f.name.lower() == "ovmf_vars_4m.fd"), None)
system_ovmf_vars_path = next(
(f for f in system_ovmf_firmware_dir.glob("*.fd") if f.name.lower() == "ovmf_vars_4m.fd"), None
)
except (FileNotFoundError, StopIteration):
system_ovmf_vars_path = None
if system_ovmf_vars_path:
@@ -2389,15 +2418,18 @@ class QemuVM(BaseNode):
swtpm_version = await self.manager.get_swtpm_version(swtpm)
if swtpm_version and parse_version(swtpm_version) < parse_version("0.8.0"):
# swtpm >= version 0.8.0 is required
raise QemuError("swtpm version 0.8.0 or above must be installed (detected version is {})".format(swtpm_version))
raise QemuError(
"swtpm version 0.8.0 or above must be installed (detected version is {})".format(swtpm_version)
)
try:
command = [
swtpm,
"socket",
"--tpm2",
'--tpmstate', "dir={}".format(tpm_dir),
"--tpmstate",
"dir={}".format(tpm_dir),
"--ctrl",
"type=unixio,path={},terminate".format(tpm_sock)
"type=unixio,path={},terminate".format(tpm_sock),
]
command_string = " ".join(shlex.quote(s) for s in command)
log.debug("Starting swtpm (TPM emulator) with: {}".format(command_string))
@@ -2429,7 +2461,7 @@ class QemuVM(BaseNode):
"-tpmdev",
"emulator,id=tpm0,chardev=chrtpm",
"-device",
"tpm-tis,tpmdev=tpm0"
"tpm-tis,tpmdev=tpm0",
]
return options
@@ -2506,7 +2538,7 @@ class QemuVM(BaseNode):
Disable graphics depending of the QEMU version
"""
if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl" "-spice", "-vnc"]):
if any(opt in self._options for opt in ["-display", "-nographic", "-curses", "-sdl-spice", "-vnc"]):
return []
if self._qemu_version and parse_version(self._qemu_version) >= parse_version("3.0"):
return ["-display", "none"]
@@ -2663,7 +2695,11 @@ class QemuVM(BaseNode):
command.extend(["-enable-kvm"])
# Issue on some combo Intel CPU + KVM + Qemu 2.4.0
# https://github.com/GNS3/gns3-server/issues/685
if self._qemu_version and parse_version(self._qemu_version) >= parse_version("2.4.0") and self.platform == "x86_64":
if (
self._qemu_version
and parse_version(self._qemu_version) >= parse_version("2.4.0")
and self.platform == "x86_64"
):
command.extend(["-machine", "smm=off"])
elif sys.platform.startswith("darwin"):
command.extend(["-enable-hax"])
@@ -2701,7 +2737,7 @@ class QemuVM(BaseNode):
# avoiding mouse offset (see https://github.com/GNS3/gns3-server/issues/2335)
if self._console_type == "vnc":
command.extend(['-machine', 'usb=on', '-device', 'usb-tablet'])
command.extend(["-machine", "usb=on", "-device", "usb-tablet"])
return command
def asdict(self):
+11 -4
View File
@@ -60,11 +60,18 @@ class Qcow2:
# } QCowHeader;
struct_format = ">IIQiiQi"
with open(self._path, 'rb') as f:
with open(self._path, "rb") as f:
content = f.read(struct.calcsize(struct_format))
try:
(self.magic, self.version, self.backing_file_offset, self.backing_file_size,
self.cluster_bits, self.size, self.crypt_method) = struct.unpack_from(struct_format, content)
(
self.magic,
self.version,
self.backing_file_offset,
self.backing_file_size,
self.cluster_bits,
self.size,
self.crypt_method,
) = struct.unpack_from(struct_format, content)
except struct.error:
raise Qcow2Error(f"Invalid file header for {self._path}")
@@ -112,7 +119,7 @@ class Qcow2:
"filename": base_image,
},
}
return ("json:"+json.dumps(options, separators=(',', ':')), base_qcow2)
return ("json:" + json.dumps(options, separators=(",", ":")), base_qcow2)
else:
return (base_image, base_qcow2)
except Qcow2Error:
-1
View File
@@ -37,7 +37,6 @@ log = logging.getLogger(__name__)
class Hypervisor(UBridgeHypervisor):
"""
Hypervisor.
@@ -26,7 +26,6 @@ log = logging.getLogger(__name__)
class UBridgeHypervisor:
"""
Creates a new connection to a uBridge hypervisor control channel.
+1 -5
View File
@@ -34,7 +34,6 @@ from .virtualbox_error import VirtualBoxError
class VirtualBox(BaseManager):
_NODE_CLASS = VirtualBoxVM
def __init__(self):
@@ -106,10 +105,7 @@ class VirtualBox(BaseManager):
env["LANG"] = "en" # force english output because we rely on it to parse the output
try:
process = await asyncio.create_subprocess_exec(
*command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
env=env
*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, env=env
)
except (OSError, subprocess.SubprocessError) as e:
raise VirtualBoxError(f"Could not execute VBoxManage: {e}")
@@ -22,5 +22,4 @@ from ..error import NodeError
class VirtualBoxError(NodeError):
pass
@@ -45,7 +45,6 @@ log = logging.getLogger(__name__)
class VirtualBoxVM(BaseNode):
"""
VirtualBox VM implementation.
"""
@@ -242,23 +241,23 @@ class VirtualBoxVM(BaseNode):
try:
tree = ET.parse(linked_vbox_file)
except ET.ParseError:
raise VirtualBoxError(f"Cannot modify VirtualBox linked node file. "
"File {linked_vbox_file} is corrupted.")
raise VirtualBoxError(f"Cannot modify VirtualBox linked node file. File {{linked_vbox_file}} is corrupted.")
except OSError as e:
raise VirtualBoxError(f"Cannot modify VirtualBox linked nodes file '{self._linked_vbox_file()}': {e}")
machine = tree.getroot().find("{http://www.virtualbox.org/}Machine")
if machine is not None and machine.get("uuid") != "{" + self.id + "}":
for image in tree.getroot().findall("{http://www.virtualbox.org/}Image"):
currentSnapshot = machine.get("currentSnapshot")
if currentSnapshot:
newSnapshot = re.sub(r"\{.*\}", "{" + str(uuid.uuid4()) + "}", currentSnapshot)
shutil.move(
os.path.join(self.working_dir, self._vmname, "Snapshots", currentSnapshot) + ".vdi",
os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi"
os.path.join(self.working_dir, self._vmname, "Snapshots", newSnapshot) + ".vdi",
)
log.info(
f"VirtualBox VM '{self.name}' [{self.id}] snapshot file moved from '{currentSnapshot}' to '{newSnapshot}'"
)
log.info(f"VirtualBox VM '{self.name}' [{self.id}] snapshot file moved from '{currentSnapshot}' to '{newSnapshot}'")
image.set("uuid", newSnapshot)
log.info(f"VirtualBox VM '{self.name}' [{self.id}] '{linked_vbox_file}' has been patched")
@@ -346,7 +345,6 @@ class VirtualBoxVM(BaseNode):
vm_state = await self._get_vm_state()
log.info(f"Stopping VirtualBox VM '{self.name}' [{self.id}] (current state is {vm_state})")
if vm_state in ("running", "paused"):
if self.on_close == "save_vm_state":
# add a guest property to know the VM has been saved
await self.manager.execute("guestproperty", ["set", self._uuid, "SavedByGNS3", "yes"])
-1
View File
@@ -42,7 +42,6 @@ from gns3server.compute.vmware.vmware_error import VMwareError
class VMware(BaseManager):
_NODE_CLASS = VMwareVM
def __init__(self):
@@ -22,5 +22,4 @@ from ..error import NodeError
class VMwareError(NodeError):
pass
+3 -4
View File
@@ -42,7 +42,6 @@ log = logging.getLogger(__name__)
class VMwareVM(BaseNode):
"""
VMware VM implementation.
"""
@@ -257,7 +256,6 @@ class VMwareVM(BaseNode):
# first some sanity checks
for adapter_number in range(0, self._adapters):
# we want the vmnet interface to be connected when starting the VM
connected = f"ethernet{adapter_number}.startConnected"
if self._get_vmx_setting(connected):
@@ -271,7 +269,6 @@ class VMwareVM(BaseNode):
self.manager.refresh_vmnet_list(ubridge=use_ubridge)
# then configure VMware network adapters
for adapter_number in range(0, self._adapters):
custom_adapter = self._get_custom_adapter_settings(adapter_number)
adapter_type = custom_adapter.get("adapter_type", self._adapter_type)
@@ -365,7 +362,9 @@ class VMwareVM(BaseNode):
await self._add_ubridge_ethernet_connection(vnet, vmnet_interface, block_host_traffic)
else:
# special case on macOS, we cannot bind VMnet interfaces using the libpcap
await self._ubridge_send('bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, interface=vmnet_interface))
await self._ubridge_send(
'bridge add_nio_fusion_vmnet {name} "{interface}"'.format(name=vnet, interface=vmnet_interface)
)
else:
await self._add_ubridge_ethernet_connection(vnet, vmnet_interface, block_host_traffic)
-1
View File
@@ -27,7 +27,6 @@ from .vpcs_vm import VPCSVM
class VPCS(BaseManager):
_NODE_CLASS = VPCSVM
def __init__(self):
-1
View File
@@ -22,5 +22,4 @@ from ..error import NodeError
class VPCSError(NodeError):
pass