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