Possibility to customize port names and adapter types for Qemu, VirtualBox, VMware and Docker. Fixes #2361.

MAC addresses can customized for Qemu as well.
This commit is contained in:
grossmj 2018-04-02 22:27:12 +07:00
parent 509b171b06
commit 757c103c03
14 changed files with 162 additions and 27 deletions

View File

@ -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 {}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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 "<gns3server.controller.Node {} {}>".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]
}

View File

@ -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:

View File

@ -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",

View File

@ -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 <http://www.gnu.org/licenses/>.
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"]
},
}

View File

@ -16,6 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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,
}

View File

@ -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",

View File

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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,
}

View File

@ -16,6 +16,9 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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,
}

View File

@ -15,6 +15,8 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
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
}

View File

@ -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": []
}