mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-11-03 14:01:15 +02:00
Use black with -l 120 param.
This commit is contained in:
1 parent
f928738bd5
commit
c021e21309
194 files changed
+6034
-4564
No files matched your search
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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)
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
)
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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],
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
|
||||
@@ -22,7 +22,6 @@ from .serial_port import SerialPort
|
||||
|
||||
|
||||
class POSPort(SerialPort):
|
||||
|
||||
@staticmethod
|
||||
def long_name_type():
|
||||
"""
|
||||
|
||||
@@ -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"}
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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"])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in new issue
Block a user