Use black with -l 120 param.

This commit is contained in:
grossmj committed 2021-04-13 18:46:50 +09:30
1 parent f928738bd5
commit c021e21309
194 files changed
+6034 -4564

No files matched your search

+34 -25
View File
@@ -39,6 +39,7 @@ from .controller_error import ControllerError, ControllerNotFoundError
import logging
log = logging.getLogger(__name__)
@@ -56,8 +57,7 @@ class Controller:
self.gns3vm = GNS3VM(self)
self.symbols = Symbols()
self._appliance_manager = ApplianceManager()
self._iou_license_settings = {"iourc_content": "",
"license_check": True}
self._iou_license_settings = {"iourc_content": "", "license_check": True}
self._config_loaded = False
async def start(self, computes=None):
@@ -92,19 +92,23 @@ class Controller:
log.warning(f"Protocol changed to 'https' for local compute because SSL is enabled")
protocol = "https"
try:
self._local_server = await self.add_compute(compute_id="local",
name=name,
protocol=protocol,
host=host,
console_host=console_host,
port=port,
user=server_config.user,
password=server_config.password,
force=True,
connect=True,
ssl_context=self._ssl_context)
self._local_server = await self.add_compute(
compute_id="local",
name=name,
protocol=protocol,
host=host,
console_host=console_host,
port=port,
user=server_config.user,
password=server_config.password,
force=True,
connect=True,
ssl_context=self._ssl_context,
)
except ControllerError:
log.fatal(f"Cannot access to the local server, make sure something else is not running on the TCP port {port}")
log.fatal(
f"Cannot access to the local server, make sure something else is not running on the TCP port {port}"
)
sys.exit(1)
if computes:
@@ -125,6 +129,7 @@ class Controller:
def _create_ssl_context(self, server_config):
import ssl
ssl_context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
certfile = server_config.certfile
certkey = server_config.certkey
@@ -204,7 +209,7 @@ class Controller:
iourc_path = os.path.join(server_config.secrets_dir, "gns3_iourc_license")
try:
with open(iourc_path, 'w+') as f:
with open(iourc_path, "w+") as f:
f.write(self._iou_license_settings["iourc_content"])
log.info(f"iourc file '{iourc_path}' saved")
except OSError as e:
@@ -258,8 +263,8 @@ class Controller:
log.error(f"Cannot read IOU license file '{iourc_path}': {e}")
self._iou_license_settings["license_check"] = iou_config.license_check
#self._appliance_manager.appliances_etag = controller_config.get("appliances_etag", None)
#self._appliance_manager.load_appliances()
# self._appliance_manager.appliances_etag = controller_config.get("appliances_etag", None)
# self._appliance_manager.load_appliances()
self._config_loaded = True
async def load_projects(self):
@@ -290,7 +295,7 @@ class Controller:
"""
dst_path = self.configs_path()
src_path = get_resource('configs')
src_path = get_resource("configs")
try:
for file in os.listdir(src_path):
if not os.path.exists(os.path.join(dst_path, file)):
@@ -332,12 +337,12 @@ class Controller:
if compute_id not in self._computes:
# We disallow to create from the outside the local and VM server
if (compute_id == 'local' or compute_id == 'vm') and not force:
if (compute_id == "local" or compute_id == "vm") and not force:
return None
# It seem we have error with a gns3vm imported as a remote server and conflict
# with GNS3 VM settings. That's why we ignore server name gns3vm
if name == 'gns3vm':
if name == "gns3vm":
return None
for compute in self._computes.values():
@@ -346,7 +351,7 @@ class Controller:
compute = Compute(compute_id=compute_id, controller=self, name=name, **kwargs)
self._computes[compute.id] = compute
#self.save()
# self.save()
if connect:
asyncio.get_event_loop().call_later(1, lambda: asyncio.ensure_future(compute.connect()))
self.notification.controller_emit("compute.created", compute.__json__())
@@ -392,7 +397,7 @@ class Controller:
await self.close_compute_projects(compute)
await compute.close()
del self._computes[compute_id]
#self.save()
# self.save()
self.notification.controller_emit("compute.deleted", compute.__json__())
@property
@@ -494,7 +499,9 @@ class Controller:
if topo_data["project_id"] in self._projects:
project = self._projects[topo_data["project_id"]]
else:
project = await self.add_project(path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data)
project = await self.add_project(
path=os.path.dirname(path), status="closed", filename=os.path.basename(path), **topo_data
)
if load or project.auto_open:
await project.open()
return project
@@ -566,7 +573,7 @@ class Controller:
:returns: instance of Controller
"""
if not hasattr(Controller, '_instance') or Controller._instance is None:
if not hasattr(Controller, "_instance") or Controller._instance is None:
Controller._instance = Controller()
return Controller._instance
@@ -586,7 +593,9 @@ class Controller:
await project.delete()
self.remove_project(project)
project = await self.add_project(name="AUTOIDLEPC")
node = await project.add_node(compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram)
node = await project.add_node(
compute, "AUTOIDLEPC", str(uuid.uuid4()), node_type="dynamips", platform=platform, image=image, ram=ram
)
res = await node.dynamips_auto_idlepc()
await project.delete()
self.remove_project(project)
+2 -2
View File
@@ -19,11 +19,11 @@ import copy
import uuid
import logging
log = logging.getLogger(__name__)
class Appliance:
def __init__(self, appliance_id, data, builtin=True):
if appliance_id is None:
self._id = str(uuid.uuid4())
@@ -36,7 +36,7 @@ class Appliance:
if "appliance_id" in self._data:
del self._data["appliance_id"]
if self.status != 'broken':
if self.status != "broken":
log.debug(f'Appliance "{self.name}" [{self._id}] loaded')
@property
+37 -12
View File
@@ -29,6 +29,7 @@ from ..utils.http_client import HTTPClient
from .controller_error import ControllerError
import logging
log = logging.getLogger(__name__)
@@ -82,18 +83,29 @@ class ApplianceManager:
"""
self._appliances = {}
for directory, builtin in ((get_resource('appliances'), True,), (self.appliances_path(), False,)):
for directory, builtin in (
(
get_resource("appliances"),
True,
),
(
self.appliances_path(),
False,
),
):
if directory and os.path.isdir(directory):
for file in os.listdir(directory):
if not file.endswith('.gns3a') and not file.endswith('.gns3appliance'):
if not file.endswith(".gns3a") and not file.endswith(".gns3appliance"):
continue
path = os.path.join(directory, file)
appliance_id = uuid.uuid3(uuid.NAMESPACE_URL, path) # Generate UUID from path to avoid change between reboots
appliance_id = uuid.uuid3(
uuid.NAMESPACE_URL, path
) # Generate UUID from path to avoid change between reboots
try:
with open(path, encoding='utf-8') as f:
with open(path, encoding="utf-8") as f:
appliance = Appliance(appliance_id, json.load(f), builtin=builtin)
json_data = appliance.__json__() # Check if loaded without error
if appliance.status != 'broken':
if appliance.status != "broken":
self._appliances[appliance.id] = appliance
if not appliance.symbol or appliance.symbol.startswith(":/symbols/"):
# apply a default symbol if the appliance has none or a default symbol
@@ -110,6 +122,7 @@ class ApplianceManager:
"""
from . import Controller
controller = Controller.instance()
category = appliance["category"]
if category == "guest":
@@ -125,6 +138,7 @@ class ApplianceManager:
"""
from . import Controller
symbol_dir = Controller.instance().symbols.symbols_path()
self.load_appliances()
for appliance in self._appliances.values():
@@ -145,12 +159,14 @@ class ApplianceManager:
symbol_url = f"https://raw.githubusercontent.com/GNS3/gns3-registry/master/symbols/{symbol}"
async with HTTPClient.get(symbol_url) as response:
if response.status != 200:
log.warning(f"Could not retrieve appliance symbol {symbol} from GitHub due to HTTP error code {response.status}")
log.warning(
f"Could not retrieve appliance symbol {symbol} from GitHub due to HTTP error code {response.status}"
)
else:
try:
symbol_data = await response.read()
log.info(f"Saving {symbol} symbol to {destination_path}")
with open(destination_path, 'wb') as f:
with open(destination_path, "wb") as f:
f.write(symbol_data)
except asyncio.TimeoutError:
log.warning(f"Timeout while downloading '{symbol_url}'")
@@ -169,19 +185,24 @@ class ApplianceManager:
log.info(f"Checking if appliances are up-to-date (ETag {self._appliances_etag})")
headers["If-None-Match"] = self._appliances_etag
async with HTTPClient.get('https://api.github.com/repos/GNS3/gns3-registry/contents/appliances', headers=headers) as response:
async with HTTPClient.get(
"https://api.github.com/repos/GNS3/gns3-registry/contents/appliances", headers=headers
) as response:
if response.status == 304:
log.info(f"Appliances are already up-to-date (ETag {self._appliances_etag})")
return
elif response.status != 200:
raise ControllerError(f"Could not retrieve appliances from GitHub due to HTTP error code {response.status}")
raise ControllerError(
f"Could not retrieve appliances from GitHub due to HTTP error code {response.status}"
)
etag = response.headers.get("ETag")
if etag:
self._appliances_etag = etag
from . import Controller
Controller.instance().save()
json_data = await response.json()
appliances_dir = get_resource('appliances')
appliances_dir = get_resource("appliances")
downloaded_appliance_files = []
for appliance in json_data:
if appliance["type"] == "file":
@@ -189,7 +210,11 @@ class ApplianceManager:
log.info("Download appliance file from '{}'".format(appliance["download_url"]))
async with HTTPClient.get(appliance["download_url"]) as response:
if response.status != 200:
log.warning("Could not download '{}' due to HTTP error code {}".format(appliance["download_url"], response.status))
log.warning(
"Could not download '{}' due to HTTP error code {}".format(
appliance["download_url"], response.status
)
)
continue
try:
appliance_data = await response.read()
@@ -199,7 +224,7 @@ class ApplianceManager:
path = os.path.join(appliances_dir, appliance_name)
try:
log.info(f"Saving {appliance_name} file to {path}")
with open(path, 'wb') as f:
with open(path, "wb") as f:
f.write(appliance_data)
except OSError as e:
raise ControllerError(f"Could not write appliance file '{path}': {e}")
+63 -35
View File
@@ -34,11 +34,13 @@ from ..controller.controller_error import (
ControllerNotFoundError,
ControllerForbiddenError,
ControllerTimeoutError,
ControllerUnauthorizedError)
ControllerUnauthorizedError,
)
from ..version import __version__, __version_info__
import logging
log = logging.getLogger(__name__)
@@ -64,8 +66,19 @@ class Compute:
A GNS3 compute.
"""
def __init__(self, compute_id, controller=None, protocol="http", host="localhost",
port=3080, user=None, password=None, name=None, console_host=None, ssl_context=None):
def __init__(
self,
compute_id,
controller=None,
protocol="http",
host="localhost",
port=3080,
user=None,
password=None,
name=None,
console_host=None,
ssl_context=None,
):
self._http_session = None
assert controller is not None
log.info("Create compute %s", compute_id)
@@ -91,14 +104,7 @@ class Compute:
self._disk_usage_percent = 0
self._last_error = None
self._ssl_context = ssl_context
self._capabilities = {
"version": "",
"platform": "",
"cpus": 0,
"memory": 0,
"disk_size": 0,
"node_types": []
}
self._capabilities = {"version": "", "platform": "", "cpus": 0, "memory": 0, "disk_size": 0, "node_types": []}
self.name = name
# Cache of interfaces on remote host
self._interfaces_cache = None
@@ -223,7 +229,7 @@ class Compute:
try:
return socket.gethostbyname(self._host)
except socket.gaierror:
return '0.0.0.0'
return "0.0.0.0"
@host.setter
def host(self, host):
@@ -295,7 +301,7 @@ class Compute:
"name": self._name,
"protocol": self._protocol,
"host": self._host,
"port": self._port
"port": self._port,
}
return {
"compute_id": self._id,
@@ -309,7 +315,7 @@ class Compute:
"memory_usage_percent": self._memory_usage_percent,
"disk_usage_percent": self._disk_usage_percent,
"capabilities": self._capabilities,
"last_error": self._last_error
"last_error": self._last_error,
}
async def download_file(self, project, path):
@@ -380,7 +386,9 @@ class Compute:
# Try to reconnect after 5 seconds if server unavailable only if not during tests (otherwise we create a ressource usage bomb)
if not hasattr(sys, "_called_from_test") or not sys._called_from_test:
if self.id != "local" and self.id != "vm" and not self._controller.compute_has_open_project(self):
log.warning(f"Not reconnecting to compute '{self._id}' because there is no project opened on it")
log.warning(
f"Not reconnecting to compute '{self._id}' because there is no project opened on it"
)
return
self._connection_failure += 1
# After 5 failure we close the project using the compute to avoid sync issues
@@ -407,12 +415,15 @@ class Compute:
if response.json["version"].split("-")[0] != __version__.split("-")[0]:
if self._name.startswith("GNS3 VM"):
msg = "GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format(__version__,
response.json["version"])
msg = (
"GNS3 version {} is not the same as the GNS3 VM version {}. Please upgrade the GNS3 VM.".format(
__version__, response.json["version"]
)
)
else:
msg = "GNS3 controller version {} is not the same as compute {} version {}".format(__version__,
self._name,
response.json["version"])
msg = "GNS3 controller version {} is not the same as compute {} version {}".format(
__version__, self._name, response.json["version"]
)
if __version_info__[3] == 0:
# Stable release
log.error(msg)
@@ -454,10 +465,12 @@ class Compute:
self._cpu_usage_percent = event["cpu_usage_percent"]
self._memory_usage_percent = event["memory_usage_percent"]
self._disk_usage_percent = event["disk_usage_percent"]
#FIXME: slow down number of compute events
# FIXME: slow down number of compute events
self._controller.notification.controller_emit("compute.updated", self.__json__())
else:
await self._controller.notification.dispatch(action, event, project_id=project_id, compute_id=self.id)
await self._controller.notification.dispatch(
action, event, project_id=project_id, compute_id=self.id
)
else:
if response.type == aiohttp.WSMsgType.CLOSE:
await ws.close()
@@ -504,31 +517,40 @@ class Compute:
async def _run_http_query(self, method, path, data=None, timeout=20, raw=False):
with async_timeout.timeout(timeout):
url = self._getUrl(path)
headers = {'content-type': 'application/json'}
headers = {"content-type": "application/json"}
chunked = None
if data == {}:
data = None
elif data is not None:
if hasattr(data, '__json__'):
if hasattr(data, "__json__"):
data = json.dumps(data.__json__())
elif isinstance(data, aiohttp.streams.EmptyStreamReader):
data = None
# Stream the request
elif isinstance(data, aiohttp.streams.StreamReader) or isinstance(data, bytes):
chunked = True
headers['content-type'] = 'application/octet-stream'
headers["content-type"] = "application/octet-stream"
# If the data is an open file we will iterate on it
elif isinstance(data, io.BufferedIOBase):
chunked = True
headers['content-type'] = 'application/octet-stream'
headers["content-type"] = "application/octet-stream"
else:
data = json.dumps(data).encode("utf-8")
try:
log.debug(f"Attempting request to compute: {method} {url} {headers}")
response = await self._session().request(method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout)
response = await self._session().request(
method, url, headers=headers, data=data, auth=self._auth, chunked=chunked, timeout=timeout
)
except asyncio.TimeoutError:
raise ComputeError(f"Timeout error for {method} call to {url} after {timeout}s")
except (aiohttp.ClientError, aiohttp.ServerDisconnectedError, aiohttp.ClientResponseError, ValueError, KeyError, socket.gaierror) as e:
except (
aiohttp.ClientError,
aiohttp.ServerDisconnectedError,
aiohttp.ClientResponseError,
ValueError,
KeyError,
socket.gaierror,
) as e:
# aiohttp 2.3.1 raises socket.gaierror when cannot find host
raise ComputeError(str(e))
body = await response.read()
@@ -614,12 +636,12 @@ class Compute:
try:
if type in ["qemu", "dynamips", "iou"]:
#for local_image in list_images(type):
# for local_image in list_images(type):
# if local_image['filename'] not in [i['filename'] for i in images]:
# images.append(local_image)
images = sorted(images, key=itemgetter('filename'))
images = sorted(images, key=itemgetter("filename"))
else:
images = sorted(images, key=itemgetter('image'))
images = sorted(images, key=itemgetter("image"))
except OSError as e:
raise ComputeError(f"Cannot list images: {str(e)}")
return images
@@ -643,7 +665,7 @@ class Compute:
return self.host_ip, self.host_ip
# Perhaps the user has correct network gateway, we trust him
if self.host_ip not in ('0.0.0.0', '127.0.0.1') and other_compute.host_ip not in ('0.0.0.0', '127.0.0.1'):
if self.host_ip not in ("0.0.0.0", "127.0.0.1") and other_compute.host_ip not in ("0.0.0.0", "127.0.0.1"):
return self.host_ip, other_compute.host_ip
this_compute_interfaces = await self.interfaces()
@@ -652,7 +674,9 @@ class Compute:
# Sort interface to put the compute host in first position
# we guess that if user specified this host it could have a reason (VMware Nat / Host only interface)
this_compute_interfaces = sorted(this_compute_interfaces, key=lambda i: i["ip_address"] != self.host_ip)
other_compute_interfaces = sorted(other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip)
other_compute_interfaces = sorted(
other_compute_interfaces, key=lambda i: i["ip_address"] != other_compute.host_ip
)
for this_interface in this_compute_interfaces:
# Skip if no ip or no netmask (vbox when stopped set a null netmask)
@@ -662,7 +686,9 @@ class Compute:
if this_interface["ip_address"].startswith("169.254."):
continue
this_network = ipaddress.ip_network("{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False)
this_network = ipaddress.ip_network(
"{}/{}".format(this_interface["ip_address"], this_interface["netmask"]), strict=False
)
for other_interface in other_compute_interfaces:
if len(other_interface["ip_address"]) == 0 or other_interface["netmask"] is None:
@@ -672,7 +698,9 @@ class Compute:
if other_interface["ip_address"] == this_interface["ip_address"]:
continue
other_network = ipaddress.ip_network("{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False)
other_network = ipaddress.ip_network(
"{}/{}".format(other_interface["ip_address"], other_interface["netmask"]), strict=False
)
if this_network.overlaps(other_network):
return this_interface["ip_address"], other_interface["ip_address"]
@@ -17,7 +17,6 @@
class ControllerError(Exception):
def __init__(self, message: str):
super().__init__()
self._message = message
@@ -30,30 +29,25 @@ class ControllerError(Exception):
class ControllerNotFoundError(ControllerError):
def __init__(self, message: str):
super().__init__(message)
class ControllerBadRequestError(ControllerError):
def __init__(self, message: str):
super().__init__(message)
class ControllerUnauthorizedError(ControllerError):
def __init__(self, message: str):
super().__init__(message)
class ControllerForbiddenError(ControllerError):
def __init__(self, message: str):
super().__init__(message)
class ControllerTimeoutError(ControllerError):
def __init__(self, message: str):
super().__init__(message)
+8 -5
View File
@@ -27,6 +27,7 @@ from gns3server.utils.picture import get_size
import logging
log = logging.getLogger(__name__)
@@ -74,7 +75,9 @@ class Drawing:
return data.decode()
except UnicodeError:
width, height, filetype = get_size(data)
return "<svg xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" height=\"{height}\" width=\"{width}\">\n<image height=\"{height}\" width=\"{width}\" xlink:href=\"data:image/{filetype};base64,{b64}\" />\n</svg>".format(b64=base64.b64encode(data).decode(), filetype=filetype, width=width, height=height)
return '<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" height="{height}" width="{width}">\n<image height="{height}" width="{width}" xlink:href="data:image/{filetype};base64,{b64}" />\n</svg>'.format(
b64=base64.b64encode(data).decode(), filetype=filetype, width=width, height=height
)
except OSError:
log.warning("Image file %s missing", filename)
return "<svg></svg>"
@@ -99,8 +102,8 @@ class Drawing:
log.error(f"Can't parse SVG: {e}")
return
# SVG is the default namespace no need to prefix it
ET.register_namespace('xmlns', "http://www.w3.org/2000/svg")
ET.register_namespace('xmlns:xlink', "http://www.w3.org/1999/xlink")
ET.register_namespace("xmlns", "http://www.w3.org/2000/svg")
ET.register_namespace("xmlns:xlink", "http://www.w3.org/1999/xlink")
if len(root.findall("{http://www.w3.org/2000/svg}image")) == 1:
href = "{http://www.w3.org/1999/xlink}href"
@@ -208,7 +211,7 @@ class Drawing:
"z": self._z,
"locked": self._locked,
"rotation": self._rotation,
"svg": self._svg
"svg": self._svg,
}
return {
"project_id": self._project.id,
@@ -218,7 +221,7 @@ class Drawing:
"z": self._z,
"locked": self._locked,
"rotation": self._rotation,
"svg": self.svg
"svg": self.svg,
}
def __repr__(self):
+55 -23
View File
@@ -28,12 +28,22 @@ from .controller_error import ControllerError, ControllerNotFoundError, Controll
from datetime import datetime
import logging
log = logging.getLogger(__name__)
CHUNK_SIZE = 1024 * 8 # 8KB
async def export_project(zstream, project, temporary_dir, include_images=False, include_snapshots=False, keep_compute_id=False, allow_all_nodes=False, reset_mac_addresses=False):
async def export_project(
zstream,
project,
temporary_dir,
include_images=False,
include_snapshots=False,
keep_compute_id=False,
allow_all_nodes=False,
reset_mac_addresses=False,
):
"""
Export a project to a zip file.
@@ -63,7 +73,16 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
# First we process the .gns3 in order to be sure we don't have an error
for file in os.listdir(project._path):
if file.endswith(".gns3"):
await _patch_project_file(project, os.path.join(project._path, file), zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses)
await _patch_project_file(
project,
os.path.join(project._path, file),
zstream,
include_images,
keep_compute_id,
allow_all_nodes,
temporary_dir,
reset_mac_addresses,
)
# Export the local files
for root, dirs, files in os.walk(project._path, topdown=True, followlinks=False):
@@ -97,15 +116,21 @@ async def export_project(zstream, project, temporary_dir, include_images=False,
log.debug("Downloading file '{}' from compute '{}'".format(compute_file["path"], compute.id))
response = await compute.download_file(project, compute_file["path"])
if response.status != 200:
log.warning(f"Cannot export file from compute '{compute.id}'. Compute returned status code {response.status}.")
log.warning(
f"Cannot export file from compute '{compute.id}'. Compute returned status code {response.status}."
)
continue
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
async with aiofiles.open(fd, 'wb') as f:
async with aiofiles.open(fd, "wb") as f:
while True:
try:
data = await response.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise ControllerTimeoutError("Timeout when downloading file '{}' from remote compute {}:{}".format(compute_file["path"], compute.host, compute.port))
raise ControllerTimeoutError(
"Timeout when downloading file '{}' from remote compute {}:{}".format(
compute_file["path"], compute.host, compute.port
)
)
if not data:
break
await f.write(data)
@@ -161,12 +186,14 @@ def _is_exportable(path, include_snapshots=False):
# do not export log files and OS noise
filename = os.path.basename(path)
if filename.endswith('_log.txt') or filename.endswith('.log') or filename == '.DS_Store':
if filename.endswith("_log.txt") or filename.endswith(".log") or filename == ".DS_Store":
return False
return True
async def _patch_project_file(project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses):
async def _patch_project_file(
project, path, zstream, include_images, keep_compute_id, allow_all_nodes, temporary_dir, reset_mac_addresses
):
"""
Patch a project file (.gns3) to export a project.
The .gns3 file is renamed to project.gns3
@@ -186,10 +213,14 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
if "topology" in topology:
if "nodes" in topology["topology"]:
for node in topology["topology"]["nodes"]:
compute_id = node.get('compute_id', 'local')
compute_id = node.get("compute_id", "local")
if node["node_type"] == "virtualbox" and node.get("properties", {}).get("linked_clone"):
raise ControllerError("Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format(node["node_type"]))
raise ControllerError(
"Projects with a linked {} clone node cannot not be exported. Please use Qemu instead.".format(
node["node_type"]
)
)
if not allow_all_nodes and node["node_type"] in ["virtualbox", "vmware"]:
raise ControllerError("Projects with a {} node cannot be exported".format(node["node_type"]))
@@ -208,30 +239,26 @@ async def _patch_project_file(project, path, zstream, include_images, keep_compu
continue
elif not prop.endswith("image"):
continue
if value is None or value.strip() == '':
if value is None or value.strip() == "":
continue
if not keep_compute_id: # If we keep the original compute we can keep the image path
node["properties"][prop] = os.path.basename(value)
if include_images is True:
images.append({
'compute_id': compute_id,
'image': value,
'image_type': node['node_type']
})
images.append({"compute_id": compute_id, "image": value, "image_type": node["node_type"]})
if not keep_compute_id:
topology["topology"]["computes"] = [] # Strip compute information because could contain secret info like password
topology["topology"][
"computes"
] = [] # Strip compute information because could contain secret info like password
local_images = {i['image'] for i in images if i['compute_id'] == 'local'}
local_images = {i["image"] for i in images if i["compute_id"] == "local"}
for image in local_images:
_export_local_image(image, zstream)
remote_images = {
(i['compute_id'], i['image_type'], i['image'])
for i in images if i['compute_id'] != 'local'}
remote_images = {(i["compute_id"], i["image_type"], i["image"]) for i in images if i["compute_id"] != "local"}
for compute_id, image_type, image in remote_images:
await _export_remote_images(project, compute_id, image_type, image, zstream, temporary_dir)
@@ -249,6 +276,7 @@ def _export_local_image(image, zstream):
"""
from ..compute import MODULES
for module in MODULES:
try:
images_directory = module.instance().get_images_directory()
@@ -282,15 +310,19 @@ async def _export_remote_images(project, compute_id, image_type, image, project_
response = await compute.download_image(image_type, image)
if response.status != 200:
raise ControllerError(f"Cannot export image from compute '{compute_id}'. Compute returned status code {response.status}.")
raise ControllerError(
f"Cannot export image from compute '{compute_id}'. Compute returned status code {response.status}."
)
(fd, temp_path) = tempfile.mkstemp(dir=temporary_dir)
async with aiofiles.open(fd, 'wb') as f:
async with aiofiles.open(fd, "wb") as f:
while True:
try:
data = await response.content.read(CHUNK_SIZE)
except asyncio.TimeoutError:
raise ControllerTimeoutError(f"Timeout when downloading image '{image}' from remote compute {compute.host}:{compute.port}")
raise ControllerTimeoutError(
f"Timeout when downloading image '{image}' from remote compute {compute.host}:{compute.port}"
)
if not data:
break
await f.write(data)
+37 -31
View File
@@ -31,6 +31,7 @@ from ..compute import ComputeError
from ..controller_error import ControllerError
import logging
log = logging.getLogger(__name__)
@@ -60,37 +61,47 @@ class GNS3VM:
:returns: Return list of engines supported by GNS3 for the GNS3VM
"""
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(version=__version__)
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VMware.Workstation.{version}.zip".format(
version=__version__
)
vmware_info = {
"engine_id": "vmware",
"description": f'VMware is the recommended choice for best performances.<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>.',
"support_when_exit": True,
"support_headless": True,
"support_ram": True
"support_ram": True,
}
if sys.platform.startswith("darwin"):
vmware_info["name"] = "VMware Fusion (recommended)"
else:
vmware_info["name"] = "VMware Workstation / Player (recommended)"
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.Hyper-V.{version}.zip".format(version=__version__)
download_url = (
"https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.Hyper-V.{version}.zip".format(
version=__version__
)
)
hyperv_info = {
"engine_id": "hyper-v",
"name": "Hyper-V",
"description": f'Hyper-V support (Windows 10/Server 2016 and above). Nested virtualization must be supported and enabled (Intel processor only)<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>',
"support_when_exit": True,
"support_headless": False,
"support_ram": True
"support_ram": True,
}
download_url = "https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(version=__version__)
download_url = (
"https://github.com/GNS3/gns3-gui/releases/download/v{version}/GNS3.VM.VirtualBox.{version}.zip".format(
version=__version__
)
)
virtualbox_info = {
"engine_id": "virtualbox",
"name": "VirtualBox",
"description": f'VirtualBox support. Nested virtualization for both Intel and AMD processors is supported since version 6.1<br>The GNS3 VM can be <a href="{download_url}">downloaded here</a>',
"support_when_exit": True,
"support_headless": True,
"support_ram": True
"support_ram": True,
}
remote_info = {
@@ -99,12 +110,10 @@ class GNS3VM:
"description": "Use a remote GNS3 server as the GNS3 VM.",
"support_when_exit": False,
"support_headless": False,
"support_ram": False
"support_ram": False,
}
engines = [vmware_info,
virtualbox_info,
remote_info]
engines = [vmware_info, virtualbox_info, remote_info]
if sys.platform.startswith("win"):
engines.append(hyperv_info)
@@ -260,7 +269,7 @@ class GNS3VM:
engine = self._get_engine(engine)
vms = []
try:
for vm in (await engine.list()):
for vm in await engine.list():
vms.append({"vmname": vm["vmname"]})
except GNS3VMError as e:
# We raise error only if user activated the GNS3 VM
@@ -280,10 +289,9 @@ class GNS3VM:
except GNS3VMError as e:
# User will receive the error later when they will try to use the node
try:
compute = await self._controller.add_compute(compute_id="vm",
name=f"GNS3 VM ({self.current_engine().vmname})",
host=None,
force=True)
compute = await self._controller.add_compute(
compute_id="vm", name=f"GNS3 VM ({self.current_engine().vmname})", host=None, force=True
)
compute.set_last_error(str(e))
except ControllerError:
@@ -318,11 +326,9 @@ class GNS3VM:
engine.vcpus = self._settings["vcpus"]
engine.headless = self._settings["headless"]
engine.port = self._settings["port"]
compute = await self._controller.add_compute(compute_id="vm",
name=f"GNS3 VM is starting ({engine.vmname})",
host=None,
force=True,
connect=False)
compute = await self._controller.add_compute(
compute_id="vm", name=f"GNS3 VM is starting ({engine.vmname})", host=None, force=True, connect=False
)
try:
await engine.start()
@@ -333,12 +339,14 @@ class GNS3VM:
compute.set_last_error(str(e))
raise e
await compute.connect() # we can connect now that the VM has started
await compute.update(name=f"GNS3 VM ({engine.vmname})",
protocol=self.protocol,
host=self.ip_address,
port=self.port,
user=self.user,
password=self.password)
await compute.update(
name=f"GNS3 VM ({engine.vmname})",
protocol=self.protocol,
host=self.ip_address,
port=self.port,
user=self.user,
password=self.password,
)
# check if the VM is in the same subnet as the local server, start 10 seconds later to give
# some time for the compute in the VM to be ready for requests
@@ -370,11 +378,9 @@ class GNS3VM:
if netmask:
compute_network = ipaddress.ip_interface(f"{compute.host_ip}/{netmask}").network
if vm_network.compare_networks(compute_network) != 0:
msg = "The GNS3 VM (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format(self.ip_address,
vm_network,
compute_id,
compute.host_ip,
compute_network)
msg = "The GNS3 VM (IP={}, NETWORK={}) is not on the same network as the {} server (IP={}, NETWORK={}), please make sure the local server binding is in the same network as the GNS3 VM".format(
self.ip_address, vm_network, compute_id, compute.host_ip, compute_network
)
self._controller.notification.controller_emit("log.warning", {"message": msg})
except ComputeError as e:
log.warning(f"Could not check the VM is in the same subnet as the local server: {e}")
+3 -3
View File
@@ -18,11 +18,11 @@
import psutil
import logging
log = logging.getLogger(__name__)
class BaseGNS3VM:
def __init__(self, controller):
self._controller = controller
@@ -42,9 +42,9 @@ class BaseGNS3VM:
# because this is likely to degrade performances.
self._vcpus = psutil.cpu_count(logical=False)
# we want to allocate half of the available physical memory
#ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2)
# ram = int(psutil.virtual_memory().total / (1024 * 1024) / 2)
# value must be a multiple of 4 (VMware requirement)
#ram -= ram % 4
# ram -= ram % 4
ram = 2048
@@ -17,7 +17,6 @@
class GNS3VMError(Exception):
def __init__(self, message):
super().__init__(message)
self._message = message
+36 -17
View File
@@ -23,6 +23,7 @@ import ipaddress
from .base_gns3_vm import BaseGNS3VM
from .gns3_vm_error import GNS3VMError
log = logging.getLogger(__name__)
@@ -55,17 +56,25 @@ class HyperVGNS3VM(BaseGNS3VM):
raise GNS3VMError("Hyper-V is only supported on Windows")
if sys.getwindowsversion().platform_version[0] < 10:
raise GNS3VMError(f"Windows 10/Windows Server 2016 or a later version is required to run Hyper-V with nested virtualization enabled (version {sys.getwindowsversion().platform_version[0]} detected)")
raise GNS3VMError(
f"Windows 10/Windows Server 2016 or a later version is required to run Hyper-V with nested virtualization enabled (version {sys.getwindowsversion().platform_version[0]} detected)"
)
is_windows_10 = sys.getwindowsversion().platform_version[0] == 10 and sys.getwindowsversion().platform_version[1] == 0
is_windows_10 = (
sys.getwindowsversion().platform_version[0] == 10 and sys.getwindowsversion().platform_version[1] == 0
)
if is_windows_10 and sys.getwindowsversion().platform_version[2] < 14393:
raise GNS3VMError("Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update (build 10.0.14393) or later")
raise GNS3VMError(
"Hyper-V with nested virtualization is only supported on Windows 10 Anniversary Update (build 10.0.14393) or later"
)
try:
import pythoncom
pythoncom.CoInitialize()
import wmi
self._wmi = wmi
conn = self._wmi.WMI()
except self._wmi.x_wmi as e:
@@ -77,12 +86,16 @@ class HyperVGNS3VM(BaseGNS3VM):
if conn.Win32_Processor()[0].Manufacturer != "GenuineIntel":
if is_windows_10 and conn.Win32_Processor()[0].Manufacturer == "AuthenticAMD":
if sys.getwindowsversion().platform_version[2] < 19640:
raise GNS3VMError("Windows 10 (build 10.0.19640) or later is required by Hyper-V to support nested virtualization with AMD processors")
raise GNS3VMError(
"Windows 10 (build 10.0.19640) or later is required by Hyper-V to support nested virtualization with AMD processors"
)
else:
raise GNS3VMError("An Intel processor is required by Hyper-V to support nested virtualization on this version of Windows")
raise GNS3VMError(
"An Intel processor is required by Hyper-V to support nested virtualization on this version of Windows"
)
# This is not reliable
#if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled:
# if not conn.Win32_Processor()[0].VirtualizationFirmwareEnabled:
# raise GNS3VMError("Nested Virtualization (VT-x) is not enabled on this system")
def _connect(self):
@@ -134,8 +147,8 @@ class HyperVGNS3VM(BaseGNS3VM):
:param vm: VM instance
"""
vm_settings = vm.associators(wmi_result_class='Msvm_VirtualSystemSettingData')
return [s for s in vm_settings if s.VirtualSystemType == 'Microsoft:Hyper-V:System:Realized'][0]
vm_settings = vm.associators(wmi_result_class="Msvm_VirtualSystemSettingData")
return [s for s in vm_settings if s.VirtualSystemType == "Microsoft:Hyper-V:System:Realized"][0]
def _get_vm_resources(self, vm, resource_class):
"""
@@ -158,11 +171,13 @@ class HyperVGNS3VM(BaseGNS3VM):
available_vcpus = psutil.cpu_count(logical=False)
if vcpus > available_vcpus:
raise GNS3VMError(f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)")
raise GNS3VMError(
f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)"
)
try:
mem_settings = self._get_vm_resources(self._vm, 'Msvm_MemorySettingData')[0]
cpu_settings = self._get_vm_resources(self._vm, 'Msvm_ProcessorSettingData')[0]
mem_settings = self._get_vm_resources(self._vm, "Msvm_MemorySettingData")[0]
cpu_settings = self._get_vm_resources(self._vm, "Msvm_ProcessorSettingData")[0]
mem_settings.VirtualQuantity = ram
mem_settings.Reservation = ram
@@ -201,7 +216,7 @@ class HyperVGNS3VM(BaseGNS3VM):
Gets the WMI object.
"""
return self._wmi.WMI(moniker=path.replace('\\', '/'))
return self._wmi.WMI(moniker=path.replace("\\", "/"))
async def _set_state(self, state):
"""
@@ -234,8 +249,12 @@ class HyperVGNS3VM(BaseGNS3VM):
IPv4/v6 (4098)
"""
wql = "SELECT * FROM Msvm_GuestNetworkAdapterConfiguration WHERE InstanceID like \
'Microsoft:GuestNetwork\\" + self._vm.Name + "%' and ProtocolIFType > 0 "
wql = (
"SELECT * FROM Msvm_GuestNetworkAdapterConfiguration WHERE InstanceID like \
'Microsoft:GuestNetwork\\"
+ self._vm.Name
+ "%' and ProtocolIFType > 0 "
)
nic_count = len(self._conn.query(wql))
while nic_count == 0:
await asyncio.sleep(0.1) # 100ms
@@ -272,15 +291,15 @@ class HyperVGNS3VM(BaseGNS3VM):
trial = 120
guest_ip_address = ""
log.info("Waiting for GNS3 VM IP")
ports = self._get_vm_resources(self._vm, 'Msvm_EthernetPortAllocationSettingData')
vnics = self._get_vm_resources(self._vm, 'Msvm_SyntheticEthernetPortSettingData')
ports = self._get_vm_resources(self._vm, "Msvm_EthernetPortAllocationSettingData")
vnics = self._get_vm_resources(self._vm, "Msvm_SyntheticEthernetPortSettingData")
while True:
for port in ports:
try:
vnic = [v for v in vnics if port.Parent == v.path_()][0]
except IndexError:
continue
config = vnic.associators(wmi_result_class='Msvm_GuestNetworkAdapterConfiguration')
config = vnic.associators(wmi_result_class="Msvm_GuestNetworkAdapterConfiguration")
ip_addresses = config[0].IPAddresses
for ip_address in ip_addresses:
# take the first valid IPv4 address
@@ -20,11 +20,11 @@ from .gns3_vm_error import GNS3VMError
import logging
log = logging.getLogger(__name__)
class RemoteGNS3VM(BaseGNS3VM):
def __init__(self, controller):
self._engine = "remote"
@@ -27,16 +27,12 @@ from gns3server.utils import parse_version
from gns3server.utils.http_client import HTTPClient
from gns3server.utils.asyncio import wait_run_in_executor
from ...compute.virtualbox import (
VirtualBox,
VirtualBoxError
)
from ...compute.virtualbox import VirtualBox, VirtualBoxError
log = logging.getLogger(__name__)
class VirtualBoxGNS3VM(BaseGNS3VM):
def __init__(self, controller):
self._engine = "virtualbox"
@@ -48,7 +44,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
try:
result = await self._virtualbox_manager.execute(subcommand, args, timeout)
return ("\n".join(result))
return "\n".join(result)
except VirtualBoxError as e:
raise GNS3VMError(f"Error while executing VBoxManage command: {e}")
@@ -61,8 +57,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
if "=" in info:
name, value = info.split("=", 1)
if name == "VMState":
return value.strip('"')
return "unknown"
@@ -77,7 +73,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
properties = await self._execute("list", ["systemproperties"])
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
name, value = prop.split(":", 1)
except ValueError:
continue
self._system_properties[name.strip()] = value.strip()
@@ -92,14 +88,19 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
if "API version" not in self._system_properties:
raise VirtualBoxError(f"Can't access to VirtualBox API version:\n{self._system_properties}")
from cpuinfo import get_cpu_info
cpu_info = await wait_run_in_executor(get_cpu_info)
vendor_id = cpu_info.get('vendor_id_raw')
vendor_id = cpu_info.get("vendor_id_raw")
if vendor_id == "GenuineIntel":
if parse_version(self._system_properties["API version"]) < parse_version("6_1"):
raise VirtualBoxError("VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors")
raise VirtualBoxError(
"VirtualBox version 6.1 or above is required to run the GNS3 VM with nested virtualization enabled on Intel processors"
)
elif vendor_id == "AuthenticAMD":
if parse_version(self._system_properties["API version"]) < parse_version("6_0"):
raise VirtualBoxError("VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors")
raise VirtualBoxError(
"VirtualBox version 6.0 or above is required to run the GNS3 VM with nested virtualization enabled on AMD processors"
)
else:
log.warning(f"Could not determine CPU vendor: {vendor_id}")
@@ -113,8 +114,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
interface = -1
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
if "=" in info:
name, value = info.split("=", 1)
if name.startswith("nic") and value.strip('"') == network_backend:
try:
interface = int(name[3:])
@@ -132,8 +133,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
if "=" in info:
name, value = info.split("=", 1)
if name == f"hostonlyadapter{interface_number}":
return value.strip('"')
return None
@@ -150,7 +151,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
flag_dhcp_server_found = False
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
name, value = prop.split(":", 1)
except ValueError:
continue
if name.strip() == "NetworkName" and value.strip().endswith(vboxnet):
@@ -171,7 +172,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
properties = await self._execute("list", ["hostonlyifs"])
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
name, value = prop.split(":", 1)
except ValueError:
continue
if name.strip() == "Name" and value.strip() == vboxnet:
@@ -186,7 +187,7 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
properties = await self._execute("list", ["hostonlyifs"])
for prop in properties.splitlines():
try:
name, value = prop.split(':', 1)
name, value = prop.split(":", 1)
except ValueError:
continue
if name.strip() == "Name":
@@ -202,8 +203,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
result = await self._execute("showvminfo", [self._vmname, "--machinereadable"])
for info in result.splitlines():
if '=' in info:
name, value = info.split('=', 1)
if "=" in info:
name, value = info.split("=", 1)
if name.startswith("Forwarding") and value.strip('"').startswith("GNS3VM"):
return True
return False
@@ -237,7 +238,9 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
vboxnet = await self._look_for_vboxnet(hostonly_interface_number)
if vboxnet is None:
raise GNS3VMError(f'A VirtualBox host-only network could not be found on network adapter {hostonly_interface_number} for "{self._vmname}"')
raise GNS3VMError(
f'A VirtualBox host-only network could not be found on network adapter {hostonly_interface_number} for "{self._vmname}"'
)
if not (await self._check_vboxnet_exists(vboxnet)):
if sys.platform.startswith("win") and vboxnet == "vboxnet0":
@@ -245,13 +248,17 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
# on Windows. Try to patch this with the first available vboxnet we find.
first_available_vboxnet = await self._find_first_available_vboxnet()
if first_available_vboxnet is None:
raise GNS3VMError(f'Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {hostonly_interface_number} for "{self._vmname}"')
raise GNS3VMError(
f'Please add a VirtualBox host-only network with DHCP enabled and attached it to network adapter {hostonly_interface_number} for "{self._vmname}"'
)
await self.set_hostonly_network(hostonly_interface_number, first_available_vboxnet)
vboxnet = first_available_vboxnet
else:
raise GNS3VMError('VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format(vboxnet,
hostonly_interface_number,
self._vmname))
raise GNS3VMError(
'VirtualBox host-only network "{}" does not exist, please make the sure the network adapter {} configuration is valid for "{}"'.format(
vboxnet, hostonly_interface_number, self._vmname
)
)
if not (await self._check_dhcp_server(vboxnet)):
raise GNS3VMError(f'DHCP must be enabled on VirtualBox host-only network "{vboxnet}"')
@@ -287,15 +294,17 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
except OSError as e:
raise GNS3VMError(f"Error while getting random port: {e}")
if (await self._check_vbox_port_forwarding()):
if await self._check_vbox_port_forwarding():
# delete the GNS3VM NAT port forwarding rule if it exists
log.info(f"Removing GNS3VM NAT port forwarding rule from interface {nat_interface_number}")
await self._execute("controlvm", [self._vmname, f"natpf{nat_interface_number}", "delete", "GNS3VM"])
# add a GNS3VM NAT port forwarding rule to redirect 127.0.0.1 with random port to the port in the VM
log.info(f"Adding GNS3VM NAT port forwarding rule with port {api_port} to interface {nat_interface_number}")
await self._execute("controlvm", [self._vmname, f"natpf{nat_interface_number}",
f"GNS3VM,tcp,{ip_address},{api_port},,{self.port}"])
await self._execute(
"controlvm",
[self._vmname, f"natpf{nat_interface_number}", f"GNS3VM,tcp,{ip_address},{api_port},,{self.port}"],
)
self.ip_address = await self._get_ip(hostonly_interface_number, api_port)
log.info(f"GNS3 VM has been started with IP {self.ip_address}")
@@ -320,7 +329,8 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
if json_data:
for interface in json_data:
if "name" in interface and interface["name"] == "eth{}".format(
hostonly_interface_number - 1):
hostonly_interface_number - 1
):
if "ip_address" in interface and len(interface["ip_address"]) > 0:
return interface["ip_address"]
except ValueError:
@@ -405,7 +415,11 @@ class VirtualBoxGNS3VM(BaseGNS3VM):
:param hostonly_network_name: name of the VirtualBox host-only network
"""
await self._execute("modifyvm", [self._vmname, f"--hostonlyadapter{adapter_number}", hostonly_network_name], timeout=3)
log.info('VirtualBox host-only network "{}" set on network adapter {} for "{}"'.format(hostonly_network_name,
adapter_number,
self._vmname))
await self._execute(
"modifyvm", [self._vmname, f"--hostonlyadapter{adapter_number}", hostonly_network_name], timeout=3
)
log.info(
'VirtualBox host-only network "{}" set on network adapter {} for "{}"'.format(
hostonly_network_name, adapter_number, self._vmname
)
)
+11 -12
View File
@@ -20,18 +20,15 @@ import logging
import asyncio
import psutil
from gns3server.compute.vmware import (
VMware,
VMwareError
)
from gns3server.compute.vmware import VMware, VMwareError
from .base_gns3_vm import BaseGNS3VM
from .gns3_vm_error import GNS3VMError
log = logging.getLogger(__name__)
class VMwareGNS3VM(BaseGNS3VM):
def __init__(self, controller):
self._engine = "vmware"
@@ -47,7 +44,7 @@ class VMwareGNS3VM(BaseGNS3VM):
try:
result = await self._vmware_manager.execute(subcommand, args, timeout, log_level=log_level)
return (''.join(result))
return "".join(result)
except VMwareError as e:
raise GNS3VMError(f"Error while executing VMware command: {e}")
@@ -73,7 +70,9 @@ class VMwareGNS3VM(BaseGNS3VM):
if not float(vcpus).is_integer():
raise GNS3VMError(f"The allocated vCPUs value is not an integer: {vcpus}")
if vcpus > available_vcpus:
raise GNS3VMError(f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)")
raise GNS3VMError(
f"You have allocated too many vCPUs for the GNS3 VM! (max available is {available_vcpus} vCPUs)"
)
try:
pairs = VMware.parse_vmware_file(self._vmx_path)
@@ -94,9 +93,7 @@ class VMwareGNS3VM(BaseGNS3VM):
Due to bug/change in VMWare 14 we're not able to pass Hardware Virtualization in GNS3VM.
We only enable this when it's not present in current configuration and user hasn't deactivated that.
"""
extra_config = (
("vhv.enable", "TRUE"),
)
extra_config = (("vhv.enable", "TRUE"),)
pairs = VMware.parse_vmware_file(self._vmx_path)
updated = False
for key, value in extra_config:
@@ -117,7 +114,7 @@ class VMwareGNS3VM(BaseGNS3VM):
"""
try:
return (await self._vmware_manager.list_vms())
return await self._vmware_manager.list_vms()
except VMwareError as e:
raise GNS3VMError(f"Could not list VMware VMs: {str(e)}")
@@ -169,7 +166,9 @@ class VMwareGNS3VM(BaseGNS3VM):
log.info("Waiting for GNS3 VM IP")
while True:
try:
guest_ip_address = await self._execute("readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG)
guest_ip_address = await self._execute(
"readVariable", [self._vmx_path, "guestVar", "gns3.eth0"], timeout=120, log_level=logging.DEBUG
)
guest_ip_address = guest_ip_address.strip()
if len(guest_ip_address) != 0:
break
+24 -7
View File
@@ -31,6 +31,7 @@ from ..utils.asyncio import wait_run_in_executor
from ..utils.asyncio import aiozipstream
import logging
log = logging.getLogger(__name__)
"""
@@ -150,9 +151,17 @@ async def import_project(controller, project_id, stream, location=None, name=Non
# Project created on the remote GNS3 VM?
if node["compute_id"] not in compute_created:
compute = controller.get_compute(node["compute_id"])
await compute.post("/projects", data={"name": project_name, "project_id": project_id,})
await compute.post(
"/projects",
data={
"name": project_name,
"project_id": project_id,
},
)
compute_created.add(node["compute_id"])
await _move_files_to_compute(compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"]))
await _move_files_to_compute(
compute, project_id, path, os.path.join("project-files", node["node_type"], node["node_id"])
)
# And we dump the updated.gns3
dot_gns3_path = os.path.join(path, project_name + ".gns3")
@@ -259,7 +268,9 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
except OSError as e:
raise ControllerError(f"Cannot open snapshot '{os.path.basename(snapshot)}': {e}")
except zipfile.BadZipFile:
raise ControllerError(f"Cannot extract files from snapshot '{os.path.basename(snapshot)}': not a GNS3 project (invalid zip)")
raise ControllerError(
f"Cannot extract files from snapshot '{os.path.basename(snapshot)}': not a GNS3 project (invalid zip)"
)
# patch the topology with the correct project name and ID
try:
@@ -272,9 +283,13 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
with open(topology_file_path, "w+", encoding="utf-8") as f:
json.dump(topology, f, indent=4, sort_keys=True)
except OSError as e:
raise ControllerError(f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file cannot be modified: {e}")
raise ControllerError(
f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file cannot be modified: {e}"
)
except (ValueError, KeyError):
raise ControllerError(f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file is corrupted")
raise ControllerError(
f"Cannot update snapshot '{os.path.basename(snapshot)}': the project.gns3 file is corrupted"
)
# write everything back to the original snapshot file
try:
@@ -283,8 +298,10 @@ async def _import_snapshots(snapshots_path, project_name, project_id):
for file in files:
path = os.path.join(root, file)
zstream.write(path, os.path.relpath(path, tmpdir))
async with aiofiles.open(snapshot_path, 'wb+') as f:
async with aiofiles.open(snapshot_path, "wb+") as f:
async for chunk in zstream:
await f.write(chunk)
except OSError as e:
raise ControllerError(f"Cannot update snapshot '{os.path.basename(snapshot)}': the snapshot cannot be recreated: {e}")
raise ControllerError(
f"Cannot update snapshot '{os.path.basename(snapshot)}': the snapshot cannot be recreated: {e}"
)
+66 -97
View File
@@ -23,6 +23,7 @@ import html
from .controller_error import ControllerError, ControllerNotFoundError
import logging
log = logging.getLogger(__name__)
@@ -31,76 +32,35 @@ FILTERS = [
"type": "frequency_drop",
"name": "Frequency drop",
"description": "It will drop everything with a -1 frequency, drop every Nth packet with a positive frequency, or drop nothing",
"parameters": [
{
"name": "Frequency",
"minimum": -1,
"maximum": 32767,
"type": "int",
"unit": "th packet"
}
]
"parameters": [{"name": "Frequency", "minimum": -1, "maximum": 32767, "type": "int", "unit": "th packet"}],
},
{
"type": "packet_loss",
"name": "Packet loss",
"description": "The percentage represents the chance for a packet to be lost",
"parameters": [
{
"name": "Chance",
"minimum": 0,
"maximum": 100,
"type": "int",
"unit": "%"
}
]
"parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "type": "int", "unit": "%"}],
},
{
"type": "delay",
"name": "Delay",
"description": "Delay packets in milliseconds. You can add jitter in milliseconds (+/-) of the delay",
"parameters": [
{
"name": "Latency",
"minimum": 0,
"maximum": 32767,
"unit": "ms",
"type": "int"
},
{
"name": "Jitter (-/+)",
"minimum": 0,
"maximum": 32767,
"unit": "ms",
"type": "int"
}
]
{"name": "Latency", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
{"name": "Jitter (-/+)", "minimum": 0, "maximum": 32767, "unit": "ms", "type": "int"},
],
},
{
"type": "corrupt",
"name": "Corrupt",
"description": "The percentage represents the chance for a packet to be corrupted",
"parameters": [
{
"name": "Chance",
"minimum": 0,
"maximum": 100,
"unit": "%",
"type": "int"
}
]
"parameters": [{"name": "Chance", "minimum": 0, "maximum": 100, "unit": "%", "type": "int"}],
},
{
"type": "bpf",
"name": "Berkeley Packet Filter (BPF)",
"description": "This filter will drop any packet matching a BPF expression. Put one expression per line",
"parameters": [
{
"name": "Filters",
"type": "text"
}
]
}
"parameters": [{"name": "Filters", "type": "text"}],
},
]
@@ -193,7 +153,7 @@ class Link:
else:
new_values.append(int(value))
values = new_values
if len(values) != 0 and values[0] != 0 and values[0] != '':
if len(values) != 0 and values[0] != 0 and values[0] != "":
new_filters[filter] = values
if new_filters != self.filters:
@@ -238,28 +198,32 @@ class Link:
if node.node_type in ["nat", "cloud"]:
if other_node["node"].node_type in ["nat", "cloud"]:
raise ControllerError("Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type))
raise ControllerError(
"Connecting a {} to a {} is not allowed".format(other_node["node"].node_type, node.node_type)
)
# Check if user is not connecting serial => ethernet
other_port = other_node["node"].get_port(other_node["adapter_number"], other_node["port_number"])
if other_port is None:
raise ControllerNotFoundError("Port {}/{} for {} not found".format(other_node["adapter_number"], other_node["port_number"], other_node["node"].name))
raise ControllerNotFoundError(
"Port {}/{} for {} not found".format(
other_node["adapter_number"], other_node["port_number"], other_node["node"].name
)
)
if port.link_type != other_port.link_type:
raise ControllerError(f"Connecting a {other_port.link_type} interface to a {port.link_type} interface is not allowed")
raise ControllerError(
f"Connecting a {other_port.link_type} interface to a {port.link_type} interface is not allowed"
)
if label is None:
label = {
"text": html.escape(f"{adapter_number}/{port_number}"),
"style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;"
"style": "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;",
}
self._nodes.append({
"node": node,
"adapter_number": adapter_number,
"port_number": port_number,
"port": port,
"label": label
})
self._nodes.append(
{"node": node, "adapter_number": adapter_number, "port_number": port_number, "port": port, "label": label}
)
if len(self._nodes) == 2:
await self.create()
@@ -345,12 +309,16 @@ class Link:
node_id = self.capture_node["node"].id
adapter_number = self.capture_node["adapter_number"]
port_number = self.capture_node["port_number"]
url = "/projects/{project_id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/" \
"ports/{port_number}/capture/stream".format(project_id=self.project.id,
node_type=node_type,
node_id=node_id,
adapter_number=adapter_number,
port_number=port_number)
url = (
"/projects/{project_id}/{node_type}/nodes/{node_id}/adapters/{adapter_number}/"
"ports/{port_number}/capture/stream".format(
project_id=self.project.id,
node_type=node_type,
node_id=node_id,
adapter_number=adapter_number,
port_number=port_number,
)
)
return compute._getUrl(url)
@@ -365,12 +333,14 @@ class Link:
:returns: File name for a capture on this link
"""
capture_file_name = "{}_{}-{}_to_{}_{}-{}".format(self._nodes[0]["node"].name,
self._nodes[0]["adapter_number"],
self._nodes[0]["port_number"],
self._nodes[1]["node"].name,
self._nodes[1]["adapter_number"],
self._nodes[1]["port_number"])
capture_file_name = "{}_{}-{}_to_{}_{}-{}".format(
self._nodes[0]["node"].name,
self._nodes[0]["adapter_number"],
self._nodes[0]["port_number"],
self._nodes[1]["node"].name,
self._nodes[1]["adapter_number"],
self._nodes[1]["port_number"],
)
return re.sub(r"[^0-9A-Za-z_-]", "", capture_file_name) + ".pcap"
@property
@@ -379,7 +349,7 @@ class Link:
@property
def nodes(self):
return [node['node'] for node in self._nodes]
return [node["node"] for node in self._nodes]
@property
def capturing(self):
@@ -425,16 +395,18 @@ class Link:
:returns: None if no node support filtering else the node
"""
for node in self._nodes:
if node["node"].node_type in ('vpcs',
'traceng',
'vmware',
'dynamips',
'qemu',
'iou',
'cloud',
'nat',
'virtualbox',
'docker'):
if node["node"].node_type in (
"vpcs",
"traceng",
"vmware",
"dynamips",
"qemu",
"iou",
"cloud",
"nat",
"virtualbox",
"docker",
):
return node["node"]
return None
@@ -452,19 +424,16 @@ class Link:
"""
res = []
for side in self._nodes:
res.append({
"node_id": side["node"].id,
"adapter_number": side["adapter_number"],
"port_number": side["port_number"],
"label": side["label"]
})
res.append(
{
"node_id": side["node"].id,
"adapter_number": side["adapter_number"],
"port_number": side["port_number"],
"label": side["label"],
}
)
if topology_dump:
return {
"nodes": res,
"link_id": self._id,
"filters": self._filters,
"suspend": self._suspended
}
return {"nodes": res, "link_id": self._id, "filters": self._filters, "suspend": self._suspended}
return {
"nodes": res,
"link_id": self._id,
@@ -475,5 +444,5 @@ class Link:
"capture_compute_id": self.capture_compute_id,
"link_type": self._link_type,
"filters": self._filters,
"suspend": self._suspended
"suspend": self._suspended,
}
+99 -34
View File
@@ -29,14 +29,29 @@ from ..utils.qt import qt_font_to_style
import logging
log = logging.getLogger(__name__)
class Node:
# This properties are used only on controller and are not forwarded to the compute
CONTROLLER_ONLY_PROPERTIES = ["x", "y", "z", "locked", "width", "height", "symbol", "label", "console_host",
"port_name_format", "first_port_name", "port_segment_size", "ports",
"category", "console_auto_start"]
CONTROLLER_ONLY_PROPERTIES = [
"x",
"y",
"z",
"locked",
"width",
"height",
"symbol",
"label",
"console_host",
"port_name_format",
"first_port_name",
"port_segment_size",
"ports",
"category",
"console_auto_start",
]
def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs):
"""
@@ -93,7 +108,7 @@ class Node:
# This properties will be recompute
ignore_properties = ("width", "height", "hover_symbol")
self.properties = kwargs.pop('properties', {})
self.properties = kwargs.pop("properties", {})
# Update node properties with additional elements
for prop in kwargs:
@@ -124,10 +139,7 @@ class Node:
:returns: Boolean True if the node is always running
like ethernet switch
"""
return self.node_type not in (
"qemu", "docker", "dynamips",
"vpcs", "vmware", "virtualbox",
"iou", "traceng")
return self.node_type not in ("qemu", "docker", "dynamips", "vpcs", "vmware", "virtualbox", "iou", "traceng")
@property
def id(self):
@@ -299,7 +311,7 @@ class Node:
# Apply to label user style or default
try:
style = None # FIXME: allow configuration of default label font & color on controller
#style = qt_font_to_style(self._project.controller.settings["GraphicsView"]["default_label_font"],
# style = qt_font_to_style(self._project.controller.settings["GraphicsView"]["default_label_font"],
# self._project.controller.settings["GraphicsView"]["default_label_color"])
except KeyError:
style = "font-family: TypeWriter;font-size: 10.0;font-weight: bold;fill: #000000;fill-opacity: 1.0;"
@@ -309,7 +321,7 @@ class Node:
"text": html.escape(self._name),
"style": style, # None: means the client will apply its default style
"x": None, # None: means the client should center it
"rotation": 0
"rotation": 0,
}
@property
@@ -384,7 +396,9 @@ class Node:
trial = 0
while trial != 6:
try:
response = await self._compute.post(f"/projects/{self._project.id}/{self._node_type}/nodes", data=data, timeout=timeout)
response = await self._compute.post(
f"/projects/{self._project.id}/{self._node_type}/nodes", data=data, timeout=timeout
)
except ComputeConflict as e:
if e.response.get("exception") == "ImageMissingError":
res = await self._upload_missing_image(self._node_type, e.response["image"])
@@ -419,7 +433,12 @@ class Node:
if prop == "properties":
compute_properties = kwargs[prop]
else:
if prop == "name" and self.status == "started" and self._node_type not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch"):
if (
prop == "name"
and self.status == "started"
and self._node_type
not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch")
):
raise ControllerError("Sorry, it is not possible to rename a node that is already powered on")
setattr(self, prop, kwargs[prop])
@@ -458,10 +477,14 @@ class Node:
self._aux_type = value
elif key == "name":
self.name = value
elif key in ["node_id", "project_id", "console_host",
"startup_config_content",
"private_config_content",
"startup_script"]:
elif key in [
"node_id",
"project_id",
"console_host",
"startup_config_content",
"private_config_content",
"startup_script",
]:
if key in self._properties:
del self._properties[key]
else:
@@ -495,13 +518,26 @@ class Node:
if self._console:
# console is optional for builtin nodes
data["console"] = self._console
if self._console_type and self._node_type not in ("cloud", "nat", "ethernet_hub", "frame_relay_switch", "atm_switch"):
if self._console_type and self._node_type not in (
"cloud",
"nat",
"ethernet_hub",
"frame_relay_switch",
"atm_switch",
):
# console_type is not supported by all builtin nodes excepting Ethernet switch
data["console_type"] = self._console_type
if self._aux:
# aux is optional for builtin nodes
data["aux"] = self._aux
if self._aux_type and self._node_type not in ("cloud", "nat", "ethernet_switch", "ethernet_hub", "frame_relay_switch", "atm_switch"):
if self._aux_type and self._node_type not in (
"cloud",
"nat",
"ethernet_switch",
"ethernet_hub",
"frame_relay_switch",
"atm_switch",
):
# aux_type is not supported by all builtin nodes
data["aux_type"] = self._aux_type
if self.custom_adapters:
@@ -526,9 +562,11 @@ class Node:
if self.node_type == "iou":
license_check = self._project.controller.iou_license.get("license_check", True)
iourc_content = self._project.controller.iou_license.get("iourc_content", None)
#if license_check and not iourc_content:
# if license_check and not iourc_content:
# raise aiohttp.web.HTTPConflict(text="IOU licence is not configured")
await self.post("/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content})
await self.post(
"/start", timeout=240, data={"license_check": license_check, "iourc_content": iourc_content}
)
else:
await self.post("/start", data=data, timeout=240)
except asyncio.TimeoutError:
@@ -580,9 +618,13 @@ class Node:
HTTP post on the node
"""
if data:
return (await self._compute.post(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", data=data, **kwargs))
return await self._compute.post(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", data=data, **kwargs
)
else:
return (await self._compute.post(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs))
return await self._compute.post(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs
)
async def put(self, path, data=None, **kwargs):
"""
@@ -593,18 +635,22 @@ class Node:
else:
path = f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}"
if data:
return (await self._compute.put(path, data=data, **kwargs))
return await self._compute.put(path, data=data, **kwargs)
else:
return (await self._compute.put(path, **kwargs))
return await self._compute.put(path, **kwargs)
async def delete(self, path=None, **kwargs):
"""
HTTP post on the node
"""
if path is None:
return await self._compute.delete(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}", **kwargs)
return await self._compute.delete(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}", **kwargs
)
else:
return await self._compute.delete(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs)
return await self._compute.delete(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs
)
async def _upload_missing_image(self, type, img):
"""
@@ -617,8 +663,10 @@ class Node:
if os.path.exists(image):
self.project.emit_notification("log.info", {"message": f"Uploading missing image {img}"})
try:
with open(image, 'rb') as f:
await self._compute.post(f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None)
with open(image, "rb") as f:
await self._compute.post(
f"/{self._node_type}/images/{os.path.basename(img)}", data=f, timeout=None
)
except OSError as e:
raise ControllerError(f"Can't upload {image}: {str(e)}")
self.project.emit_notification("log.info", {"message": f"Upload finished for {img}"})
@@ -629,13 +677,21 @@ class Node:
"""
Compute the idle PC for a dynamips node
"""
return (await self._compute.get(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/auto_idlepc", timeout=240)).json
return (
await self._compute.get(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/auto_idlepc", timeout=240
)
).json
async def dynamips_idlepc_proposals(self):
"""
Compute a list of potential idle PC
"""
return (await self._compute.get(f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/idlepc_proposals", timeout=240)).json
return (
await self._compute.get(
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}/idlepc_proposals", timeout=240
)
).json
def get_port(self, adapter_number, port_number):
"""
@@ -693,7 +749,9 @@ class Node:
# Basic node we don't want to have adapter number
port_number = 0
for port in self._properties.get("ports_mapping", []):
self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name=f"e{port_number}"))
self._ports.append(
PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name=f"e{port_number}")
)
port_number += 1
elif self._node_type in ("vpcs", "traceng"):
self._ports.append(PortFactory("Ethernet0", 0, 0, 0, "ethernet", short_name="e0"))
@@ -703,7 +761,14 @@ class Node:
self._ports.append(PortFactory(port["name"], 0, 0, port_number, "ethernet", short_name=port["name"]))
port_number += 1
else:
self._ports = StandardPortFactory(self._properties, self._port_by_adapter, self._first_port_name, self._port_name_format, self._port_segment_size, self._custom_adapters)
self._ports = StandardPortFactory(
self._properties,
self._port_by_adapter,
self._first_port_name,
self._port_name_format,
self._port_segment_size,
self._custom_adapters,
)
def __repr__(self):
return f"<gns3server.controller.Node {self._node_type} {self._name}>"
@@ -742,7 +807,7 @@ class Node:
"port_name_format": self._port_name_format,
"port_segment_size": self._port_segment_size,
"first_port_name": self._first_port_name,
"custom_adapters": self._custom_adapters
"custom_adapters": self._custom_adapters,
}
return {
"compute_id": str(self._compute.id),
@@ -773,5 +838,5 @@ class Node:
"port_segment_size": self._port_segment_size,
"first_port_name": self._first_port_name,
"custom_adapters": self._custom_adapters,
"ports": [port.__json__() for port in self.ports]
"ports": [port.__json__() for port in self.ports],
}
+6 -4
View File
@@ -76,9 +76,10 @@ class Notification:
os.makedirs("docs/api/notifications", exist_ok=True)
try:
import json
data = json.dumps(event, indent=4, sort_keys=True)
if "MagicMock" not in data:
with open(os.path.join("docs/api/notifications", action + ".json"), 'w+') as f:
with open(os.path.join("docs/api/notifications", action + ".json"), "w+") as f:
f.write(data)
except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump
pass
@@ -113,8 +114,8 @@ class Notification:
except ControllerError: # Project closing
return
elif action == "ping":
event["compute_id"] = compute_id
self.project_emit(action, event)
event["compute_id"] = compute_id
self.project_emit(action, event)
else:
self.project_emit(action, event, project_id)
@@ -131,9 +132,10 @@ class Notification:
os.makedirs("docs/api/notifications", exist_ok=True)
try:
import json
data = json.dumps(event, indent=4, sort_keys=True)
if "MagicMock" not in data:
with open(os.path.join("docs/api/notifications", action + ".json"), 'w+') as f:
with open(os.path.join("docs/api/notifications", action + ".json"), "w+") as f:
f.write(data)
except TypeError: # If we receive a mock as an event it will raise TypeError when using json dump
pass
-1
View File
@@ -22,7 +22,6 @@ from .serial_port import SerialPort
class ATMPort(SerialPort):
@staticmethod
def long_name_type():
"""
@@ -22,7 +22,6 @@ from .port import Port
class FastEthernetPort(Port):
@staticmethod
def long_name_type():
"""
@@ -22,7 +22,6 @@ from .serial_port import SerialPort
class FrameRelayPort(SerialPort):
@staticmethod
def long_name_type():
"""
@@ -22,7 +22,6 @@ from .port import Port
class GigabitEthernetPort(Port):
@staticmethod
def long_name_type():
"""
+2 -2
View File
@@ -83,7 +83,7 @@ class Port:
# If port name format has changed we use the port name as the short name (1.X behavior)
if self._short_name:
return self._short_name
elif '/' in self._name:
elif "/" in self._name:
return self._name.replace(self.long_name_type(), self.short_name_type())
elif self._name.startswith(f"{self.long_name_type()}{self._interface_number}"):
return self.short_name_type() + f"{self._interface_number}"
@@ -100,7 +100,7 @@ class Port:
"data_link_types": self.data_link_types,
"port_number": self._port_number,
"adapter_number": self._adapter_number,
"link_type": self.link_type
"link_type": self.link_type,
}
if self._adapter_type:
info["adapter_type"] = self._adapter_type
+55 -69
View File
@@ -27,15 +27,16 @@ from .pos_port import POSPort
import logging
log = logging.getLogger(__name__)
PORTS = {
'atm': ATMPort,
'frame_relay': FrameRelayPort,
'fastethernet': FastEthernetPort,
'gigabitethernet': GigabitEthernetPort,
'ethernet': EthernetPort,
'serial': SerialPort
"atm": ATMPort,
"frame_relay": FrameRelayPort,
"fastethernet": FastEthernetPort,
"gigabitethernet": GigabitEthernetPort,
"ethernet": EthernetPort,
"serial": SerialPort,
}
@@ -52,7 +53,10 @@ class StandardPortFactory:
"""
Create ports for standard device
"""
def __new__(cls, properties, port_by_adapter, first_port_name, port_name_format, port_segment_size, custom_adapters):
def __new__(
cls, properties, port_by_adapter, first_port_name, port_name_format, port_segment_size, custom_adapters
):
ports = []
adapter_number = interface_number = segment_number = 0
@@ -72,14 +76,17 @@ class StandardPortFactory:
for port_number in range(0, port_by_adapter):
if first_port_name and adapter_number == 0:
port_name = custom_adapter_settings.get("port_name", first_port_name)
port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet", short_name=port_name)
port = PortFactory(
port_name, segment_number, adapter_number, port_number, "ethernet", short_name=port_name
)
else:
try:
port_name = port_name_format.format(
interface_number,
segment_number,
adapter=adapter_number,
**cls._generate_replacement(interface_number, segment_number))
**cls._generate_replacement(interface_number, segment_number),
)
except (IndexError, ValueError, KeyError) as e:
raise ControllerError(f"Invalid port name format {port_name_format}: {str(e)}")
@@ -106,7 +113,15 @@ class StandardPortFactory:
if "serial_adapters" in properties:
for adapter_number in range(adapter_number, properties["serial_adapters"] + adapter_number):
for port_number in range(0, port_by_adapter):
ports.append(PortFactory(f"Serial{segment_number}/{port_number}", segment_number, adapter_number, port_number, "serial"))
ports.append(
PortFactory(
f"Serial{segment_number}/{port_number}",
segment_number,
adapter_number,
port_number,
"serial",
)
)
segment_number += 1
return ports
@@ -132,67 +147,38 @@ class DynamipsPortFactory:
"""
ADAPTER_MATRIX = {
"C1700-MB-1FE": {"nb_ports": 1,
"port": FastEthernetPort},
"C1700-MB-WIC1": {"nb_ports": 0,
"port": None},
"C2600-MB-1E": {"nb_ports": 1,
"port": EthernetPort},
"C2600-MB-1FE": {"nb_ports": 1,
"port": FastEthernetPort},
"C2600-MB-2E": {"nb_ports": 2,
"port": EthernetPort},
"C2600-MB-2FE": {"nb_ports": 2,
"port": FastEthernetPort},
"C7200-IO-2FE": {"nb_ports": 2,
"port": FastEthernetPort},
"C7200-IO-FE": {"nb_ports": 1,
"port": FastEthernetPort},
"C7200-IO-GE-E": {"nb_ports": 1,
"port": GigabitEthernetPort},
"GT96100-FE": {"nb_ports": 2,
"port": FastEthernetPort},
"Leopard-2FE": {"nb_ports": 2,
"port": FastEthernetPort},
"NM-16ESW": {"nb_ports": 16,
"port": FastEthernetPort},
"NM-1E": {"nb_ports": 1,
"port": EthernetPort},
"NM-1FE-TX": {"nb_ports": 1,
"port": FastEthernetPort},
"NM-4E": {"nb_ports": 4,
"port": EthernetPort},
"NM-4T": {"nb_ports": 4,
"port": SerialPort},
"PA-2FE-TX": {"nb_ports": 2,
"port": FastEthernetPort},
"PA-4E": {"nb_ports": 4,
"port": EthernetPort},
"PA-4T+": {"nb_ports": 4,
"port": SerialPort},
"PA-8E": {"nb_ports": 8,
"port": EthernetPort},
"PA-8T": {"nb_ports": 8,
"port": SerialPort},
"PA-A1": {"nb_ports": 1,
"port": ATMPort},
"PA-FE-TX": {"nb_ports": 1,
"port": FastEthernetPort},
"PA-GE": {"nb_ports": 1,
"port": GigabitEthernetPort},
"PA-POS-OC3": {"nb_ports": 1,
"port": POSPort},
"C1700-MB-1FE": {"nb_ports": 1, "port": FastEthernetPort},
"C1700-MB-WIC1": {"nb_ports": 0, "port": None},
"C2600-MB-1E": {"nb_ports": 1, "port": EthernetPort},
"C2600-MB-1FE": {"nb_ports": 1, "port": FastEthernetPort},
"C2600-MB-2E": {"nb_ports": 2, "port": EthernetPort},
"C2600-MB-2FE": {"nb_ports": 2, "port": FastEthernetPort},
"C7200-IO-2FE": {"nb_ports": 2, "port": FastEthernetPort},
"C7200-IO-FE": {"nb_ports": 1, "port": FastEthernetPort},
"C7200-IO-GE-E": {"nb_ports": 1, "port": GigabitEthernetPort},
"GT96100-FE": {"nb_ports": 2, "port": FastEthernetPort},
"Leopard-2FE": {"nb_ports": 2, "port": FastEthernetPort},
"NM-16ESW": {"nb_ports": 16, "port": FastEthernetPort},
"NM-1E": {"nb_ports": 1, "port": EthernetPort},
"NM-1FE-TX": {"nb_ports": 1, "port": FastEthernetPort},
"NM-4E": {"nb_ports": 4, "port": EthernetPort},
"NM-4T": {"nb_ports": 4, "port": SerialPort},
"PA-2FE-TX": {"nb_ports": 2, "port": FastEthernetPort},
"PA-4E": {"nb_ports": 4, "port": EthernetPort},
"PA-4T+": {"nb_ports": 4, "port": SerialPort},
"PA-8E": {"nb_ports": 8, "port": EthernetPort},
"PA-8T": {"nb_ports": 8, "port": SerialPort},
"PA-A1": {"nb_ports": 1, "port": ATMPort},
"PA-FE-TX": {"nb_ports": 1, "port": FastEthernetPort},
"PA-GE": {"nb_ports": 1, "port": GigabitEthernetPort},
"PA-POS-OC3": {"nb_ports": 1, "port": POSPort},
}
WIC_MATRIX = {"WIC-1ENET": {"nb_ports": 1,
"port": EthernetPort},
"WIC-1T": {"nb_ports": 1,
"port": SerialPort},
"WIC-2T": {"nb_ports": 2,
"port": SerialPort}
}
WIC_MATRIX = {
"WIC-1ENET": {"nb_ports": 1, "port": EthernetPort},
"WIC-1T": {"nb_ports": 1, "port": SerialPort},
"WIC-2T": {"nb_ports": 2, "port": SerialPort},
}
def __new__(cls, properties):
-1
View File
@@ -22,7 +22,6 @@ from .serial_port import SerialPort
class POSPort(SerialPort):
@staticmethod
def long_name_type():
"""
+1 -4
View File
@@ -22,7 +22,6 @@ from .port import Port
class SerialPort(Port):
@staticmethod
def long_name_type():
"""
@@ -61,6 +60,4 @@ class SerialPort(Port):
:return: dictionary
"""
return {"Frame Relay": "DLT_FRELAY",
"Cisco HDLC": "DLT_C_HDLC",
"Cisco PPP": "DLT_PPP_SERIAL"}
return {"Frame Relay": "DLT_FRELAY", "Cisco HDLC": "DLT_C_HDLC", "Cisco PPP": "DLT_PPP_SERIAL"}
+90 -56
View File
@@ -46,6 +46,7 @@ from .import_project import import_project
from .controller_error import ControllerError, ControllerForbiddenError, ControllerNotFoundError
import logging
log = logging.getLogger(__name__)
@@ -58,6 +59,7 @@ def open_required(func):
if self._status == "closed":
raise ControllerForbiddenError("The project is not opened")
return func(self, *args, **kwargs)
return wrapper
@@ -70,10 +72,29 @@ class Project:
:param status: Status of the project (opened / closed)
"""
def __init__(self, name=None, project_id=None, path=None, controller=None, status="opened",
filename=None, auto_start=False, auto_open=False, auto_close=True,
scene_height=1000, scene_width=2000, zoom=100, show_layers=False, snap_to_grid=False, show_grid=False,
grid_size=75, drawing_grid_size=25, show_interface_labels=False, variables=None, supplier=None):
def __init__(
self,
name=None,
project_id=None,
path=None,
controller=None,
status="opened",
filename=None,
auto_start=False,
auto_open=False,
auto_close=True,
scene_height=1000,
scene_width=2000,
zoom=100,
show_layers=False,
snap_to_grid=False,
show_grid=False,
grid_size=75,
drawing_grid_size=25,
show_interface_labels=False,
variables=None,
supplier=None,
):
self._controller = controller
assert name is not None
@@ -160,11 +181,7 @@ class Project:
# update on computes
for compute in list(self._project_created_on_compute):
await compute.put(
f"/projects/{self._id}", {
"variables": self.variables
}
)
await compute.put(f"/projects/{self._id}", {"variables": self.variables})
def reset(self):
"""
@@ -409,7 +426,9 @@ class Project:
raise ControllerError(f"Could not create project directory: {e}")
if '"' in path:
raise ControllerForbiddenError("You are not allowed to use \" in the project directory path. Not supported by Dynamips.")
raise ControllerForbiddenError(
'You are not allowed to use " in the project directory path. Not supported by Dynamips.'
)
self._path = path
@@ -465,7 +484,7 @@ class Project:
if base_name in self._allocated_node_names:
base_name = re.sub(r"[0-9]+$", "{0}", base_name)
if '{0}' in base_name or '{id}' in base_name:
if "{0}" in base_name or "{id}" in base_name:
# base name is a template, replace {0} or {id} by an unique identifier
for number in range(1, 1000000):
try:
@@ -524,16 +543,9 @@ class Project:
if compute not in self._project_created_on_compute:
# For a local server we send the project path
if compute.id == "local":
data = {
"name": self._name,
"project_id": self._id,
"path": self._path
}
data = {"name": self._name, "project_id": self._id, "path": self._path}
else:
data = {
"name": self._name,
"project_id": self._id
}
data = {"name": self._name, "project_id": self._id}
if self._variables:
data["variables"] = self._variables
@@ -568,7 +580,9 @@ class Project:
# to generate MAC addresses) when creating multiple IOU node at the same time
if "properties" in kwargs.keys():
# allocate a new application id for nodes loaded from the project
kwargs.get("properties")["application_id"] = get_next_application_id(self._controller.projects, self._computes)
kwargs.get("properties")["application_id"] = get_next_application_id(
self._controller.projects, self._computes
)
elif "application_id" not in kwargs.keys() and not kwargs.get("properties"):
# allocate a new application id for nodes added to the project
kwargs["application_id"] = get_next_application_id(self._controller.projects, self._computes)
@@ -821,7 +835,7 @@ class Project:
# don't remove supplier's logo
if self.supplier:
try:
logo = self.supplier['logo']
logo = self.supplier["logo"]
pictures.remove(logo)
except KeyError:
pass
@@ -846,7 +860,9 @@ class Project:
try:
project_directory = get_default_project_directory()
if not os.path.commonprefix([project_directory, self.path]) == project_directory:
raise ControllerError(f"Project '{self._name}' cannot be deleted because it is not in the default project directory: '{project_directory}'")
raise ControllerError(
f"Project '{self._name}' cannot be deleted because it is not in the default project directory: '{project_directory}'"
)
shutil.rmtree(self.path)
except OSError as e:
raise ControllerError(f"Cannot delete project directory {self.path}: {str(e)}")
@@ -906,7 +922,7 @@ class Project:
try:
project_data = load_topology(path)
#load meta of project
# load meta of project
keys_to_load = [
"auto_start",
"auto_close",
@@ -919,7 +935,7 @@ class Project:
"show_grid",
"grid_size",
"drawing_grid_size",
"show_interface_labels"
"show_interface_labels",
]
for key in keys_to_load:
@@ -944,7 +960,7 @@ class Project:
node_id = node.pop("node_id", str(uuid.uuid4()))
await self.add_node(compute, name, node_id, dump=False, **node)
for link_data in topology.get("links", []):
if 'link_id' not in link_data.keys():
if "link_id" not in link_data.keys():
# skip the link
continue
link = await self.add_link(link_id=link_data["link_id"])
@@ -954,12 +970,26 @@ class Project:
node = self.get_node(node_link["node_id"])
port = node.get_port(node_link["adapter_number"], node_link["port_number"])
if port is None:
log.warning("Port {}/{} for {} not found".format(node_link["adapter_number"], node_link["port_number"], node.name))
log.warning(
"Port {}/{} for {} not found".format(
node_link["adapter_number"], node_link["port_number"], node.name
)
)
continue
if port.link is not None:
log.warning("Port {}/{} is already connected to link ID {}".format(node_link["adapter_number"], node_link["port_number"], port.link.id))
log.warning(
"Port {}/{} is already connected to link ID {}".format(
node_link["adapter_number"], node_link["port_number"], port.link.id
)
)
continue
await link.add_node(node, node_link["adapter_number"], node_link["port_number"], label=node_link.get("label"), dump=False)
await link.add_node(
node,
node_link["adapter_number"],
node_link["port_number"],
label=node_link.get("label"),
dump=False,
)
if len(link.nodes) != 2:
# a link should have 2 attached nodes, this can happen with corrupted projects
await self.delete_link(link.id, force_delete=True)
@@ -1039,18 +1069,27 @@ class Project:
with tempfile.TemporaryDirectory(dir=working_dir) as tmpdir:
# Do not compress the exported project when duplicating
with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream:
await export_project(zstream, self, tmpdir, keep_compute_id=True, allow_all_nodes=True, reset_mac_addresses=reset_mac_addresses)
await export_project(
zstream,
self,
tmpdir,
keep_compute_id=True,
allow_all_nodes=True,
reset_mac_addresses=reset_mac_addresses,
)
# export the project to a temporary location
project_path = os.path.join(tmpdir, "project.gns3p")
log.info(f"Exporting project to '{project_path}'")
async with aiofiles.open(project_path, 'wb') as f:
async with aiofiles.open(project_path, "wb") as f:
async for chunk in zstream:
await f.write(chunk)
# import the temporary project
with open(project_path, "rb") as f:
project = await import_project(self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True)
project = await import_project(
self._controller, str(uuid.uuid4()), f, location=location, name=name, keep_compute_id=True
)
log.info(f"Project '{project.name}' duplicated in {time.time() - begin:.4f} seconds")
except (ValueError, OSError, UnicodeEncodeError) as e:
@@ -1143,31 +1182,26 @@ class Project:
data = copy.deepcopy(node.__json__(topology_dump=True))
# Some properties like internal ID should not be duplicated
for unique_property in (
'node_id',
'name',
'mac_addr',
'mac_address',
'compute_id',
'application_id',
'dynamips_id'):
"node_id",
"name",
"mac_addr",
"mac_address",
"compute_id",
"application_id",
"dynamips_id",
):
data.pop(unique_property, None)
if 'properties' in data:
data['properties'].pop(unique_property, None)
node_type = data.pop('node_type')
data['x'] = x
data['y'] = y
data['z'] = z
data['locked'] = False # duplicated node must not be locked
if "properties" in data:
data["properties"].pop(unique_property, None)
node_type = data.pop("node_type")
data["x"] = x
data["y"] = y
data["z"] = z
data["locked"] = False # duplicated node must not be locked
new_node_uuid = str(uuid.uuid4())
new_node = await self.add_node(node.compute,
node.name,
new_node_uuid,
node_type=node_type,
**data)
new_node = await self.add_node(node.compute, node.name, new_node_uuid, node_type=node_type, **data)
try:
await node.post("/duplicate", timeout=None, data={
"destination_node_id": new_node_uuid
})
await node.post("/duplicate", timeout=None, data={"destination_node_id": new_node_uuid})
except ControllerNotFoundError:
await self.delete_node(new_node_uuid)
raise ControllerError("This node type cannot be duplicated")
@@ -1182,7 +1216,7 @@ class Project:
"nodes": len(self._nodes),
"links": len(self._links),
"drawings": len(self._drawings),
"snapshots": len(self._snapshots)
"snapshots": len(self._snapshots),
}
def __json__(self):
@@ -1205,7 +1239,7 @@ class Project:
"drawing_grid_size": self._drawing_grid_size,
"show_interface_labels": self._show_interface_labels,
"supplier": self._supplier,
"variables": self._variables
"variables": self._variables,
}
def __repr__(self):
+18 -6
View File
@@ -32,6 +32,7 @@ from .export_project import export_project
from .import_project import import_project
import logging
log = logging.getLogger(__name__)
@@ -48,17 +49,26 @@ class Snapshot:
assert filename or name, "You need to pass a name or a filename"
self._id = str(uuid.uuid4()) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk
self._id = str(
uuid.uuid4()
) # We don't need to keep id between project loading because they are use only as key for operation like delete, update.. but have no impact on disk
self._project = project
if name:
self._name = name
self._created_at = datetime.now().timestamp()
filename = self._name + "_" + datetime.utcfromtimestamp(self._created_at).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT) + ".gns3project"
filename = (
self._name
+ "_"
+ datetime.utcfromtimestamp(self._created_at).replace(tzinfo=None).strftime(FILENAME_TIME_FORMAT)
+ ".gns3project"
)
else:
self._name = filename.split("_")[0]
datestring = filename.replace(self._name + "_", "").split(".")[0]
try:
self._created_at = datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp()
self._created_at = (
datetime.strptime(datestring, FILENAME_TIME_FORMAT).replace(tzinfo=timezone.utc).timestamp()
)
except ValueError:
self._created_at = datetime.utcnow().timestamp()
self._path = os.path.join(project.path, "snapshots", filename)
@@ -99,7 +109,7 @@ class Snapshot:
# Do not compress the snapshots
with aiozipstream.ZipFile(compression=zipfile.ZIP_STORED) as zstream:
await export_project(zstream, self._project, tmpdir, keep_compute_id=True, allow_all_nodes=True)
async with aiofiles.open(self.path, 'wb') as f:
async with aiofiles.open(self.path, "wb") as f:
async for chunk in zstream:
await f.write(chunk)
log.info(f"Snapshot '{self.name}' created in {time.time() - begin:.4f} seconds")
@@ -121,7 +131,9 @@ class Snapshot:
if os.path.exists(project_files_path):
await wait_run_in_executor(shutil.rmtree, project_files_path)
with open(self._path, "rb") as f:
project = await import_project(self._project.controller, self._project.id, f, location=self._project.path)
project = await import_project(
self._project.controller, self._project.id, f, location=self._project.path
)
except (OSError, PermissionError) as e:
raise ControllerError(str(e))
await project.open()
@@ -133,5 +145,5 @@ class Snapshot:
"snapshot_id": self._id,
"name": self._name,
"created_at": int(self._created_at),
"project_id": self._project.id
"project_id": self._project.id,
}
+121 -105
View File
@@ -16,115 +16,131 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
CLASSIC_SYMBOL_THEME = {"cloud": ":/symbols/classic/cloud.svg",
"ethernet_switch": ":/symbols/classic/ethernet_switch.svg",
"ethernet_hub": ":/symbols/classic/hub.svg",
"frame_relay_switch": ":/symbols/classic/frame_relay_switch.svg",
"atm_switch": ":/symbols/classic/atm_switch.svg",
"router": ":/symbols/classic/router.svg",
"multilayer_switch": ":/symbols/classic/multilayer_switch.svg",
"firewall": ":/symbols/classic/firewall.svg",
"computer": ":/symbols/classic/computer.svg",
"vpcs_guest": ":/symbols/classic/vpcs_guest.svg",
"qemu_guest": ":/symbols/classic/qemu_guest.svg",
"vbox_guest": ":/symbols/classic/vbox_guest.svg",
"vmware_guest": ":/symbols/classic/vmware_guest.svg",
"docker_guest": ":/symbols/classic/docker_guest.svg"}
CLASSIC_SYMBOL_THEME = {
"cloud": ":/symbols/classic/cloud.svg",
"ethernet_switch": ":/symbols/classic/ethernet_switch.svg",
"ethernet_hub": ":/symbols/classic/hub.svg",
"frame_relay_switch": ":/symbols/classic/frame_relay_switch.svg",
"atm_switch": ":/symbols/classic/atm_switch.svg",
"router": ":/symbols/classic/router.svg",
"multilayer_switch": ":/symbols/classic/multilayer_switch.svg",
"firewall": ":/symbols/classic/firewall.svg",
"computer": ":/symbols/classic/computer.svg",
"vpcs_guest": ":/symbols/classic/vpcs_guest.svg",
"qemu_guest": ":/symbols/classic/qemu_guest.svg",
"vbox_guest": ":/symbols/classic/vbox_guest.svg",
"vmware_guest": ":/symbols/classic/vmware_guest.svg",
"docker_guest": ":/symbols/classic/docker_guest.svg",
}
AFFINITY_SQUARE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/blue/hub.svg",
"frame_relay_switch.svg": ":/symbols/affinity/square/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/square/blue/atm.svg",
"router": ":/symbols/affinity/square/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/square/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/blue/firewall3.svg",
"computer": ":/symbols/affinity/square/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/square/blue/client.svg",
"qemu_guest": ":/symbols/affinity/square/blue/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/blue/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/square/blue/docker.svg"}
AFFINITY_SQUARE_BLUE_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/square/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/blue/hub.svg",
"frame_relay_switch.svg": ":/symbols/affinity/square/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/square/blue/atm.svg",
"router": ":/symbols/affinity/square/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/square/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/blue/firewall3.svg",
"computer": ":/symbols/affinity/square/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/square/blue/client.svg",
"qemu_guest": ":/symbols/affinity/square/blue/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/blue/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/square/blue/docker.svg",
}
AFFINITY_SQUARE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/red/isdn.svg",
"atm_switch": ":/symbols/affinity/square/red/atm.svg",
"router": ":/symbols/affinity/square/red/router.svg",
"multilayer_switch": ":/symbols/affinity/square/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/red/firewall3.svg",
"computer": ":/symbols/affinity/square/red/client.svg",
"vpcs_guest": ":/symbols/affinity/square/red/client.svg",
"qemu_guest": ":/symbols/affinity/square/red/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/red/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/red/vmware.svg",
"docker_guest": ":/symbols/affinity/square/red/docker.svg"}
AFFINITY_SQUARE_RED_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/square/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/red/isdn.svg",
"atm_switch": ":/symbols/affinity/square/red/atm.svg",
"router": ":/symbols/affinity/square/red/router.svg",
"multilayer_switch": ":/symbols/affinity/square/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/red/firewall3.svg",
"computer": ":/symbols/affinity/square/red/client.svg",
"vpcs_guest": ":/symbols/affinity/square/red/client.svg",
"qemu_guest": ":/symbols/affinity/square/red/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/red/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/red/vmware.svg",
"docker_guest": ":/symbols/affinity/square/red/docker.svg",
}
AFFINITY_SQUARE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/square/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/square/gray/atm.svg",
"router": ":/symbols/affinity/square/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/square/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/gray/firewall3.svg",
"computer": ":/symbols/affinity/square/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/square/gray/client.svg",
"qemu_guest": ":/symbols/affinity/square/gray/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/gray/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/gray/vmware.svg",
"docker_guest": ":/symbols/affinity/square/gray/docker.svg"}
AFFINITY_SQUARE_GRAY_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/square/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/square/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/square/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/square/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/square/gray/atm.svg",
"router": ":/symbols/affinity/square/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/square/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/square/gray/firewall3.svg",
"computer": ":/symbols/affinity/square/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/square/gray/client.svg",
"qemu_guest": ":/symbols/affinity/square/gray/client_vm.svg",
"vbox_guest": ":/symbols/affinity/square/gray/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/square/gray/vmware.svg",
"docker_guest": ":/symbols/affinity/square/gray/docker.svg",
}
AFFINITY_CIRCLE_BLUE_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/blue/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/blue/atm.svg",
"router": ":/symbols/affinity/circle/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/blue/firewall3.svg",
"computer": ":/symbols/affinity/circle/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/blue/client.svg",
"qemu_guest": ":/symbols/affinity/circle/blue/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/blue/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/blue/docker.svg"}
AFFINITY_CIRCLE_BLUE_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/circle/blue/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/blue/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/blue/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/blue/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/blue/atm.svg",
"router": ":/symbols/affinity/circle/blue/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/blue/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/blue/firewall3.svg",
"computer": ":/symbols/affinity/circle/blue/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/blue/client.svg",
"qemu_guest": ":/symbols/affinity/circle/blue/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/blue/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/blue/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/blue/docker.svg",
}
AFFINITY_CIRCLE_RED_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/red/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/red/atm.svg",
"router": ":/symbols/affinity/circle/red/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/red/firewall3.svg",
"computer": ":/symbols/affinity/circle/red/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/red/client.svg",
"qemu_guest": ":/symbols/affinity/circle/red/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/red/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/red/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/red/docker.svg"}
AFFINITY_CIRCLE_RED_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/circle/red/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/red/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/red/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/red/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/red/atm.svg",
"router": ":/symbols/affinity/circle/red/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/red/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/red/firewall3.svg",
"computer": ":/symbols/affinity/circle/red/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/red/client.svg",
"qemu_guest": ":/symbols/affinity/circle/red/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/red/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/red/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/red/docker.svg",
}
AFFINITY_CIRCLE_GRAY_SYMBOL_THEME = {"cloud": ":/symbols/affinity/circle/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/gray/atm.svg",
"router": ":/symbols/affinity/circle/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/gray/firewall3.svg",
"computer": ":/symbols/affinity/circle/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/gray/client.svg",
"qemu_guest": ":/symbols/affinity/circle/gray/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/gray/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/gray/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/gray/docker.svg"}
AFFINITY_CIRCLE_GRAY_SYMBOL_THEME = {
"cloud": ":/symbols/affinity/circle/gray/cloud.svg",
"ethernet_switch": ":/symbols/affinity/circle/gray/switch.svg",
"ethernet_hub": ":/symbols/affinity/circle/gray/hub.svg",
"frame_relay_switch": ":/symbols/affinity/circle/gray/isdn.svg",
"atm_switch": ":/symbols/affinity/circle/gray/atm.svg",
"router": ":/symbols/affinity/circle/gray/router.svg",
"multilayer_switch": ":/symbols/affinity/circle/gray/switch_multilayer.svg",
"firewall": ":/symbols/affinity/circle/gray/firewall3.svg",
"computer": ":/symbols/affinity/circle/gray/client.svg",
"vpcs_guest": ":/symbols/affinity/circle/gray/client.svg",
"qemu_guest": ":/symbols/affinity/circle/gray/client_vm.svg",
"vbox_guest": ":/symbols/affinity/circle/gray/virtualbox.svg",
"vmware_guest": ":/symbols/affinity/circle/gray/vmware.svg",
"docker_guest": ":/symbols/affinity/circle/gray/docker.svg",
}
BUILTIN_SYMBOL_THEMES = {"Classic": CLASSIC_SYMBOL_THEME,
"Affinity-square-blue": AFFINITY_SQUARE_BLUE_SYMBOL_THEME,
"Affinity-square-red": AFFINITY_SQUARE_RED_SYMBOL_THEME,
"Affinity-square-gray": AFFINITY_SQUARE_GRAY_SYMBOL_THEME,
"Affinity-circle-blue": AFFINITY_CIRCLE_BLUE_SYMBOL_THEME,
"Affinity-circle-red": AFFINITY_CIRCLE_RED_SYMBOL_THEME,
"Affinity-circle-gray": AFFINITY_CIRCLE_GRAY_SYMBOL_THEME}
BUILTIN_SYMBOL_THEMES = {
"Classic": CLASSIC_SYMBOL_THEME,
"Affinity-square-blue": AFFINITY_SQUARE_BLUE_SYMBOL_THEME,
"Affinity-square-red": AFFINITY_SQUARE_RED_SYMBOL_THEME,
"Affinity-square-gray": AFFINITY_SQUARE_GRAY_SYMBOL_THEME,
"Affinity-circle-blue": AFFINITY_CIRCLE_BLUE_SYMBOL_THEME,
"Affinity-circle-red": AFFINITY_CIRCLE_RED_SYMBOL_THEME,
"Affinity-circle-gray": AFFINITY_CIRCLE_GRAY_SYMBOL_THEME,
}
+14 -15
View File
@@ -25,6 +25,7 @@ from ..utils.picture import get_size
from ..config import Config
import logging
log = logging.getLogger(__name__)
@@ -79,33 +80,31 @@ class Symbols:
if get_resource("symbols"):
for root, _, files in os.walk(get_resource("symbols")):
for filename in files:
if filename.startswith('.'):
if filename.startswith("."):
continue
symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), get_resource("symbols"))).replace('\\', '/')
theme = posixpath.dirname(symbol_file).replace('/', '-').capitalize()
symbol_file = posixpath.normpath(
os.path.relpath(os.path.join(root, filename), get_resource("symbols"))
).replace("\\", "/")
theme = posixpath.dirname(symbol_file).replace("/", "-").capitalize()
if not theme:
continue
symbol_id = ':/symbols/' + symbol_file
symbols.append({'symbol_id': symbol_id,
'filename': filename,
'theme': theme,
'builtin': True})
symbol_id = ":/symbols/" + symbol_file
symbols.append({"symbol_id": symbol_id, "filename": filename, "theme": theme, "builtin": True})
self._symbols_path[symbol_id] = os.path.join(root, filename)
directory = self.symbols_path()
if directory:
for root, _, files in os.walk(directory):
for filename in files:
if filename.startswith('.'):
if filename.startswith("."):
continue
symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), directory)).replace('\\', '/')
theme = posixpath.dirname(symbol_file).replace('/', '-').capitalize()
symbol_file = posixpath.normpath(os.path.relpath(os.path.join(root, filename), directory)).replace(
"\\", "/"
)
theme = posixpath.dirname(symbol_file).replace("/", "-").capitalize()
if not theme:
theme = "Custom symbols"
symbols.append({'symbol_id': symbol_file,
'filename': filename,
'builtin': False,
'theme': theme})
symbols.append({"symbol_id": symbol_file, "filename": filename, "builtin": False, "theme": theme})
self._symbols_path[symbol_file] = os.path.join(root, filename)
symbols.sort(key=lambda x: x["filename"])
+63 -64
View File
@@ -35,6 +35,7 @@ from gns3server.schemas.topology import Topology
from gns3server.schemas.dynamips_nodes import DynamipsCreate
import logging
log = logging.getLogger(__name__)
@@ -81,15 +82,10 @@ def project_to_topology(project):
"show_interface_labels": project.show_interface_labels,
"variables": project.variables,
"supplier": project.supplier,
"topology": {
"nodes": [],
"links": [],
"computes": [],
"drawings": []
},
"topology": {"nodes": [], "links": [], "computes": [], "drawings": []},
"type": "topology",
"revision": GNS3_FILE_FORMAT_REVISION,
"version": __version__
"version": __version__,
}
for node in project.nodes.values():
@@ -110,7 +106,10 @@ def project_to_topology(project):
for compute in project.computes:
if hasattr(compute, "__json__"):
compute = compute.__json__(topology_dump=True)
if compute["compute_id"] not in ("vm", "local", ):
if compute["compute_id"] not in (
"vm",
"local",
):
data["topology"]["computes"].append(compute)
elif isinstance(compute, dict):
data["topology"]["computes"].append(compute)
@@ -130,7 +129,11 @@ def load_topology(path):
raise ControllerError(f"Could not load topology {path}: {str(e)}")
if topo.get("revision", 0) > GNS3_FILE_FORMAT_REVISION:
raise ControllerError("This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format(topo["revision"], topo["version"]))
raise ControllerError(
"This project was created with more recent version of GNS3 (file revision: {}). Please upgrade GNS3 to version {} or later".format(
topo["revision"], topo["version"]
)
)
changed = False
if "revision" not in topo or topo["revision"] < GNS3_FILE_FORMAT_REVISION:
@@ -317,12 +320,7 @@ def _convert_1_3_later(topo, topo_path):
"auto_start": topo.get("auto_start", False),
"name": topo["name"],
"project_id": topo.get("project_id"),
"topology": {
"links": [],
"drawings": [],
"computes": [],
"nodes": []
}
"topology": {"links": [], "drawings": [], "computes": [], "nodes": []},
}
if new_topo["project_id"] is None:
new_topo["project_id"] = str(uuid.uuid4()) # Could arrive for topologues with drawing only
@@ -336,7 +334,7 @@ def _convert_1_3_later(topo, topo_path):
compute = {
"host": server.get("host", "localhost"),
"port": server.get("port", 3080),
"protocol": server.get("protocol", "http")
"protocol": server.get("protocol", "http"),
}
if server["local"]:
compute["compute_id"] = "local"
@@ -407,27 +405,40 @@ def _convert_1_3_later(topo, topo_path):
node["symbol"] = ":/symbols/hub.svg"
node["properties"]["ports_mapping"] = []
for port in old_node.get("ports", []):
node["properties"]["ports_mapping"].append({
"name": "Ethernet{}".format(port["port_number"] - 1),
"port_number": port["port_number"] - 1
})
node["properties"]["ports_mapping"].append(
{"name": "Ethernet{}".format(port["port_number"] - 1), "port_number": port["port_number"] - 1}
)
elif old_node["type"] == "EthernetSwitch":
node["node_type"] = "ethernet_switch"
node["symbol"] = ":/symbols/ethernet_switch.svg"
node["console_type"] = None
node["properties"]["ports_mapping"] = []
for port in old_node.get("ports", []):
node["properties"]["ports_mapping"].append({
"name": "Ethernet{}".format(port["port_number"] - 1),
"port_number": port["port_number"] - 1,
"type": port["type"],
"vlan": port["vlan"]
})
node["properties"]["ports_mapping"].append(
{
"name": "Ethernet{}".format(port["port_number"] - 1),
"port_number": port["port_number"] - 1,
"type": port["type"],
"vlan": port["vlan"],
}
)
elif old_node["type"] == "FrameRelaySwitch":
node["node_type"] = "frame_relay_switch"
node["symbol"] = ":/symbols/frame_relay_switch.svg"
node["console_type"] = None
elif old_node["type"].upper() in ["C1700", "C2600", "C2691", "C3600", "C3620", "C3640", "C3660", "C3725", "C3745", "C7200", "EtherSwitchRouter"]:
elif old_node["type"].upper() in [
"C1700",
"C2600",
"C2691",
"C3600",
"C3620",
"C3640",
"C3660",
"C3725",
"C3745",
"C7200",
"EtherSwitchRouter",
]:
if node["symbol"] is None:
node["symbol"] = ":/symbols/router.svg"
node["node_type"] = "dynamips"
@@ -485,23 +496,20 @@ def _convert_1_3_later(topo, topo_path):
source_node = {
"adapter_number": ports[old_link["source_port_id"]].get("adapter_number", 0),
"port_number": ports[old_link["source_port_id"]].get("port_number", 0),
"node_id": node_id_to_node_uuid[old_link["source_node_id"]]
"node_id": node_id_to_node_uuid[old_link["source_node_id"]],
}
nodes.append(source_node)
destination_node = {
"adapter_number": ports[old_link["destination_port_id"]].get("adapter_number", 0),
"port_number": ports[old_link["destination_port_id"]].get("port_number", 0),
"node_id": node_id_to_node_uuid[old_link["destination_node_id"]]
"node_id": node_id_to_node_uuid[old_link["destination_node_id"]],
}
nodes.append(destination_node)
except KeyError:
continue
link = {
"link_id": str(uuid.uuid4()),
"nodes": nodes
}
link = {"link_id": str(uuid.uuid4()), "nodes": nodes}
new_topo["topology"]["links"].append(link)
# Ellipse
@@ -514,7 +522,7 @@ def _convert_1_3_later(topo, topo_path):
rx=int(ellipse["width"] / 2),
ry=int(ellipse["height"] / 2),
fill=ellipse.get("color", "#ffffff"),
border_style=_convert_border_style(ellipse)
border_style=_convert_border_style(ellipse),
)
new_ellipse = {
"drawing_id": str(uuid.uuid4()),
@@ -522,7 +530,7 @@ def _convert_1_3_later(topo, topo_path):
"y": int(ellipse["y"]),
"z": int(ellipse.get("z", 0)),
"rotation": int(ellipse.get("rotation", 0)),
"svg": svg
"svg": svg,
}
new_topo["topology"]["drawings"].append(new_ellipse)
@@ -543,12 +551,14 @@ def _convert_1_3_later(topo, topo_path):
height=int(font_info[1]) * 2,
width=int(font_info[1]) * len(note["text"]),
fill="#" + note.get("color", "#00000000")[-6:],
opacity=round(1.0 / 255 * int(note.get("color", "#ffffffff")[:3][-2:], base=16), 2), # Extract the alpha channel from the hexa version
opacity=round(
1.0 / 255 * int(note.get("color", "#ffffffff")[:3][-2:], base=16), 2
), # Extract the alpha channel from the hexa version
family=font_info[0],
size=int(font_info[1]),
weight=weight,
style=style,
text=html.escape(note["text"])
text=html.escape(note["text"]),
)
new_note = {
"drawing_id": str(uuid.uuid4()),
@@ -556,7 +566,7 @@ def _convert_1_3_later(topo, topo_path):
"y": int(note["y"]),
"z": int(note.get("z", 0)),
"rotation": int(note.get("rotation", 0)),
"svg": svg
"svg": svg,
}
new_topo["topology"]["drawings"].append(new_note)
@@ -576,7 +586,7 @@ def _convert_1_3_later(topo, topo_path):
"y": int(image["y"]),
"z": int(image.get("z", 0)),
"rotation": int(image.get("rotation", 0)),
"svg": os.path.basename(img_path)
"svg": os.path.basename(img_path),
}
new_topo["topology"]["drawings"].append(new_image)
@@ -586,7 +596,7 @@ def _convert_1_3_later(topo, topo_path):
height=int(rectangle["height"]),
width=int(rectangle["width"]),
fill=rectangle.get("color", "#ffffff"),
border_style=_convert_border_style(rectangle)
border_style=_convert_border_style(rectangle),
)
new_rectangle = {
"drawing_id": str(uuid.uuid4()),
@@ -594,7 +604,7 @@ def _convert_1_3_later(topo, topo_path):
"y": int(rectangle["y"]),
"z": int(rectangle.get("z", 0)),
"rotation": int(rectangle.get("rotation", 0)),
"svg": svg
"svg": svg,
}
new_topo["topology"]["drawings"].append(new_rectangle)
@@ -608,12 +618,7 @@ def _convert_1_3_later(topo, topo_path):
def _convert_border_style(element):
QT_DASH_TO_SVG = {
2: "25, 25",
3: "5, 25",
4: "5, 25, 25",
5: "25, 25, 5, 25, 5"
}
QT_DASH_TO_SVG = {2: "25, 25", 3: "5, 25", 4: "5, 25, 25", 5: "25, 25, 5, 25, 5"}
border_style = int(element.get("border_style", 0))
style = ""
if border_style == 1: # No border
@@ -623,8 +628,7 @@ def _convert_border_style(element):
else:
style += f'stroke-dasharray="{QT_DASH_TO_SVG[border_style]}" '
style += 'stroke="{stroke}" stroke-width="{stroke_width}"'.format(
stroke=element.get("border_color", "#000000"),
stroke_width=element.get("border_width", 2)
stroke=element.get("border_color", "#000000"), stroke_width=element.get("border_width", 2)
)
return style
@@ -639,7 +643,7 @@ def _convert_label(label):
"rotation": 0,
"style": style,
"x": int(label["x"]),
"y": int(label["y"])
"y": int(label["y"]),
}
@@ -677,14 +681,14 @@ def _create_cloud(node, old_node, icon):
"type": port_type,
"lport": int(lport),
"rhost": rhost,
"rport": int(rport)
"rport": int(rport),
}
else:
port = {
"interface": old_port["name"].split(":")[1],
"name": old_port["name"].split(":")[1],
"port_number": len(ports) + 1,
"type": port_type
"type": port_type,
}
keep_ports.append(old_port)
ports.append(port)
@@ -716,10 +720,14 @@ def _convert_snapshots(topo_dir):
if is_gns3_topo:
snapshot_arc = os.path.join(new_snapshots_dir, snapshot + ".gns3project")
with zipfile.ZipFile(snapshot_arc, 'w', allowZip64=True) as myzip:
with zipfile.ZipFile(snapshot_arc, "w", allowZip64=True) as myzip:
for root, dirs, files in os.walk(snapshot_dir):
for file in files:
myzip.write(os.path.join(root, file), os.path.relpath(os.path.join(root, file), snapshot_dir), compress_type=zipfile.ZIP_DEFLATED)
myzip.write(
os.path.join(root, file),
os.path.relpath(os.path.join(root, file), snapshot_dir),
compress_type=zipfile.ZIP_DEFLATED,
)
shutil.rmtree(old_snapshots_dir)
@@ -735,16 +743,7 @@ def _convert_qemu_node(node, old_node):
node["console_type"] = None
node["node_type"] = "nat"
del old_node["properties"]
node["properties"] = {
"ports": [
{
"interface": "eth1",
"name": "nat0",
"port_number": 0,
"type": "ethernet"
}
]
}
node["properties"] = {"ports": [{"interface": "eth1", "name": "nat0", "port_number": 0, "type": "ethernet"}]}
if node["symbol"] is None:
node["symbol"] = ":/symbols/cloud.svg"
return node
+46 -25
View File
@@ -21,7 +21,6 @@ from .link import Link
class UDPLink(Link):
def __init__(self, project, link_id=None):
super().__init__(project, link_id=link_id)
self._created = False
@@ -67,26 +66,32 @@ class UDPLink(Link):
node2_filters = self.get_active_filters()
# Create the tunnel on both side
self._link_data.append({
"lport": self._node1_port,
"rhost": node2_host,
"rport": self._node2_port,
"type": "nio_udp",
"filters": node1_filters,
"suspend": self._suspended
})
self._link_data.append(
{
"lport": self._node1_port,
"rhost": node2_host,
"rport": self._node2_port,
"type": "nio_udp",
"filters": node1_filters,
"suspend": self._suspended,
}
)
await node1.post(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120)
self._link_data.append({
"lport": self._node2_port,
"rhost": node1_host,
"rport": self._node1_port,
"type": "nio_udp",
"filters": node2_filters,
"suspend": self._suspended
})
self._link_data.append(
{
"lport": self._node2_port,
"rhost": node1_host,
"rport": self._node1_port,
"type": "nio_udp",
"filters": node2_filters,
"suspend": self._suspended,
}
)
try:
await node2.post(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=120)
await node2.post(
f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=120
)
except Exception as e:
# We clean the first NIO
await node1.delete(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", timeout=120)
@@ -116,14 +121,18 @@ class UDPLink(Link):
self._link_data[0]["filters"] = node1_filters
self._link_data[0]["suspend"] = self._suspended
if node1.node_type not in ("ethernet_switch", "ethernet_hub"):
await node1.put(f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120)
await node1.put(
f"/adapters/{adapter_number1}/ports/{port_number1}/nio", data=self._link_data[0], timeout=120
)
adapter_number2 = self._nodes[1]["adapter_number"]
port_number2 = self._nodes[1]["port_number"]
self._link_data[1]["filters"] = node2_filters
self._link_data[1]["suspend"] = self._suspended
if node2.node_type not in ("ethernet_switch", "ethernet_hub"):
await node2.put(f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=221)
await node2.put(
f"/adapters/{adapter_number2}/ports/{port_number2}/nio", data=self._link_data[1], timeout=221
)
async def delete(self):
"""
@@ -172,9 +181,13 @@ class UDPLink(Link):
if not capture_file_name:
capture_file_name = self.default_capture_file_name()
self._capture_node = self._choose_capture_side()
data = {"capture_file_name": capture_file_name,
"data_link_type": data_link_type}
await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/start_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]), data=data)
data = {"capture_file_name": capture_file_name, "data_link_type": data_link_type}
await self._capture_node["node"].post(
"/adapters/{adapter_number}/ports/{port_number}/start_capture".format(
adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]
),
data=data,
)
await super().start_capture(data_link_type=data_link_type, capture_file_name=capture_file_name)
async def stop_capture(self):
@@ -182,7 +195,11 @@ class UDPLink(Link):
Stop capture on a link
"""
if self._capture_node:
await self._capture_node["node"].post("/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]))
await self._capture_node["node"].post(
"/adapters/{adapter_number}/ports/{port_number}/stop_capture".format(
adapter_number=self._capture_node["adapter_number"], port_number=self._capture_node["port_number"]
)
)
self._capture_node = None
await super().stop_capture()
@@ -199,7 +216,11 @@ class UDPLink(Link):
ALWAYS_RUNNING_NODES_TYPE = ("cloud", "nat", "ethernet_switch", "ethernet_hub")
for node in self._nodes:
if node["node"].compute.id == "local" and node["node"].node_type in ALWAYS_RUNNING_NODES_TYPE and node["node"].status == "started":
if (
node["node"].compute.id == "local"
and node["node"].node_type in ALWAYS_RUNNING_NODES_TYPE
and node["node"].status == "started"
):
return node
for node in self._nodes: