From 757c103c03f1a848b9aeeded2ba47d0b16ce2b7d Mon Sep 17 00:00:00 2001 From: grossmj Date: Mon, 2 Apr 2018 22:27:12 +0700 Subject: [PATCH] Possibility to customize port names and adapter types for Qemu, VirtualBox, VMware and Docker. Fixes #2361. MAC addresses can customized for Qemu as well. --- gns3server/compute/base_node.py | 16 ++++++ gns3server/compute/qemu/qemu_vm.py | 13 +++-- .../compute/virtualbox/virtualbox_vm.py | 16 +++--- gns3server/compute/vmware/vmware_vm.py | 11 +++-- gns3server/controller/node.py | 33 +++++++++++-- gns3server/controller/ports/port_factory.py | 13 ++++- .../handlers/api/compute/docker_handler.py | 2 +- gns3server/schemas/custom_adapters.py | 49 +++++++++++++++++++ gns3server/schemas/docker.py | 7 ++- gns3server/schemas/node.py | 2 + gns3server/schemas/qemu.py | 8 ++- gns3server/schemas/virtualbox.py | 7 ++- gns3server/schemas/vmware.py | 8 ++- tests/controller/test_node.py | 4 +- 14 files changed, 162 insertions(+), 27 deletions(-) create mode 100644 gns3server/schemas/custom_adapters.py diff --git a/gns3server/compute/base_node.py b/gns3server/compute/base_node.py index 62e02a25..2fd7802a 100644 --- a/gns3server/compute/base_node.py +++ b/gns3server/compute/base_node.py @@ -77,6 +77,7 @@ class BaseNode: self._wrap_console = wrap_console self._wrapper_telnet_server = None self._internal_console_port = None + self._custom_adapters = [] if self._console is not None: if console_type == "vnc": @@ -123,6 +124,14 @@ class BaseNode: def linked_clone(self, val): self._linked_clone = val + @property + def custom_adapters(self): + return self._custom_adapters + + @custom_adapters.setter + def custom_adapters(self, val): + self._custom_adapters = val + @property def status(self): """ @@ -760,3 +769,10 @@ class BaseNode: percentage_left, platform.node()) self.project.emit("log.warning", {"message": message}) + + def _get_custom_adapter_settings(self, adapter_number): + + for custom_adapter in self.custom_adapters: + if custom_adapter["adapter_number"] == adapter_number: + return custom_adapter + return {} diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 1eec8f38..834d60d1 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1696,10 +1696,17 @@ class QemuVM(BaseNode): if adapter_number not in self._local_udp_tunnels: self._local_udp_tunnels[adapter_number] = self._create_local_udp_tunnel() nio = self._local_udp_tunnels[adapter_number][0] + + custom_adapter = self._get_custom_adapter_settings(adapter_number) + adapter_type = custom_adapter.get("adapter_type", self._adapter_type) + custom_mac_address = custom_adapter.get("mac_address") + if custom_mac_address: + mac = int_to_macaddress(macaddress_to_int(custom_mac_address)) + if self._legacy_networking: # legacy QEMU networking syntax (-net) if nio: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)]) + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, adapter_type)]) if isinstance(nio, NIOUDP): if patched_qemu: # use patched Qemu syntax @@ -1719,11 +1726,11 @@ class QemuVM(BaseNode): elif isinstance(nio, NIOTAP): network_options.extend(["-net", "tap,name=gns3-{},ifname={}".format(adapter_number, nio.tap_device)]) else: - network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, self._adapter_type)]) + network_options.extend(["-net", "nic,vlan={},macaddr={},model={}".format(adapter_number, mac, adapter_type)]) else: # newer QEMU networking syntax - device_string = "{},mac={}".format(self._adapter_type, mac) + device_string = "{},mac={}".format(adapter_type, mac) bridge_id = math.floor(pci_device_id / 32) if bridge_id > 0: addr = pci_device_id % 32 diff --git a/gns3server/compute/virtualbox/virtualbox_vm.py b/gns3server/compute/virtualbox/virtualbox_vm.py index 4faf559a..5d1a42f8 100644 --- a/gns3server/compute/virtualbox/virtualbox_vm.py +++ b/gns3server/compute/virtualbox/virtualbox_vm.py @@ -852,18 +852,22 @@ class VirtualBoxVM(BaseNode): continue yield from self._modify_vm("--nictrace{} off".format(adapter_number + 1)) + + custom_adapter = self._get_custom_adapter_settings(adapter_number) + adapter_type = custom_adapter.get("adapter_type", self._adapter_type) + vbox_adapter_type = "82540EM" - if self._adapter_type == "PCnet-PCI II (Am79C970A)": + if adapter_type == "PCnet-PCI II (Am79C970A)": vbox_adapter_type = "Am79C970A" - if self._adapter_type == "PCNet-FAST III (Am79C973)": + if adapter_type == "PCNet-FAST III (Am79C973)": vbox_adapter_type = "Am79C973" - if self._adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": + if adapter_type == "Intel PRO/1000 MT Desktop (82540EM)": vbox_adapter_type = "82540EM" - if self._adapter_type == "Intel PRO/1000 T Server (82543GC)": + if adapter_type == "Intel PRO/1000 T Server (82543GC)": vbox_adapter_type = "82543GC" - if self._adapter_type == "Intel PRO/1000 MT Server (82545EM)": + if adapter_type == "Intel PRO/1000 MT Server (82545EM)": vbox_adapter_type = "82545EM" - if self._adapter_type == "Paravirtualized Network (virtio-net)": + if adapter_type == "Paravirtualized Network (virtio-net)": vbox_adapter_type = "virtio" args = [self._vmname, "--nictype{}".format(adapter_number + 1), vbox_adapter_type] yield from self.manager.execute("modifyvm", args) diff --git a/gns3server/compute/vmware/vmware_vm.py b/gns3server/compute/vmware/vmware_vm.py index 4fc78e0b..a8d4a5a0 100644 --- a/gns3server/compute/vmware/vmware_vm.py +++ b/gns3server/compute/vmware/vmware_vm.py @@ -258,17 +258,20 @@ class VMwareVM(BaseNode): self.manager.refresh_vmnet_list() for adapter_number in range(0, self._adapters): + custom_adapter = self._get_custom_adapter_settings(adapter_number) + adapter_type = custom_adapter.get("adapter_type", self._adapter_type) + # add/update the interface - if self._adapter_type == "default": + if adapter_type == "default": # force default to e1000 because some guest OS don't detect the adapter (i.e. Windows 2012 server) # when 'virtualdev' is not set in the VMX file. - adapter_type = "e1000" + vmware_adapter_type = "e1000" else: - adapter_type = self._adapter_type + vmware_adapter_type = adapter_type ethernet_adapter = {"ethernet{}.present".format(adapter_number): "TRUE", "ethernet{}.addresstype".format(adapter_number): "generated", "ethernet{}.generatedaddressoffset".format(adapter_number): "0", - "ethernet{}.virtualdev".format(adapter_number): adapter_type} + "ethernet{}.virtualdev".format(adapter_number): vmware_adapter_type} self._vmx_pairs.update(ethernet_adapter) connection_type = "ethernet{}.connectiontype".format(adapter_number) diff --git a/gns3server/controller/node.py b/gns3server/controller/node.py index 441a17e8..2baaf6e2 100644 --- a/gns3server/controller/node.py +++ b/gns3server/controller/node.py @@ -74,6 +74,7 @@ class Node: self._z = 0 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 @@ -305,6 +306,14 @@ class Node: 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 @@ -330,6 +339,7 @@ class Node: data["node_id"] = self._id if self._node_type == "docker": timeout = None + else: timeout = 1200 trial = 0 @@ -374,6 +384,9 @@ class Node: else: 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) @@ -442,6 +455,8 @@ class Node: data["console"] = self._console if self._console_type: data["console_type"] = self._console_type + if self.custom_adapters: + data["custom_adapters"] = self.custom_adapters # None properties are not be send. Because it can mean the emulator doesn't support it for key in list(data.keys()): @@ -585,7 +600,7 @@ class Node: """ 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 personnalize the port naming). + node where you can not personalize the port naming). """ self._ports = [] # Some special cases @@ -615,7 +630,14 @@ class Node: return elif self._node_type == "docker": for adapter_number in range(0, self._properties["adapters"]): - self._ports.append(PortFactory("eth{}".format(adapter_number), 0, adapter_number, 0, "ethernet", short_name="eth{}".format(adapter_number))) + custom_adapter_settings = {} + for custom_adapter in self.custom_adapters: + if custom_adapter["adapter_number"] == adapter_number: + custom_adapter_settings = custom_adapter + break + port_name = "eth{}".format(adapter_number) + port_name = custom_adapter_settings.get("port_name", port_name) + self._ports.append(PortFactory(port_name, 0, adapter_number, 0, "ethernet", short_name="eth{}".format(adapter_number))) elif self._node_type in ("ethernet_switch", "ethernet_hub"): # Basic node we don't want to have adapter number port_number = 0 @@ -630,7 +652,7 @@ 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._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 "".format(self._node_type, self._name) @@ -644,6 +666,7 @@ class Node: """ :param topology_dump: Filter to keep only properties require for saving on disk """ + if topology_dump: return { "compute_id": str(self._compute.id), @@ -662,7 +685,8 @@ class Node: "symbol": self._symbol, "port_name_format": self._port_name_format, "port_segment_size": self._port_segment_size, - "first_port_name": self._first_port_name + "first_port_name": self._first_port_name, + "custom_adapters": self._custom_adapters } return { "compute_id": str(self._compute.id), @@ -687,5 +711,6 @@ 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, "ports": [port.__json__() for port in self.ports] } diff --git a/gns3server/controller/ports/port_factory.py b/gns3server/controller/ports/port_factory.py index a46d0f95..c6f5ba6b 100644 --- a/gns3server/controller/ports/port_factory.py +++ b/gns3server/controller/ports/port_factory.py @@ -51,7 +51,7 @@ class StandardPortFactory: """ Create ports for standard device """ - def __new__(cls, properties, port_by_adapter, first_port_name, port_name_format, port_segment_size): + 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 @@ -61,9 +61,16 @@ class StandardPortFactory: ethernet_adapters = properties.get("adapters", 1) for adapter_number in range(adapter_number, ethernet_adapters + adapter_number): + + custom_adapter_settings = {} + for custom_adapter in custom_adapters: + if custom_adapter["adapter_number"] == adapter_number: + custom_adapter_settings = custom_adapter + break + for port_number in range(0, port_by_adapter): if first_port_name and adapter_number == 0: - port_name = first_port_name + port_name = custom_adapter_settings.get("port_name", first_port_name) port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") else: try: @@ -74,6 +81,8 @@ class StandardPortFactory: **cls._generate_replacement(interface_number, segment_number)) except (ValueError, KeyError) as e: raise aiohttp.web.HTTPConflict(text="Invalid port name format {}: {}".format(port_name_format, str(e))) + + port_name = custom_adapter_settings.get("port_name", port_name) port = PortFactory(port_name, segment_number, adapter_number, port_number, "ethernet") interface_number += 1 if port_segment_size: diff --git a/gns3server/handlers/api/compute/docker_handler.py b/gns3server/handlers/api/compute/docker_handler.py index 139cd241..f1ebaf6a 100644 --- a/gns3server/handlers/api/compute/docker_handler.py +++ b/gns3server/handlers/api/compute/docker_handler.py @@ -221,7 +221,7 @@ class DockerHandler: "project_id": "Project UUID", "node_id": "Node UUID", "adapter_number": "Adapter where the nio should be added", - "port_number": "Port on the adapter" + "port_number": "Port on the adapter (always 0)" }, status_codes={ 201: "NIO created", diff --git a/gns3server/schemas/custom_adapters.py b/gns3server/schemas/custom_adapters.py new file mode 100644 index 00000000..d24b9db1 --- /dev/null +++ b/gns3server/schemas/custom_adapters.py @@ -0,0 +1,49 @@ +#!/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 . + +CUSTOM_ADAPTERS_ARRAY_SCHEMA = { + "type": "array", + "items": { + "type": "object", + "description": "Custom properties", + "properties": { + "adapter_number": { + "type": "integer", + "description": "Adapter number" + }, + "port_name": { + "type": "string", + "description": "Custom port name", + "minLength": 1, + }, + "adapter_type": { + "type": "string", + "description": "Custom adapter type", + "minLength": 1, + }, + "mac_address": { + "description": "Custom MAC address", + "type": "string", + "minLength": 1, + "pattern": "^([0-9a-fA-F]{2}[:]){5}([0-9a-fA-F]{2})$" + }, + }, + "additionalProperties": False, + "required": ["adapter_number"] + }, +} + diff --git a/gns3server/schemas/docker.py b/gns3server/schemas/docker.py index bbf247ab..6a21a803 100644 --- a/gns3server/schemas/docker.py +++ b/gns3server/schemas/docker.py @@ -16,6 +16,9 @@ # along with this program. If not, see . +from .custom_adapters import CUSTOM_ADAPTERS_ARRAY_SCHEMA + + DOCKER_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to create a new Docker container", @@ -93,7 +96,8 @@ DOCKER_CREATE_SCHEMA = { "minLength": 12, "maxLength": 64, "pattern": "^[a-f0-9]+$" - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA # not used at this time }, "additionalProperties": False, "required": ["name", "image"] @@ -192,6 +196,7 @@ DOCKER_OBJECT_SCHEMA = { "description": "VM status Read only", "enum": ["started", "stopped", "suspended"] }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA # not used at this time }, "additionalProperties": False, } diff --git a/gns3server/schemas/node.py b/gns3server/schemas/node.py index 9ca659dd..c5f355ce 100644 --- a/gns3server/schemas/node.py +++ b/gns3server/schemas/node.py @@ -17,6 +17,7 @@ import copy from .label import LABEL_OBJECT_SCHEMA +from .custom_adapters import CUSTOM_ADAPTERS_ARRAY_SCHEMA NODE_TYPE_SCHEMA = { "description": "Type of node", @@ -194,6 +195,7 @@ NODE_OBJECT_SCHEMA = { "description": "Name of the first port", "type": ["string", "null"], }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA, "ports": { "description": "List of node ports READ only", "type": "array", diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index b49f88d3..8da99160 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .custom_adapters import CUSTOM_ADAPTERS_ARRAY_SCHEMA + QEMU_PLATFORMS = ["aarch64", "alpha", "arm", "cris", "i386", "lm32", "m68k", "microblaze", "microblazeel", "mips", "mips64", "mips64el", "mipsel", "moxie", "or32", "ppc", "ppc64", "ppcemb", "s390x", "sh4", "sh4eb", "sparc", "sparc64", "tricore", "unicore32", "x86_64", "xtensa", "xtensaeb"] @@ -207,7 +209,8 @@ QEMU_CREATE_SCHEMA = { "options": { "description": "Additional QEMU options", "type": ["string", "null"], - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False, "required": ["name"], @@ -392,7 +395,8 @@ QEMU_UPDATE_SCHEMA = { "options": { "description": "Additional QEMU options", "type": ["string", "null"], - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False, } diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 5189b95b..5c0a884a 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -16,6 +16,9 @@ # along with this program. If not, see . +from .custom_adapters import CUSTOM_ADAPTERS_ARRAY_SCHEMA + + VBOX_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to create a new VirtualBox VM instance", @@ -84,6 +87,7 @@ VBOX_CREATE_SCHEMA = { "description": "Action to execute on the VM is closed", "enum": ["power_off", "shutdown_signal", "save_vm_state"], }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False, "required": ["name", "vmname"], @@ -169,7 +173,8 @@ VBOX_OBJECT_SCHEMA = { "linked_clone": { "description": "Whether the VM is a linked clone or not", "type": "boolean" - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False, } diff --git a/gns3server/schemas/vmware.py b/gns3server/schemas/vmware.py index a878f2e7..05c8ad0d 100644 --- a/gns3server/schemas/vmware.py +++ b/gns3server/schemas/vmware.py @@ -15,6 +15,8 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . +from .custom_adapters import CUSTOM_ADAPTERS_ARRAY_SCHEMA + VMWARE_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", @@ -74,7 +76,8 @@ VMWARE_CREATE_SCHEMA = { "use_any_adapter": { "description": "Allow GNS3 to use any VMware adapter", "type": "boolean", - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False, "required": ["name", "vmx_path", "linked_clone"], @@ -154,7 +157,8 @@ VMWARE_OBJECT_SCHEMA = { "linked_clone": { "description": "Whether the VM is a linked clone or not", "type": "boolean" - } + }, + "custom_adapters": CUSTOM_ADAPTERS_ARRAY_SCHEMA }, "additionalProperties": False } diff --git a/tests/controller/test_node.py b/tests/controller/test_node.py index 5d18861b..7f46a9f8 100644 --- a/tests/controller/test_node.py +++ b/tests/controller/test_node.py @@ -141,6 +141,7 @@ def test_json(node, compute): "port_name_format": "Ethernet{0}", "port_segment_size": 0, "first_port_name": None, + "custom_adapters": [], "ports": [ { "adapter_number": 0, @@ -169,7 +170,8 @@ def test_json(node, compute): "label": node.label, "port_name_format": "Ethernet{0}", "port_segment_size": 0, - "first_port_name": None + "first_port_name": None, + "custom_adapters": [] }