mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 05:51:15 +02:00
refactor: ruff autofixes
This commit is contained in:
455 files changed
+12322
-14762
No files matched your search
@@ -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)
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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'"
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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}"')
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class IOU(BaseManager):
|
||||
|
||||
_NODE_CLASS = IOUVM
|
||||
_NODE_TYPE = "iou"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -20,7 +20,6 @@ Base interface for NIOs.
|
||||
|
||||
|
||||
class NIO:
|
||||
|
||||
"""
|
||||
IOU NIO.
|
||||
"""
|
||||
|
||||
@@ -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}
|
||||
@@ -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}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -22,5 +22,4 @@ from ..error import NodeError
|
||||
|
||||
|
||||
class QemuError(NodeError):
|
||||
|
||||
pass
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ from .vpcs_vm import VPCSVM
|
||||
|
||||
|
||||
class VPCS(BaseManager):
|
||||
|
||||
_NODE_CLASS = VPCSVM
|
||||
|
||||
def __init__(self):
|
||||
|
||||
@@ -22,5 +22,4 @@ from ..error import NodeError
|
||||
|
||||
|
||||
class VPCSError(NodeError):
|
||||
|
||||
pass
|
||||
Reference in new issue
Block a user