diff --git a/gns3server/modules/dynamips/dynamips_device.py b/gns3server/modules/dynamips/dynamips_device.py
new file mode 100644
index 00000000..bf5012d1
--- /dev/null
+++ b/gns3server/modules/dynamips/dynamips_device.py
@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 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 .
+
+
+from .dynamips_error import DynamipsError
+from .nodes.atm_switch import ATMSwitch
+from .nodes.ethernet_switch import EthernetSwitch
+from .nodes.ethernet_hub import EthernetHub
+from .nodes.frame_relay_switch import FrameRelaySwitch
+
+import logging
+log = logging.getLogger(__name__)
+
+DEVICES = {'atmsw': ATMSwitch,
+ 'frsw': FrameRelaySwitch,
+ 'ethsw': EthernetSwitch,
+ 'ethhub': EthernetHub}
+
+
+class DynamipsDevice:
+
+ """
+ Factory to create an Device object based on the type
+ """
+
+ def __new__(cls, name, vm_id, project, manager, device_type, **kwargs):
+
+ if type not in DEVICES:
+ raise DynamipsError("Unknown device type: {}".format(device_type))
+
+ return DEVICES[device_type](name, vm_id, project, manager, **kwargs)
diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py
new file mode 100644
index 00000000..b2186484
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/atm_switch.py
@@ -0,0 +1,348 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 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 .
+
+"""
+Interface for Dynamips virtual ATM switch module ("atmsw").
+http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L593
+"""
+
+import asyncio
+
+from .device import Device
+from ..dynamips_error import DynamipsError
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class ATMSwitch(Device):
+ """
+ Dynamips ATM switch.
+
+ :param name: name for this switch
+ :param node_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ super().__init__(name, node_id, project, manager)
+ self._nios = {}
+ self._mapping = {}
+
+ @asyncio.coroutine
+ def create(self):
+
+ if self._hypervisor is None:
+ self._hypervisor = yield from self.manager.start_new_hypervisor()
+
+ yield from self._hypervisor.send('atmsw create "{}"'.format(self._name))
+ log.info('ATM switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.append(self)
+
+ @asyncio.coroutine
+ def set_name(self, new_name):
+ """
+ Renames this ATM switch.
+
+ :param new_name: New name for this switch
+ """
+
+ yield from self._hypervisor.send('atm rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
+ log.info('ATM switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+ self._name = new_name
+
+
+ @property
+ def nios(self):
+ """
+ Returns all the NIOs member of this ATM switch.
+
+ :returns: nio list
+ """
+
+ return self._nios
+
+ @property
+ def mapping(self):
+ """
+ Returns port mapping
+
+ :returns: mapping list
+ """
+
+ return self._mapping
+
+ @asyncio.coroutine
+ def delete(self):
+ """
+ Deletes this ATM switch.
+ """
+
+ yield from self._hypervisor.send('atmsw delete "{}"'.format(self._name))
+ log.info('ATM switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.remove(self)
+ self._instances.remove(self._id)
+
+ def has_port(self, port):
+ """
+ Checks if a port exists on this ATM switch.
+
+ :returns: boolean
+ """
+
+ if port in self._nios:
+ return True
+ return False
+
+ def add_nio(self, nio, port_number):
+ """
+ Adds a NIO as new port on ATM switch.
+
+ :param nio: NIO instance to add
+ :param port_number: port to allocate for the NIO
+ """
+
+ if port_number in self._nios:
+ raise DynamipsError("Port {} isn't free".format(port_number))
+
+ log.info('ATM switch "{name}" [id={id}]: NIO {nio} bound to port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ self._nios[port_number] = nio
+
+ def remove_nio(self, port_number):
+ """
+ Removes the specified NIO as member of this ATM switch.
+
+ :param port_number: allocated port number
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ log.info('ATM switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ del self._nios[port_number]
+ return nio
+
+ @asyncio.coroutine
+ def map_vp(self, port1, vpi1, port2, vpi2):
+ """
+ Creates a new Virtual Path connection.
+
+ :param port1: input port
+ :param vpi1: input vpi
+ :param port2: output port
+ :param vpi2: output vpi
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('atmsw create_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name,
+ input_nio=nio1,
+ input_vpi=vpi1,
+ output_nio=nio2,
+ output_vpi=vpi2))
+
+ log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} created'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ vpi1=vpi1,
+ port2=port2,
+ vpi2=vpi2))
+
+ self._mapping[(port1, vpi1)] = (port2, vpi2)
+
+ @asyncio.coroutine
+ def unmap_vp(self, port1, vpi1, port2, vpi2):
+ """
+ Deletes a new Virtual Path connection.
+
+ :param port1: input port
+ :param vpi1: input vpi
+ :param port2: output port
+ :param vpi2: output vpi
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('atmsw delete_vpc "{name}" {input_nio} {input_vpi} {output_nio} {output_vpi}'.format(name=self._name,
+ input_nio=nio1,
+ input_vpi=vpi1,
+ output_nio=nio2,
+ output_vpi=vpi2))
+
+ log.info('ATM switch "{name}" [{id}]: VPC from port {port1} VPI {vpi1} to port {port2} VPI {vpi2} deleted'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ vpi1=vpi1,
+ port2=port2,
+ vpi2=vpi2))
+
+ del self._mapping[(port1, vpi1)]
+
+ @asyncio.coroutine
+ def map_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2):
+ """
+ Creates a new Virtual Channel connection (unidirectional).
+
+ :param port1: input port
+ :param vpi1: input vpi
+ :param vci1: input vci
+ :param port2: output port
+ :param vpi2: output vpi
+ :param vci2: output vci
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('atmsw create_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name,
+ input_nio=nio1,
+ input_vpi=vpi1,
+ input_vci=vci1,
+ output_nio=nio2,
+ output_vpi=vpi2,
+ output_vci=vci2))
+
+ log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} created'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ vpi1=vpi1,
+ vci1=vci1,
+ port2=port2,
+ vpi2=vpi2,
+ vci2=vci2))
+
+ self._mapping[(port1, vpi1, vci1)] = (port2, vpi2, vci2)
+
+ @asyncio.coroutine
+ def unmap_pvc(self, port1, vpi1, vci1, port2, vpi2, vci2):
+ """
+ Deletes a new Virtual Channel connection (unidirectional).
+
+ :param port1: input port
+ :param vpi1: input vpi
+ :param vci1: input vci
+ :param port2: output port
+ :param vpi2: output vpi
+ :param vci2: output vci
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('atmsw delete_vcc "{name}" {input_nio} {input_vpi} {input_vci} {output_nio} {output_vpi} {output_vci}'.format(name=self._name,
+ input_nio=nio1,
+ input_vpi=vpi1,
+ input_vci=vci1,
+ output_nio=nio2,
+ output_vpi=vpi2,
+ output_vci=vci2))
+
+ log.info('ATM switch "{name}" [{id}]: VCC from port {port1} VPI {vpi1} VCI {vci1} to port {port2} VPI {vpi2} VCI {vci2} deleted'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ vpi1=vpi1,
+ vci1=vci1,
+ port2=port2,
+ vpi2=vpi2,
+ vci2=vci2))
+ del self._mapping[(port1, vpi1, vci1)]
+
+ @asyncio.coroutine
+ def start_capture(self, port_number, output_file, data_link_type="DLT_ATM_RFC1483"):
+ """
+ Starts a packet capture.
+
+ :param port_number: allocated port number
+ :param output_file: PCAP destination file for the capture
+ :param data_link_type: PCAP data link type (DLT_*), default is DLT_ATM_RFC1483
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+
+ data_link_type = data_link_type.lower()
+ if data_link_type.startswith("dlt_"):
+ data_link_type = data_link_type[4:]
+
+ if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
+ raise DynamipsError("Port {} has already a filter applied".format(port_number))
+
+ yield from nio.bind_filter("both", "capture")
+ yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
+
+ log.info('ATM switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
+
+ @asyncio.coroutine
+ def stop_capture(self, port_number):
+ """
+ Stops a packet capture.
+
+ :param port_number: allocated port number
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from nio.unbind_filter("both")
+ log.info('ATM switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
diff --git a/gns3server/modules/dynamips/nodes/bridge.py b/gns3server/modules/dynamips/nodes/bridge.py
new file mode 100644
index 00000000..0f00137c
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/bridge.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 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 .
+
+"""
+Interface for Dynamips NIO bridge module ("nio_bridge").
+http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L538
+"""
+
+import asyncio
+from .device import Device
+
+
+class Bridge(Device):
+ """
+ Dynamips bridge.
+
+ :param name: name for this bridge
+ :param node_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ super().__init__(name, node_id, project, manager, hypervisor)
+ self._nios = []
+
+ @asyncio.coroutine
+ def create(self):
+
+ if self._hypervisor is None:
+ self._hypervisor = yield from self.manager.start_new_hypervisor()
+
+ yield from self._hypervisor.send('nio_bridge create "{}"'.format(self._name))
+ self._hypervisor.devices.append(self)
+
+ @asyncio.coroutine
+ def set_name(self, new_name):
+ """
+ Renames this bridge.
+
+ :param new_name: New name for this bridge
+ """
+
+ yield from self._hypervisor.send('nio_bridge rename "{name}" "{new_name}"'.format(name=self._name,
+ new_name=new_name))
+
+ self._name = new_name
+
+ @property
+ def nios(self):
+ """
+ Returns all the NIOs member of this bridge.
+
+ :returns: nio list
+ """
+
+ return self._nios
+
+ @asyncio.coroutine
+ def delete(self):
+ """
+ Deletes this bridge.
+ """
+
+ yield from self._hypervisor.send('nio_bridge delete "{}"'.format(self._name))
+ self._hypervisor.devices.remove(self)
+
+ @asyncio.coroutine
+ def add_nio(self, nio):
+ """
+ Adds a NIO as new port on this bridge.
+
+ :param nio: NIO instance to add
+ """
+
+ yield from self._hypervisor.send('nio_bridge add_nio "{name}" {nio}'.format(name=self._name, nio=nio))
+ self._nios.append(nio)
+
+ @asyncio.coroutine
+ def remove_nio(self, nio):
+ """
+ Removes the specified NIO as member of this bridge.
+
+ :param nio: NIO instance to remove
+ """
+
+ yield from self._hypervisor.send('nio_bridge remove_nio "{name}" {nio}'.format(name=self._name, nio=nio))
+ self._nios.remove(nio)
diff --git a/gns3server/modules/dynamips/nodes/device.py b/gns3server/modules/dynamips/nodes/device.py
new file mode 100644
index 00000000..f5b7ee75
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/device.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2015 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 .
+
+
+from ...base_vm import BaseVM
+
+
+class Device(BaseVM):
+ """
+ Base device for switches and hubs
+
+ :param name: name for this bridge
+ :param vm_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ super().__init__(name, node_id, project, manager)
+ self._hypervisor = hypervisor
+
+ @property
+ def hypervisor(self):
+ """
+ Returns the current hypervisor.
+
+ :returns: hypervisor instance
+ """
+
+ return self._hypervisor
+
+ def start(self):
+
+ pass # Dynamips switches and hubs are always on
+
+ def stop(self):
+
+ pass # Dynamips switches and hubs are always on
diff --git a/gns3server/modules/dynamips/nodes/ethernet_hub.py b/gns3server/modules/dynamips/nodes/ethernet_hub.py
new file mode 100644
index 00000000..5f7228c7
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/ethernet_hub.py
@@ -0,0 +1,161 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 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 .
+
+"""
+Hub object that uses the Bridge interface to create a hub with ports.
+"""
+
+import asyncio
+
+from .bridge import Bridge
+from ..dynamips_error import DynamipsError
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class EthernetHub(Bridge):
+ """
+ Dynamips Ethernet hub (based on Bridge)
+
+ :param name: name for this hub
+ :param node_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ Bridge.__init__(self, name, node_id, project, manager, hypervisor)
+ self._mapping = {}
+
+ @asyncio.coroutine
+ def create(self):
+
+ yield from Bridge.create()
+ log.info('Ethernet hub "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
+
+ @property
+ def mapping(self):
+ """
+ Returns port mapping
+
+ :returns: mapping list
+ """
+
+ return self._mapping
+
+ @asyncio.coroutine
+ def delete(self):
+ """
+ Deletes this hub.
+ """
+
+ yield from Bridge.delete(self)
+ log.info('Ethernet hub "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
+ self._instances.remove(self._id)
+
+ @asyncio.coroutine
+ def add_nio(self, nio, port_number):
+ """
+ Adds a NIO as new port on this hub.
+
+ :param nio: NIO instance to add
+ :param port_number: port to allocate for the NIO
+ """
+
+ if port_number in self._mapping:
+ raise DynamipsError("Port {} isn't free".format(port_number))
+
+ yield from Bridge.add_nio(self, nio)
+
+ log.info('Ethernet hub "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+ self._mapping[port_number] = nio
+
+ @asyncio.coroutine
+ def remove_nio(self, port_number):
+ """
+ Removes the specified NIO as member of this hub.
+
+ :param port_number: allocated port number
+
+ :returns: the NIO that was bound to the allocated port
+ """
+
+ if port_number not in self._mapping:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._mapping[port_number]
+ yield from Bridge.remove_nio(self, nio)
+
+ log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ del self._mapping[port_number]
+ return nio
+
+ @asyncio.coroutine
+ def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"):
+ """
+ Starts a packet capture.
+
+ :param port_number: allocated port number
+ :param output_file: PCAP destination file for the capture
+ :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
+ """
+
+ if port_number not in self._mapping:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._mapping[port_number]
+
+ data_link_type = data_link_type.lower()
+ if data_link_type.startswith("dlt_"):
+ data_link_type = data_link_type[4:]
+
+ if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
+ raise DynamipsError("Port {} has already a filter applied".format(port_number))
+
+ yield from nio.bind_filter("both", "capture")
+ yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
+
+ log.info('Ethernet hub "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
+
+ @asyncio.coroutine
+ def stop_capture(self, port_number):
+ """
+ Stops a packet capture.
+
+ :param port_number: allocated port number
+ """
+
+ if port_number not in self._mapping:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._mapping[port_number]
+ yield from nio.unbind_filter("both")
+ log.info('Ethernet hub "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
diff --git a/gns3server/modules/dynamips/nodes/ethernet_switch.py b/gns3server/modules/dynamips/nodes/ethernet_switch.py
new file mode 100644
index 00000000..24fe3020
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/ethernet_switch.py
@@ -0,0 +1,285 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 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 .
+
+"""
+Interface for Dynamips virtual Ethernet switch module ("ethsw").
+http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L558
+"""
+
+import asyncio
+
+from .device import Device
+from ..dynamips_error import DynamipsError
+
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class EthernetSwitch(Device):
+ """
+ Dynamips Ethernet switch.
+
+ :param name: name for this switch
+ :param node_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ super().__init__(name, node_id, project, manager, hypervisor)
+ self._nios = {}
+ self._mapping = {}
+
+ @asyncio.coroutine
+ def create(self):
+
+ if self._hypervisor is None:
+ self._hypervisor = yield from self.manager.start_new_hypervisor()
+
+ yield from self._hypervisor.send('ethsw create "{}"'.format(self._name))
+ log.info('Ethernet switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.append(self)
+
+ @asyncio.coroutine
+ def set_name(self, new_name):
+ """
+ Renames this Ethernet switch.
+
+ :param new_name: New name for this switch
+ """
+
+ yield from self._hypervisor.send('ethsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
+ log.info('Ethernet switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+ self._name = new_name
+
+ @property
+ def nios(self):
+ """
+ Returns all the NIOs member of this Ethernet switch.
+
+ :returns: nio list
+ """
+
+ return self._nios
+
+ @property
+ def mapping(self):
+ """
+ Returns port mapping
+
+ :returns: mapping list
+ """
+
+ return self._mapping
+
+ @asyncio.coroutine
+ def delete(self):
+ """
+ Deletes this Ethernet switch.
+ """
+
+ yield from self._hypervisor.send('ethsw delete "{}"'.format(self._name))
+ log.info('Ethernet switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.remove(self)
+ self._instances.remove(self._id)
+
+ @asyncio.coroutine
+ def add_nio(self, nio, port_number):
+ """
+ Adds a NIO as new port on Ethernet switch.
+
+ :param nio: NIO instance to add
+ :param port_number: port to allocate for the NIO
+ """
+
+ if port_number in self._nios:
+ raise DynamipsError("Port {} isn't free".format(port_number))
+
+ yield from self._hypervisor.send('ethsw add_nio "{name}" {nio}'.format(name=self._name, nio=nio))
+
+ log.info('Ethernet switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+ self._nios[port_number] = nio
+
+ @asyncio.coroutine
+ def remove_nio(self, port_number):
+ """
+ Removes the specified NIO as member of this Ethernet switch.
+
+ :param port_number: allocated port number
+
+ :returns: the NIO that was bound to the port
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from self._hypervisor.send('ethsw remove_nio "{name}" {nio}'.format(name=self._name, nio=nio))
+
+ log.info('Ethernet switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ del self._nios[port_number]
+ if port_number in self._mapping:
+ del self._mapping[port_number]
+
+ return nio
+
+ @asyncio.coroutine
+ def set_access_port(self, port_number, vlan_id):
+ """
+ Sets the specified port as an ACCESS port.
+
+ :param port_number: allocated port number
+ :param vlan_id: VLAN number membership
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from self._hypervisor.send('ethsw set_access_port "{name}" {nio} {vlan_id}'.format(name=self._name,
+ nio=nio,
+ vlan_id=vlan_id))
+
+ log.info('Ethernet switch "{name}" [{id}]: port {port} set as an access port in VLAN {vlan_id}'.format(name=self._name,
+ id=self._id,
+ port=port_number,
+ vlan_id=vlan_id))
+ self._mapping[port_number] = ("access", vlan_id)
+
+ @asyncio.coroutine
+ def set_dot1q_port(self, port_number, native_vlan):
+ """
+ Sets the specified port as a 802.1Q trunk port.
+
+ :param port_number: allocated port number
+ :param native_vlan: native VLAN for this trunk port
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from self._hypervisor.send('ethsw set_dot1q_port "{name}" {nio} {native_vlan}'.format(name=self._name,
+ nio=nio,
+ native_vlan=native_vlan))
+
+ log.info('Ethernet switch "{name}" [{id}]: port {port} set as a 802.1Q port with native VLAN {vlan_id}'.format(name=self._name,
+ id=self._id,
+ port=port_number,
+ vlan_id=native_vlan))
+
+ self._mapping[port_number] = ("dot1q", native_vlan)
+
+ @asyncio.coroutine
+ def set_qinq_port(self, port_number, outer_vlan):
+ """
+ Sets the specified port as a trunk (QinQ) port.
+
+ :param port_number: allocated port number
+ :param outer_vlan: outer VLAN (transport VLAN) for this QinQ port
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from self._hypervisor.send('ethsw set_qinq_port "{name}" {nio} {outer_vlan}'.format(name=self._name,
+ nio=nio,
+ outer_vlan=outer_vlan))
+
+ log.info('Ethernet switch "{name}" [{id}]: port {port} set as a QinQ port with outer VLAN {vlan_id}'.format(name=self._name,
+ id=self._id,
+ port=port_number,
+ vlan_id=outer_vlan))
+ self._mapping[port_number] = ("qinq", outer_vlan)
+
+ @asyncio.coroutine
+ def get_mac_addr_table(self):
+ """
+ Returns the MAC address table for this Ethernet switch.
+
+ :returns: list of entries (Ethernet address, VLAN, NIO)
+ """
+
+ mac_addr_table = yield from self._hypervisor.send('ethsw show_mac_addr_table "{}"'.format(self._name))
+ return mac_addr_table
+
+ @asyncio.coroutine
+ def clear_mac_addr_table(self):
+ """
+ Clears the MAC address table for this Ethernet switch.
+ """
+
+ yield from self._hypervisor.send('ethsw clear_mac_addr_table "{}"'.format(self._name))
+
+ @asyncio.coroutine
+ def start_capture(self, port_number, output_file, data_link_type="DLT_EN10MB"):
+ """
+ Starts a packet capture.
+
+ :param port_number: allocated port number
+ :param output_file: PCAP destination file for the capture
+ :param data_link_type: PCAP data link type (DLT_*), default is DLT_EN10MB
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+
+ data_link_type = data_link_type.lower()
+ if data_link_type.startswith("dlt_"):
+ data_link_type = data_link_type[4:]
+
+ if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
+ raise DynamipsError("Port {} has already a filter applied".format(port_number))
+
+ yield from nio.bind_filter("both", "capture")
+ yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
+
+ log.info('Ethernet switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
+
+ @asyncio.coroutine
+ def stop_capture(self, port_number):
+ """
+ Stops a packet capture.
+
+ :param port_number: allocated port number
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from nio.unbind_filter("both")
+ log.info('Ethernet switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py
new file mode 100644
index 00000000..cdbeb997
--- /dev/null
+++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py
@@ -0,0 +1,267 @@
+# -*- coding: utf-8 -*-
+#
+# Copyright (C) 2013 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 .
+
+"""
+Interface for Dynamips virtual Frame-Relay switch module.
+http://github.com/GNS3/dynamips/blob/master/README.hypervisor#L642
+"""
+
+import asyncio
+
+from .device import Device
+from ..dynamips_error import DynamipsError
+
+import logging
+log = logging.getLogger(__name__)
+
+
+class FrameRelaySwitch(Device):
+ """
+ Dynamips Frame Relay switch.
+
+ :param name: name for this switch
+ :param node_id: Node instance identifier
+ :param project: Project instance
+ :param manager: Parent VM Manager
+ :param hypervisor: Dynamips hypervisor instance
+ """
+
+ def __init__(self, name, node_id, project, manager, hypervisor=None):
+
+ super().__init__(name, node_id, project, manager, hypervisor)
+ self._nios = {}
+ self._mapping = {}
+
+ @asyncio.coroutine
+ def create(self):
+
+ if self._hypervisor is None:
+ self._hypervisor = yield from self.manager.start_new_hypervisor()
+
+ yield from self._hypervisor.send('frsw create "{}"'.format(self._name))
+ log.info('Frame Relay switch "{name}" [{id}] has been created'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.append(self)
+
+ @asyncio.coroutine
+ def set_name(self, new_name):
+ """
+ Renames this Frame Relay switch.
+
+ :param new_name: New name for this switch
+ """
+
+ yield from self._hypervisor.send('frsw rename "{name}" "{new_name}"'.format(name=self._name, new_name=new_name))
+ log.info('Frame Relay switch "{name}" [{id}]: renamed to "{new_name}"'.format(name=self._name,
+ id=self._id,
+ new_name=new_name))
+ self._name = new_name
+
+ @property
+ def nios(self):
+ """
+ Returns all the NIOs member of this Frame Relay switch.
+
+ :returns: nio list
+ """
+
+ return self._nios
+
+ @property
+ def mapping(self):
+ """
+ Returns port mapping
+
+ :returns: mapping list
+ """
+
+ return self._mapping
+
+ @asyncio.coroutine
+ def delete(self):
+ """
+ Deletes this Frame Relay switch.
+ """
+
+ yield from self._hypervisor.send('frsw delete "{}"'.format(self._name))
+ log.info('Frame Relay switch "{name}" [{id}] has been deleted'.format(name=self._name, id=self._id))
+ self._hypervisor.devices.remove(self)
+ self._instances.remove(self._id)
+
+ def has_port(self, port):
+ """
+ Checks if a port exists on this Frame Relay switch.
+
+ :returns: boolean
+ """
+
+ if port in self._nios:
+ return True
+ return False
+
+ def add_nio(self, nio, port_number):
+ """
+ Adds a NIO as new port on Frame Relay switch.
+
+ :param nio: NIO instance to add
+ :param port_number: port to allocate for the NIO
+ """
+
+ if port_number in self._nios:
+ raise DynamipsError("Port {} isn't free".format(port_number))
+
+ log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} bound to port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ self._nios[port_number] = nio
+
+ def remove_nio(self, port_number):
+ """
+ Removes the specified NIO as member of this Frame Relay switch.
+
+ :param port_number: allocated port number
+
+ :returns: the NIO that was bound to the allocated port
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ log.info('Frame Relay switch "{name}" [{id}]: NIO {nio} removed from port {port}'.format(name=self._name,
+ id=self._id,
+ nio=nio,
+ port=port_number))
+
+ del self._nios[port_number]
+ return nio
+
+ @asyncio.coroutine
+ def map_vc(self, port1, dlci1, port2, dlci2):
+ """
+ Creates a new Virtual Circuit connection (unidirectional).
+
+ :param port1: input port
+ :param dlci1: input DLCI
+ :param port2: output port
+ :param dlci2: output DLCI
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('frsw create_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name,
+ input_nio=nio1,
+ input_dlci=dlci1,
+ output_nio=nio2,
+ output_dlci=dlci2))
+
+ log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} created'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ dlci1=dlci1,
+ port2=port2,
+ dlci2=dlci2))
+
+ self._mapping[(port1, dlci1)] = (port2, dlci2)
+
+ @asyncio.coroutine
+ def unmap_vc(self, port1, dlci1, port2, dlci2):
+ """
+ Deletes a Virtual Circuit connection (unidirectional).
+
+ :param port1: input port
+ :param dlci1: input DLCI
+ :param port2: output port
+ :param dlci2: output DLCI
+ """
+
+ if port1 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port1))
+
+ if port2 not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port2))
+
+ nio1 = self._nios[port1]
+ nio2 = self._nios[port2]
+
+ yield from self._hypervisor.send('frsw delete_vc "{name}" {input_nio} {input_dlci} {output_nio} {output_dlci}'.format(name=self._name,
+ input_nio=nio1,
+ input_dlci=dlci1,
+ output_nio=nio2,
+ output_dlci=dlci2))
+
+ log.info('Frame Relay switch "{name}" [{id}]: VC from port {port1} DLCI {dlci1} to port {port2} DLCI {dlci2} deleted'.format(name=self._name,
+ id=self._id,
+ port1=port1,
+ dlci1=dlci1,
+ port2=port2,
+ dlci2=dlci2))
+ del self._mapping[(port1, dlci1)]
+
+ @asyncio.coroutine
+ def start_capture(self, port_number, output_file, data_link_type="DLT_FRELAY"):
+ """
+ Starts a packet capture.
+
+ :param port_number: allocated port number
+ :param output_file: PCAP destination file for the capture
+ :param data_link_type: PCAP data link type (DLT_*), default is DLT_FRELAY
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+
+ data_link_type = data_link_type.lower()
+ if data_link_type.startswith("dlt_"):
+ data_link_type = data_link_type[4:]
+
+ if nio.input_filter[0] is not None and nio.output_filter[0] is not None:
+ raise DynamipsError("Port {} has already a filter applied".format(port_number))
+
+ yield from nio.bind_filter("both", "capture")
+ yield from nio.setup_filter("both", "{} {}".format(data_link_type, output_file))
+
+ log.info('Frame relay switch "{name}" [{id}]: starting packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
+
+ @asyncio.coroutine
+ def stop_capture(self, port_number):
+ """
+ Stops a packet capture.
+
+ :param port_number: allocated port number
+ """
+
+ if port_number not in self._nios:
+ raise DynamipsError("Port {} is not allocated".format(port_number))
+
+ nio = self._nios[port_number]
+ yield from nio.unbind_filter("both")
+ log.info('Frame relay switch "{name}" [{id}]: stopping packet capture on {port}'.format(name=self._name,
+ id=self._id,
+ port=port_number))
diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py
index 055cab13..34e28ac5 100644
--- a/gns3server/modules/dynamips/nodes/router.py
+++ b/gns3server/modules/dynamips/nodes/router.py
@@ -1310,12 +1310,6 @@ class Router(BaseVM):
raise DynamipsError("Port {port_number} has already a filter applied on {adapter}".format(adapter=adapter,
port_number=port_number))
- # FIXME: capture
- # try:
- # os.makedirs(os.path.dirname(output_file), exist_ok=True)
- # except OSError as e:
- # raise DynamipsError("Could not create captures directory {}".format(e))
-
yield from nio.bind_filter("both", "capture")
yield from nio.setup_filter("both", '{} "{}"'.format(data_link_type, output_file))
@@ -1411,6 +1405,7 @@ class Router(BaseVM):
self._private_config = private_config
+ # TODO: rename
# def rename(self, new_name):
# """
# Renames this router.
diff --git a/gns3server/schemas/dynamips.py b/gns3server/schemas/dynamips.py
index c30d2920..2c4f755b 100644
--- a/gns3server/schemas/dynamips.py
+++ b/gns3server/schemas/dynamips.py
@@ -880,3 +880,281 @@ VM_OBJECT_SCHEMA = {
"additionalProperties": False,
"required": ["name", "vm_id", "project_id", "dynamips_id"]
}
+
+DEVICE_CREATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to create a new Dynamips device instance",
+ "type": "object",
+ "properties": {
+ "name": {
+ "description": "Dynamips device name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "vm_id": {
+ "description": "Dynamips device instance identifier",
+ "type": "string",
+ "minLength": 36,
+ "maxLength": 36,
+ "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$"
+ },
+ },
+ "additionalProperties": False,
+ "required": ["name"]
+}
+
+ETHHUB_UPDATE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to update an Ethernet hub instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Ethernet hub instance ID",
+ "type": "integer"
+ },
+ "name": {
+ "description": "Ethernet hub name",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id"]
+}
+
+ETHHUB_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to add a NIO for an Ethernet hub instance",
+ "type": "object",
+
+ "definitions": {
+ "UDP": {
+ "description": "UDP Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_udp"]
+ },
+ "lport": {
+ "description": "Local port",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ },
+ "rhost": {
+ "description": "Remote host",
+ "type": "string",
+ "minLength": 1
+ },
+ "rport": {
+ "description": "Remote port",
+ "type": "integer",
+ "minimum": 1,
+ "maximum": 65535
+ }
+ },
+ "required": ["type", "lport", "rhost", "rport"],
+ "additionalProperties": False
+ },
+ "Ethernet": {
+ "description": "Generic Ethernet Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_generic_ethernet"]
+ },
+ "ethernet_device": {
+ "description": "Ethernet device name e.g. eth0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "ethernet_device"],
+ "additionalProperties": False
+ },
+ "LinuxEthernet": {
+ "description": "Linux Ethernet Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_linux_ethernet"]
+ },
+ "ethernet_device": {
+ "description": "Ethernet device name e.g. eth0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "ethernet_device"],
+ "additionalProperties": False
+ },
+ "TAP": {
+ "description": "TAP Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_tap"]
+ },
+ "tap_device": {
+ "description": "TAP device name e.g. tap0",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "tap_device"],
+ "additionalProperties": False
+ },
+ "UNIX": {
+ "description": "UNIX Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_unix"]
+ },
+ "local_file": {
+ "description": "path to the UNIX socket file (local)",
+ "type": "string",
+ "minLength": 1
+ },
+ "remote_file": {
+ "description": "path to the UNIX socket file (remote)",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "local_file", "remote_file"],
+ "additionalProperties": False
+ },
+ "VDE": {
+ "description": "VDE Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_vde"]
+ },
+ "control_file": {
+ "description": "path to the VDE control file",
+ "type": "string",
+ "minLength": 1
+ },
+ "local_file": {
+ "description": "path to the VDE control file",
+ "type": "string",
+ "minLength": 1
+ },
+ },
+ "required": ["type", "control_file", "local_file"],
+ "additionalProperties": False
+ },
+ "NULL": {
+ "description": "NULL Network Input/Output",
+ "properties": {
+ "type": {
+ "enum": ["nio_null"]
+ },
+ },
+ "required": ["type"],
+ "additionalProperties": False
+ },
+ },
+
+ "properties": {
+ "id": {
+ "description": "Ethernet hub instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the Ethernet hub instance",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 1,
+ },
+ "nio": {
+ "type": "object",
+ "description": "Network Input/Output",
+ "oneOf": [
+ {"$ref": "#/definitions/UDP"},
+ {"$ref": "#/definitions/Ethernet"},
+ {"$ref": "#/definitions/LinuxEthernet"},
+ {"$ref": "#/definitions/TAP"},
+ {"$ref": "#/definitions/UNIX"},
+ {"$ref": "#/definitions/VDE"},
+ {"$ref": "#/definitions/NULL"},
+ ]
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port_id", "port", "nio"]
+}
+
+ETHHUB_DELETE_NIO_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to delete a NIO for an Ethernet hub instance",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Ethernet hub instance ID",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port"]
+}
+
+ETHHUB_START_CAPTURE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to start a packet capture on an Ethernet hub instance port",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Ethernet hub instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the Ethernet hub instance",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 1,
+ },
+ "capture_file_name": {
+ "description": "Capture file name",
+ "type": "string",
+ "minLength": 1,
+ },
+ "data_link_type": {
+ "description": "PCAP data link type",
+ "type": "string",
+ "minLength": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port_id", "port", "capture_file_name"]
+}
+
+ETHHUB_STOP_CAPTURE_SCHEMA = {
+ "$schema": "http://json-schema.org/draft-04/schema#",
+ "description": "Request validation to stop a packet capture on an Ethernet hub instance port",
+ "type": "object",
+ "properties": {
+ "id": {
+ "description": "Ethernet hub instance ID",
+ "type": "integer"
+ },
+ "port_id": {
+ "description": "Unique port identifier for the Ethernet hub instance",
+ "type": "integer"
+ },
+ "port": {
+ "description": "Port number",
+ "type": "integer",
+ "minimum": 1,
+ },
+ },
+ "additionalProperties": False,
+ "required": ["id", "port_id", "port"]
+}
diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py
index dcd2e60f..c195b459 100644
--- a/tests/api/test_virtualbox.py
+++ b/tests/api/test_virtualbox.py
@@ -105,7 +105,7 @@ def test_vbox_nio_create_udp(server, vm):
assert args[0] == 0
assert response.status == 201
- assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio"
+ assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
assert response.json["type"] == "nio_udp"
@@ -119,7 +119,7 @@ def test_vbox_delete_nio(server, vm):
assert args[0] == 0
assert response.status == 204
- assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_id:\d+}/ports/{port_id:\d+}/nio"
+ assert response.route == "/projects/{project_id}/virtualbox/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio"
def test_vbox_update(server, vm, free_console_port):