mirror of
https://github.com/GNS3/gns3-server.git
synced 2026-08-27 20:40:13 +03:00
Cloud and NAT nodes need live host network interface data. Previously,
GET /projects/{project_id}/nodes/{node_id} returned cached properties
from creation time, so newly added host interfaces (e.g. kernel bridges
created by EthernetSwitch nodes) were invisible until the node was
deleted and recreated.
Now the controller fetches fresh data from the compute node before
returning the response, so host interface changes are reflected
immediately. Falls back to cached data if compute is unreachable.
867 lines
29 KiB
Python
867 lines
29 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# Copyright (C) 2016 GNS3 Technologies Inc.
|
|
#
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
import asyncio
|
|
import html
|
|
import copy
|
|
import uuid
|
|
import os
|
|
|
|
from .controller_error import (
|
|
ControllerError,
|
|
ControllerTimeoutError,
|
|
ComputeError,
|
|
ComputeConflictError
|
|
)
|
|
from .node_types import BUILTIN_NODE_TYPES
|
|
from .ports.port_factory import PortFactory, StandardPortFactory, DynamipsPortFactory
|
|
from ..utils.images import images_directories
|
|
from ..utils import macaddress_to_int, int_to_macaddress
|
|
from ..config import Config
|
|
|
|
|
|
import logging
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
|
|
class Node:
|
|
# These 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",
|
|
]
|
|
|
|
def __init__(self, project, compute, name, node_id=None, node_type=None, template_id=None, **kwargs):
|
|
"""
|
|
:param project: Project of the node
|
|
:param compute: Compute where the server will run
|
|
:param name: Node name
|
|
:param node_id: UUID of the node (integer)
|
|
:param node_type: Type of emulator
|
|
:param template_id: Template ID used to create this node
|
|
:param kwargs: Node properties
|
|
"""
|
|
|
|
assert node_type
|
|
|
|
if node_id is None:
|
|
self._id = str(uuid.uuid4())
|
|
else:
|
|
self._id = node_id
|
|
|
|
self._project = project
|
|
self._compute = compute
|
|
self._node_type = node_type
|
|
|
|
self._label = None
|
|
self._links = set()
|
|
self._name = None
|
|
self.name = name
|
|
self._console = None
|
|
self._console_type = None
|
|
self._aux = None
|
|
self._aux_type = None
|
|
self._properties = None
|
|
self._command_line = None
|
|
self._node_directory = None
|
|
self._status = "stopped"
|
|
self._template_id = template_id
|
|
self._x = 0
|
|
self._y = 0
|
|
self._z = 1 # default z value is 1
|
|
self._locked = False
|
|
self._tags = []
|
|
self._ports = None
|
|
self._symbol = None
|
|
self._custom_adapters = []
|
|
if node_type == "iou":
|
|
self._port_name_format = "Ethernet{segment0}/{port0}"
|
|
self._port_by_adapter = 4
|
|
self.port_segment_size = 4
|
|
else:
|
|
self._port_name_format = "Ethernet{0}"
|
|
self._port_by_adapter = 1
|
|
self._port_segment_size = 0
|
|
self._first_port_name = None
|
|
self._console_auto_start = False
|
|
|
|
# This properties will be recomputed
|
|
ignore_properties = ("width", "height", "hover_symbol")
|
|
self.properties = kwargs.pop("properties", {})
|
|
|
|
# Update node properties with additional elements
|
|
for prop in kwargs:
|
|
if prop and prop not in ignore_properties:
|
|
if hasattr(self, prop):
|
|
try:
|
|
setattr(self, prop, kwargs[prop])
|
|
except AttributeError as e:
|
|
log.critical(f"Cannot set attribute '{prop}'")
|
|
raise e
|
|
else:
|
|
if prop not in self.CONTROLLER_ONLY_PROPERTIES and kwargs[prop] is not None and kwargs[prop] != "":
|
|
self.properties[prop] = kwargs[prop]
|
|
|
|
if self._symbol is None:
|
|
# compatibility with old node templates
|
|
if "default_symbol" in self.properties:
|
|
default_symbol = self.properties.pop("default_symbol")
|
|
if default_symbol.endswith("normal.svg"):
|
|
self.symbol = default_symbol[:-11] + ".svg"
|
|
else:
|
|
self.symbol = default_symbol
|
|
else:
|
|
self.symbol = ":/symbols/computer.svg"
|
|
|
|
def is_always_running(self):
|
|
"""
|
|
: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")
|
|
|
|
@property
|
|
def id(self):
|
|
return self._id
|
|
|
|
@property
|
|
def status(self):
|
|
return self._status
|
|
|
|
@property
|
|
def name(self):
|
|
return self._name
|
|
|
|
@name.setter
|
|
def name(self, new_name):
|
|
self._name = self._project.update_node_name(self, new_name)
|
|
# The text in label need to be always the node name
|
|
if self.label and self._label["text"] != self._name:
|
|
self._label["text"] = self._name
|
|
self._label["x"] = None # Center text
|
|
|
|
@property
|
|
def node_type(self):
|
|
return self._node_type
|
|
|
|
@property
|
|
def console(self):
|
|
return self._console
|
|
|
|
@console.setter
|
|
def console(self, val):
|
|
self._console = val
|
|
|
|
@property
|
|
def aux(self):
|
|
return self._aux
|
|
|
|
@aux.setter
|
|
def aux(self, val):
|
|
self._aux = val
|
|
|
|
@property
|
|
def console_type(self):
|
|
return self._console_type
|
|
|
|
@console_type.setter
|
|
def console_type(self, val):
|
|
self._console_type = val
|
|
|
|
@property
|
|
def aux_type(self):
|
|
return self._aux_type
|
|
|
|
@aux_type.setter
|
|
def aux_type(self, val):
|
|
self._aux_type = val
|
|
|
|
@property
|
|
def console_auto_start(self):
|
|
return self._console_auto_start
|
|
|
|
@console_auto_start.setter
|
|
def console_auto_start(self, val):
|
|
self._console_auto_start = val
|
|
|
|
@property
|
|
def properties(self):
|
|
return self._properties
|
|
|
|
@properties.setter
|
|
def properties(self, val):
|
|
self._properties = val
|
|
|
|
@property
|
|
def tags(self):
|
|
return self._tags
|
|
|
|
@tags.setter
|
|
def tags(self, val):
|
|
self._tags = val
|
|
|
|
def _base_config_file_content(self, path):
|
|
if not os.path.isabs(path):
|
|
path = os.path.join(self.project.controller.configs_path(), path)
|
|
try:
|
|
with open(path, encoding="utf-8", errors="ignore") as f:
|
|
return f.read()
|
|
except OSError:
|
|
return None
|
|
|
|
@property
|
|
def project(self):
|
|
return self._project
|
|
|
|
@property
|
|
def compute(self):
|
|
return self._compute
|
|
|
|
@property
|
|
def host(self):
|
|
"""
|
|
:returns: Domain or ip for console connection
|
|
"""
|
|
return self._compute.host
|
|
|
|
@property
|
|
def ports(self):
|
|
if self._ports is None:
|
|
self._list_ports()
|
|
return self._ports
|
|
|
|
@property
|
|
def x(self):
|
|
return self._x
|
|
|
|
@x.setter
|
|
def x(self, val):
|
|
self._x = val
|
|
|
|
@property
|
|
def y(self):
|
|
return self._y
|
|
|
|
@y.setter
|
|
def y(self, val):
|
|
self._y = val
|
|
|
|
@property
|
|
def z(self):
|
|
return self._z
|
|
|
|
@z.setter
|
|
def z(self, val):
|
|
self._z = val
|
|
|
|
@property
|
|
def locked(self):
|
|
return self._locked
|
|
|
|
@locked.setter
|
|
def locked(self, val):
|
|
self._locked = val
|
|
|
|
@property
|
|
def width(self):
|
|
return self._width
|
|
|
|
@property
|
|
def height(self):
|
|
return self._height
|
|
|
|
@property
|
|
def symbol(self):
|
|
return self._symbol
|
|
|
|
@symbol.setter
|
|
def symbol(self, val):
|
|
|
|
if val is None:
|
|
val = ":/symbols/computer.svg"
|
|
|
|
try:
|
|
if not val.startswith(":") and os.path.isabs(val):
|
|
default_symbol_directory = Config.instance().settings.Server.symbols_path
|
|
if os.path.commonprefix([default_symbol_directory, val]) != default_symbol_directory:
|
|
val = os.path.basename(val)
|
|
except OSError:
|
|
pass
|
|
|
|
self._symbol = val
|
|
try:
|
|
self._width, self._height, filetype = self._project.controller.symbols.get_size(val)
|
|
except (ValueError, OSError) as e:
|
|
log.error(f"Could not set symbol: {e}")
|
|
# If symbol is invalid we replace it by the default
|
|
self.symbol = ":/symbols/computer.svg"
|
|
if self._label is None:
|
|
# 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"],
|
|
# 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;"
|
|
|
|
self._label = {
|
|
"y": round(self._height / 2 + 10) * -1,
|
|
"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,
|
|
}
|
|
|
|
@property
|
|
def label(self):
|
|
return self._label
|
|
|
|
@label.setter
|
|
def label(self, val):
|
|
# The text in label need to be always the node name
|
|
val["text"] = self._name
|
|
self._label = val
|
|
|
|
@property
|
|
def port_name_format(self):
|
|
return self._port_name_format
|
|
|
|
@port_name_format.setter
|
|
def port_name_format(self, val):
|
|
self._port_name_format = val
|
|
|
|
@property
|
|
def port_segment_size(self):
|
|
return self._port_segment_size
|
|
|
|
@port_segment_size.setter
|
|
def port_segment_size(self, val):
|
|
self._port_segment_size = val
|
|
|
|
@property
|
|
def first_port_name(self):
|
|
return self._first_port_name
|
|
|
|
@first_port_name.setter
|
|
def first_port_name(self, val):
|
|
self._first_port_name = val
|
|
|
|
@property
|
|
def custom_adapters(self):
|
|
return self._custom_adapters
|
|
|
|
@custom_adapters.setter
|
|
def custom_adapters(self, val):
|
|
self._custom_adapters = val
|
|
|
|
def add_link(self, link):
|
|
"""
|
|
A link is connected to the node
|
|
"""
|
|
self._links.add(link)
|
|
|
|
def remove_link(self, link):
|
|
"""
|
|
A link is connected to the node
|
|
"""
|
|
self._links.remove(link)
|
|
|
|
@property
|
|
def links(self):
|
|
return self._links
|
|
|
|
async def create(self):
|
|
"""
|
|
Create the node on the compute
|
|
"""
|
|
data = self._node_data()
|
|
data["node_id"] = self._id
|
|
if self._node_type == "docker":
|
|
timeout = None
|
|
else:
|
|
timeout = 1200
|
|
trial = 0
|
|
while trial != 6:
|
|
try:
|
|
response = await self._compute.post(
|
|
f"/projects/{self._project.id}/{self._node_type}/nodes", data=data, timeout=timeout
|
|
)
|
|
except ComputeConflictError as e:
|
|
response = e.response()
|
|
if response.get("exception") == "ImageMissingError":
|
|
res = await self._upload_missing_image(self._node_type, response["image"])
|
|
if not res:
|
|
raise e
|
|
else:
|
|
raise e
|
|
else:
|
|
await self.parse_node_response(response.json)
|
|
return True
|
|
trial += 1
|
|
return False
|
|
|
|
async def update(self, **kwargs):
|
|
"""
|
|
Update the node on the compute
|
|
|
|
:param kwargs: Node properties
|
|
"""
|
|
|
|
# When updating properties used only on controller we don't need to call the compute
|
|
update_compute = False
|
|
old_json = self.asdict()
|
|
old_name = self._name
|
|
|
|
compute_properties = None
|
|
# Update node properties with additional elements
|
|
for prop in kwargs:
|
|
if getattr(self, prop) != kwargs[prop]:
|
|
if prop not in self.CONTROLLER_ONLY_PROPERTIES:
|
|
update_compute = True
|
|
|
|
# We update properties on the compute and wait for the answer from the compute node
|
|
if prop == "properties":
|
|
compute_properties = kwargs[prop]
|
|
else:
|
|
if (
|
|
prop == "name"
|
|
and self.status == "started"
|
|
and self._node_type not in BUILTIN_NODE_TYPES
|
|
):
|
|
raise ControllerError("Sorry, it is not possible to rename a node that is already powered on")
|
|
setattr(self, prop, kwargs[prop])
|
|
|
|
if compute_properties and "custom_adapters" in compute_properties:
|
|
# we need to check custom adapters to update the custom port names
|
|
self.custom_adapters = compute_properties["custom_adapters"]
|
|
self._list_ports()
|
|
if update_compute:
|
|
data = self._node_data(properties=compute_properties)
|
|
try:
|
|
response = await self.put(None, data=data)
|
|
except ComputeConflictError:
|
|
if old_name != self.name:
|
|
# special case when the new name is already updated on controller but refused by the compute
|
|
self.name = old_name
|
|
raise
|
|
await self.parse_node_response(response.json)
|
|
elif old_json != self.asdict():
|
|
# We send notif only if object has changed
|
|
self.project.emit_notification("node.updated", self.asdict())
|
|
self.project.dump()
|
|
|
|
async def parse_node_response(self, response):
|
|
"""
|
|
Update the object with the remote node object
|
|
"""
|
|
|
|
for key, value in response.items():
|
|
if key == "console":
|
|
self._console = value
|
|
elif key == "aux":
|
|
self._aux = value
|
|
elif key == "node_directory":
|
|
self._node_directory = value
|
|
elif key == "command_line":
|
|
self._command_line = value
|
|
elif key == "status":
|
|
self._status = value
|
|
elif key == "console_type":
|
|
self._console_type = value
|
|
elif key == "aux_type":
|
|
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",
|
|
"custom_adapters"
|
|
]:
|
|
if key in self._properties:
|
|
del self._properties[key]
|
|
else:
|
|
self._properties[key] = value
|
|
self._list_ports()
|
|
for link in self._links:
|
|
await link.node_updated(self)
|
|
|
|
def _node_data(self, properties=None):
|
|
"""
|
|
Prepare node data to send to the remote controller
|
|
|
|
:param properties: If properties is None use actual property otherwise use the parameter
|
|
"""
|
|
if properties:
|
|
data = copy.copy(properties)
|
|
else:
|
|
data = copy.copy(self._properties)
|
|
# We replace the startup script name by the content of the file
|
|
mapping = {
|
|
"base_script_file": "startup_script",
|
|
"startup_config": "startup_config_content",
|
|
"private_config": "private_config_content",
|
|
}
|
|
for k, v in mapping.items():
|
|
if k in list(self._properties.keys()):
|
|
data[v] = self._base_config_file_content(self._properties[k])
|
|
del data[k]
|
|
del self._properties[k] # We send the file only one time
|
|
data["name"] = self._name
|
|
|
|
# For remote computes, convert absolute image paths to relative paths
|
|
# The remote compute will search for the image in its own configured directories
|
|
if self._compute.id != "local":
|
|
# Image path fields for various node types
|
|
image_path_fields = {
|
|
# Common fields (IOU, Docker, etc.)
|
|
"path", "image",
|
|
# QEMU-specific fields
|
|
"hda_disk_image", "hdb_disk_image", "hdc_disk_image", "hdd_disk_image",
|
|
"cdrom_image", "bios_image", "initrd", "kernel_image",
|
|
# VMware-specific fields
|
|
"vmx_path",
|
|
}
|
|
|
|
for field in image_path_fields:
|
|
if field in data and data[field] and os.path.isabs(data[field]):
|
|
data[field] = os.path.basename(data[field])
|
|
|
|
if self._console:
|
|
# console is optional for builtin nodes
|
|
data["console"] = self._console
|
|
if self._console_type and self._node_type not in (BUILTIN_NODE_TYPES - {"ethernet_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 BUILTIN_NODE_TYPES:
|
|
# aux_type is not supported by all builtin nodes
|
|
data["aux_type"] = self._aux_type
|
|
if self.custom_adapters:
|
|
data["custom_adapters"] = self.custom_adapters
|
|
|
|
# None properties are not be sent because it can mean the emulator doesn't support it
|
|
for key, value in list(data.items()):
|
|
if value is None or value == {} or key in self.CONTROLLER_ONLY_PROPERTIES:
|
|
del data[key]
|
|
|
|
return data
|
|
|
|
async def destroy(self):
|
|
await self.delete()
|
|
|
|
async def start(self, data=None):
|
|
"""
|
|
Start a node
|
|
"""
|
|
try:
|
|
# For IOU: we need to send the licence everytime we start a 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)
|
|
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:
|
|
raise ControllerTimeoutError(f"Timeout when starting {self._name}")
|
|
|
|
async def stop(self):
|
|
"""
|
|
Stop a node
|
|
"""
|
|
try:
|
|
await self.post("/stop", timeout=240, dont_connect=True)
|
|
# We don't care if a node is down at this step
|
|
except (ComputeError, ControllerError):
|
|
pass
|
|
except asyncio.TimeoutError:
|
|
raise ControllerTimeoutError(f"Timeout when stopping {self._name}")
|
|
|
|
async def suspend(self):
|
|
"""
|
|
Suspend a node
|
|
"""
|
|
try:
|
|
await self.post("/suspend", timeout=240)
|
|
except asyncio.TimeoutError:
|
|
raise ControllerTimeoutError(f"Timeout when reloading {self._name}")
|
|
|
|
async def reload(self):
|
|
"""
|
|
Suspend a node
|
|
"""
|
|
try:
|
|
await self.post("/reload", timeout=240)
|
|
except asyncio.TimeoutError:
|
|
raise ControllerTimeoutError(f"Timeout when reloading {self._name}")
|
|
|
|
async def reset_console(self):
|
|
"""
|
|
Reset the console
|
|
"""
|
|
|
|
if self._console and self._console_type in ("telnet", "ssh"):
|
|
try:
|
|
await self.post("/console/reset", timeout=240)
|
|
except asyncio.TimeoutError:
|
|
raise ControllerTimeoutError(f"Timeout when reset console {self._name}")
|
|
|
|
async def get(self, path="", **kwargs):
|
|
"""
|
|
HTTP get on the node
|
|
"""
|
|
return await self._compute.get(
|
|
f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}{path}", **kwargs
|
|
)
|
|
|
|
async def post(self, path, data=None, **kwargs):
|
|
"""
|
|
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
|
|
)
|
|
else:
|
|
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):
|
|
"""
|
|
HTTP put on the node
|
|
"""
|
|
if path is None:
|
|
path = f"/projects/{self._project.id}/{self._node_type}/nodes/{self._id}"
|
|
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)
|
|
else:
|
|
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
|
|
)
|
|
else:
|
|
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):
|
|
"""
|
|
Search an image on local computer and upload it to remote compute
|
|
if the image exists
|
|
"""
|
|
|
|
for directory in images_directories(type):
|
|
image = os.path.join(directory, img)
|
|
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
|
|
)
|
|
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}"})
|
|
return True
|
|
return False
|
|
|
|
async def dynamips_auto_idlepc(self):
|
|
"""
|
|
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
|
|
|
|
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
|
|
|
|
def get_port(self, adapter_number, port_number):
|
|
"""
|
|
Return the port for this adapter_number and port_number
|
|
or returns None if the port is not found
|
|
"""
|
|
for port in self.ports:
|
|
if port.adapter_number == adapter_number and port.port_number == port_number:
|
|
return port
|
|
return None
|
|
|
|
def _list_ports(self):
|
|
"""
|
|
Generate the list of port display in the client
|
|
if the compute has sent a list we return it (use by
|
|
node where you can not personalize the port naming).
|
|
"""
|
|
self._ports = []
|
|
# Some special cases
|
|
if self._node_type == "atm_switch":
|
|
atm_port = set()
|
|
# Mapping is like {"1:0:100": "10:0:200"}
|
|
for source, dest in self._properties["mappings"].items():
|
|
atm_port.add(int(source.split(":")[0]))
|
|
atm_port.add(int(dest.split(":")[0]))
|
|
atm_port = sorted(atm_port)
|
|
for port in atm_port:
|
|
self._ports.append(PortFactory(f"{port}", 0, 0, port, "atm"))
|
|
return
|
|
|
|
elif self._node_type == "frame_relay_switch":
|
|
frame_relay_port = set()
|
|
# Mapping is like {"1:101": "10:202"}
|
|
for source, dest in self._properties["mappings"].items():
|
|
frame_relay_port.add(int(source.split(":")[0]))
|
|
frame_relay_port.add(int(dest.split(":")[0]))
|
|
frame_relay_port = sorted(frame_relay_port)
|
|
for port in frame_relay_port:
|
|
self._ports.append(PortFactory(f"{port}", 0, 0, port, "frame_relay"))
|
|
return
|
|
elif self._node_type == "dynamips":
|
|
self._ports = DynamipsPortFactory(self._properties)
|
|
return
|
|
elif self._node_type == "docker":
|
|
for adapter_number in range(0, self._properties["adapters"]):
|
|
custom_adapter_settings = {}
|
|
if self.custom_adapters:
|
|
for custom_adapter in self.custom_adapters:
|
|
if custom_adapter["adapter_number"] == adapter_number:
|
|
custom_adapter_settings = custom_adapter
|
|
break
|
|
port_name = f"eth{adapter_number}"
|
|
port_name = custom_adapter_settings.get("port_name", port_name)
|
|
mac_address = custom_adapter_settings.get("mac_address")
|
|
if not mac_address and "mac_address" in self._properties:
|
|
mac_address = int_to_macaddress(macaddress_to_int(self._properties["mac_address"]) + adapter_number)
|
|
|
|
port = PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name=port_name)
|
|
port.mac_address = mac_address
|
|
self._ports.append(port)
|
|
elif self._node_type in ("ethernet_switch", "ethernet_hub"):
|
|
# 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}")
|
|
)
|
|
port_number += 1
|
|
elif self._node_type in ("vpcs"):
|
|
self._ports.append(PortFactory("Ethernet0", 0, 0, 0, "ethernet", short_name="e0"))
|
|
elif self._node_type in ("cloud", "nat"):
|
|
port_number = 0
|
|
for port in self._properties.get("ports_mapping", []):
|
|
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,
|
|
)
|
|
|
|
def __repr__(self):
|
|
return f"<gns3server.controller.Node {self._node_type} {self._name}>"
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Node):
|
|
return False
|
|
return self.id == other.id and other.project.id == self.project.id
|
|
|
|
def asdict(self, topology_dump=False):
|
|
"""
|
|
:param topology_dump: Filter to keep only properties required for saving on disk
|
|
"""
|
|
|
|
topology = {
|
|
"compute_id": str(self._compute.id),
|
|
"node_id": self._id,
|
|
"node_type": self._node_type,
|
|
"template_id": self._template_id,
|
|
"name": self._name,
|
|
"console": self._console,
|
|
"console_type": self._console_type,
|
|
"console_auto_start": self._console_auto_start,
|
|
"aux": self._aux,
|
|
"aux_type": self._aux_type,
|
|
"properties": self._properties,
|
|
"label": self._label,
|
|
"x": self._x,
|
|
"y": self._y,
|
|
"z": self._z,
|
|
"locked": self._locked,
|
|
"width": self._width,
|
|
"height": self._height,
|
|
"symbol": self._symbol,
|
|
"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,
|
|
"tags": self._tags,
|
|
}
|
|
|
|
if topology_dump:
|
|
return topology
|
|
|
|
additional_data = {
|
|
"project_id": self._project.id,
|
|
"command_line": self._command_line,
|
|
"status": self._status,
|
|
"console_host": str(self._compute.console_host),
|
|
"node_directory": self._node_directory,
|
|
"ports": [port.asdict() for port in self.ports]
|
|
}
|
|
topology.update(additional_data)
|
|
return topology
|