diff --git a/MANIFEST.in b/MANIFEST.in index 55a68cb2..ff327eea 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -4,7 +4,7 @@ include INSTALL include LICENSE include MANIFEST.in include tox.ini -recursive-exclude tests * +recursive-include tests * recursive-exclude docs * recursive-include gns3server * recursive-exclude * __pycache__ diff --git a/gns3server/handlers/api/dynamips_device_handler.py b/gns3server/handlers/api/dynamips_device_handler.py index 95307b89..10220cec 100644 --- a/gns3server/handlers/api/dynamips_device_handler.py +++ b/gns3server/handlers/api/dynamips_device_handler.py @@ -181,10 +181,8 @@ class DynamipsDeviceHandler: dynamips_manager = Dynamips.instance() device = dynamips_manager.get_device(request.match_info["device_id"], project_id=request.match_info["project_id"]) port_number = int(request.match_info["port_number"]) - if asyncio.iscoroutinefunction(device.remove_nio): - yield from device.remove_nio(port_number) - else: - device.remove_nio(port_number) + nio = yield from device.remove_nio(port_number) + yield from nio.delete() response.set_status(204) @Route.post( diff --git a/gns3server/handlers/api/dynamips_vm_handler.py b/gns3server/handlers/api/dynamips_vm_handler.py index c125bbb2..8d96f98b 100644 --- a/gns3server/handlers/api/dynamips_vm_handler.py +++ b/gns3server/handlers/api/dynamips_vm_handler.py @@ -290,7 +290,8 @@ class DynamipsVMHandler: vm = dynamips_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) slot_number = int(request.match_info["adapter_number"]) port_number = int(request.match_info["port_number"]) - yield from vm.slot_remove_nio_binding(slot_number, port_number) + nio = yield from vm.slot_remove_nio_binding(slot_number, port_number) + yield from nio.delete() response.set_status(204) @Route.post( diff --git a/gns3server/handlers/api/iou_handler.py b/gns3server/handlers/api/iou_handler.py index 35e413b4..98bd4cf7 100644 --- a/gns3server/handlers/api/iou_handler.py +++ b/gns3server/handlers/api/iou_handler.py @@ -19,7 +19,6 @@ import os from aiohttp.web import HTTPConflict from ...web.route import Route -from ...modules.port_manager import PortManager from ...schemas.iou import IOU_CREATE_SCHEMA from ...schemas.iou import IOU_UPDATE_SCHEMA from ...schemas.iou import IOU_OBJECT_SCHEMA @@ -52,20 +51,16 @@ class IOUHandler: def create(request, response): iou = IOU.instance() - vm = yield from iou.create_vm(request.json["name"], + vm = yield from iou.create_vm(request.json.pop("name"), request.match_info["project_id"], request.json.get("vm_id"), - console=request.json.get("console"), - serial_adapters=request.json.get("serial_adapters"), - ethernet_adapters=request.json.get("ethernet_adapters"), - ram=request.json.get("ram"), - nvram=request.json.get("nvram"), - use_default_iou_values=request.json.get("use_default_iou_values"), - l1_keepalives=request.json.get("l1_keepalives"), - initial_config=request.json.get("initial_config_content"), - iourc_content=request.json.get("iourc_content") - ) - vm.path = request.json.get("path", vm.path) + console=request.json.get("console")) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + if "initial_config_content" in request.json: + vm.initial_config = request.json.get("initial_config_content") response.set_status(201) response.json(vm) @@ -109,18 +104,12 @@ class IOUHandler: iou_manager = IOU.instance() vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - vm.name = request.json.get("name", vm.name) - vm.console = request.json.get("console", vm.console) - vm.path = request.json.get("path", vm.path) - vm.ethernet_adapters = request.json.get("ethernet_adapters", vm.ethernet_adapters) - vm.serial_adapters = request.json.get("serial_adapters", vm.serial_adapters) - vm.ram = request.json.get("ram", vm.ram) - vm.nvram = request.json.get("nvram", vm.nvram) - vm.use_default_iou_values = request.json.get("use_default_iou_values", vm.use_default_iou_values) - vm.l1_keepalives = request.json.get("l1_keepalives", vm.l1_keepalives) - vm.initial_config = request.json.get("initial_config_content", vm.initial_config) - vm.iourc_content = request.json.get("iourc_content", None) + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + if "initial_config_content" in request.json: + vm.initial_config = request.json.get("initial_config_content") response.json(vm) @classmethod @@ -273,7 +262,7 @@ class IOUHandler: pcap_file_path = os.path.join(vm.project.capture_working_directory(), request.json["capture_file_name"]) if not vm.is_running(): - raise HTTPConflict(text="You can't capture the traffic on a non started VM") + raise HTTPConflict(text="Cannot capture traffic on a non started VM") yield from vm.start_capture(adapter_number, port_number, pcap_file_path, request.json["data_link_type"]) response.json({"pcap_file_path": str(pcap_file_path)}) @@ -298,7 +287,7 @@ class IOUHandler: vm = iou_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) if not vm.is_running(): - raise HTTPConflict(text="You can't capture the traffic on a non started VM") + raise HTTPConflict(text="Cannot capture traffic on a non started VM") adapter_number = int(request.match_info["adapter_number"]) port_number = int(request.match_info["port_number"]) diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 14d12d07..714149d6 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -41,24 +41,21 @@ class QEMUHandler: 400: "Invalid request", 409: "Conflict" }, - description="Create a new Qemu.instance", + description="Create a new Qemu VM instance", input=QEMU_CREATE_SCHEMA, output=QEMU_OBJECT_SCHEMA) def create(request, response): qemu = Qemu.instance() - vm = yield from qemu.create_vm(request.json["name"], + vm = yield from qemu.create_vm(request.json.pop("name"), request.match_info["project_id"], request.json.get("vm_id"), qemu_path=request.json.get("qemu_path"), console=request.json.get("console")) - # Clear already used keys - map(request.json.__delitem__, ["name", "project_id", "vm_id", - "qemu_path", "console"]) - - for field in request.json: - setattr(vm, field, request.json[field]) + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) response.set_status(201) response.json(vm) @@ -75,7 +72,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Get a Qemu.instance", + description="Get a Qemu VM instance", output=QEMU_OBJECT_SCHEMA) def show(request, response): @@ -96,15 +93,17 @@ class QEMUHandler: 404: "Instance doesn't exist", 409: "Conflict" }, - description="Update a Qemu.instance", + description="Update a Qemu VM instance", input=QEMU_UPDATE_SCHEMA, output=QEMU_OBJECT_SCHEMA) def update(request, response): qemu_manager = Qemu.instance() vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) - for field in request.json: - setattr(vm, field, request.json[field]) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) response.json(vm) @@ -120,7 +119,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Delete a Qemu.instance") + description="Delete a Qemu VM instance") def delete(request, response): yield from Qemu.instance().delete_vm(request.match_info["vm_id"]) @@ -138,7 +137,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Start a Qemu.instance") + description="Start a Qemu VM instance") def start(request, response): qemu_manager = Qemu.instance() @@ -158,7 +157,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Stop a Qemu.instance") + description="Stop a Qemu VM instance") def stop(request, response): qemu_manager = Qemu.instance() @@ -178,7 +177,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Reload a Qemu.instance") + description="Reload a Qemu VM instance") def reload(request, response): qemu_manager = Qemu.instance() @@ -198,7 +197,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Suspend a Qemu.instance") + description="Suspend a Qemu VM instance") def suspend(request, response): qemu_manager = Qemu.instance() @@ -218,7 +217,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Resume a Qemu.instance") + description="Resume a Qemu VM instance") def resume(request, response): qemu_manager = Qemu.instance() @@ -239,7 +238,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Add a NIO to a Qemu.instance", + description="Add a NIO to a Qemu VM instance", input=QEMU_NIO_SCHEMA, output=QEMU_NIO_SCHEMA) def create_nio(request, response): @@ -265,7 +264,7 @@ class QEMUHandler: 400: "Invalid request", 404: "Instance doesn't exist" }, - description="Remove a NIO from a Qemu.instance") + description="Remove a NIO from a Qemu VM instance") def delete_nio(request, response): qemu_manager = Qemu.instance() diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index f0765180..008ac2e6 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -79,8 +79,9 @@ class VirtualBoxHandler: yield from vm.set_ram(ram) for name, value in request.json.items(): - if hasattr(vm, name) and getattr(vm, name) != value: - setattr(vm, name, value) + if name != "vm_id": + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) response.set_status(201) response.json(vm) diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 9238aaad..d6097fce 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -371,3 +371,40 @@ class BaseManager: nio = NIOGenericEthernet(nio_settings["ethernet_device"]) assert nio is not None return nio + + def get_abs_image_path(self, path): + """ + Get the absolute path of an image + + :param path: file path + :return: file path + """ + + img_directory = self.get_images_directory() + + if not os.path.isabs(path): + s = os.path.split(path) + return os.path.normpath(os.path.join(img_directory, *s)) + return path + + def get_relative_image_path(self, path): + """ + Get a path relative to images directory path + or an abspath if the path is not located inside + image directory + + :param path: file path + :return: file path + """ + + img_directory = self.get_images_directory() + path = self.get_abs_image_path(path) + if os.path.dirname(path) == img_directory: + return os.path.basename(path) + return path + + def get_images_directory(self): + """ + Get the image directory on disk + """ + raise NotImplementedError diff --git a/gns3server/modules/dynamips/__init__.py b/gns3server/modules/dynamips/__init__.py index 1592e9ce..ed6858a8 100644 --- a/gns3server/modules/dynamips/__init__.py +++ b/gns3server/modules/dynamips/__init__.py @@ -616,3 +616,9 @@ class Dynamips(BaseManager): if was_auto_started: yield from vm.stop() return validated_idlepc + + def get_images_directory(self): + """ + Return the full path of the images directory on disk + """ + return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "IOS") diff --git a/gns3server/modules/dynamips/nios/nio_fifo.py b/gns3server/modules/dynamips/nios/nio_fifo.py index f1d763fc..9e7695a2 100644 --- a/gns3server/modules/dynamips/nios/nio_fifo.py +++ b/gns3server/modules/dynamips/nios/nio_fifo.py @@ -20,6 +20,7 @@ Interface for FIFO NIOs. """ import asyncio +import uuid from .nio import NIO import logging @@ -34,24 +35,12 @@ class NIOFIFO(NIO): :param hypervisor: Dynamips hypervisor instance """ - _instance_count = 0 - def __init__(self, hypervisor): - # create an unique ID and name - nio_id = NIOFIFO._instance_count - NIOFIFO._instance_count += 1 - name = 'nio_fifo' + str(nio_id) + # create an unique name + name = 'fifo-{}'.format(uuid.uuid4()) super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py index dcc13bae..870ecb13 100644 --- a/gns3server/modules/dynamips/nios/nio_generic_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_generic_ethernet.py @@ -20,6 +20,7 @@ Interface for generic Ethernet NIOs (PCAP library). """ import asyncio +import uuid from .nio import NIO import logging @@ -35,25 +36,13 @@ class NIOGenericEthernet(NIO): :param ethernet_device: Ethernet device name (e.g. eth0) """ - _instance_count = 0 - def __init__(self, hypervisor, ethernet_device): - # create an unique ID and name - nio_id = NIOGenericEthernet._instance_count - NIOGenericEthernet._instance_count += 1 - name = 'nio_gen_eth' + str(nio_id) + # create an unique name + name = 'generic_ethernet-{}'.format(uuid.uuid4()) self._ethernet_device = ethernet_device super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py index 500123b5..cf96f4af 100644 --- a/gns3server/modules/dynamips/nios/nio_linux_ethernet.py +++ b/gns3server/modules/dynamips/nios/nio_linux_ethernet.py @@ -20,6 +20,7 @@ Interface for Linux Ethernet NIOs (Linux only). """ import asyncio +import uuid from .nio import NIO import logging @@ -35,25 +36,12 @@ class NIOLinuxEthernet(NIO): :param ethernet_device: Ethernet device name (e.g. eth0) """ - _instance_count = 0 - def __init__(self, hypervisor, ethernet_device): - - # create an unique ID and name - nio_id = NIOLinuxEthernet._instance_count - NIOLinuxEthernet._instance_count += 1 - name = 'nio_linux_eth' + str(nio_id) + # create an unique name + name = 'linux_ethernet-{}'.format(uuid.uuid4()) self._ethernet_device = ethernet_device super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_mcast.py b/gns3server/modules/dynamips/nios/nio_mcast.py index f59dc280..4ea47dc2 100644 --- a/gns3server/modules/dynamips/nios/nio_mcast.py +++ b/gns3server/modules/dynamips/nios/nio_mcast.py @@ -20,6 +20,7 @@ Interface for multicast NIOs. """ import asyncio +import uuid from .nio import NIO import logging @@ -36,27 +37,15 @@ class NIOMcast(NIO): :param port: port for binding """ - _instance_count = 0 - def __init__(self, hypervisor, group, port): - # create an unique ID and name - nio_id = NIOMcast._instance_count - NIOMcast._instance_count += 1 - name = 'nio_mcast' + str(nio_id) + # create an unique name + name = 'mcast-{}'.format(uuid.uuid4()) self._group = group self._port = port self._ttl = 1 # default TTL super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_null.py b/gns3server/modules/dynamips/nios/nio_null.py index 4d46137c..174dd032 100644 --- a/gns3server/modules/dynamips/nios/nio_null.py +++ b/gns3server/modules/dynamips/nios/nio_null.py @@ -20,6 +20,7 @@ Interface for dummy NIOs (mostly for tests). """ import asyncio +import uuid from .nio import NIO import logging @@ -34,24 +35,12 @@ class NIONull(NIO): :param hypervisor: Dynamips hypervisor instance """ - _instance_count = 0 - def __init__(self, hypervisor): - # create an unique ID and name - nio_id = NIONull._instance_count - NIONull._instance_count += 1 - name = 'nio_null' + str(nio_id) + # create an unique name + name = 'null-{}'.format(uuid.uuid4()) super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_tap.py b/gns3server/modules/dynamips/nios/nio_tap.py index b4bd172f..20fdaeaa 100644 --- a/gns3server/modules/dynamips/nios/nio_tap.py +++ b/gns3server/modules/dynamips/nios/nio_tap.py @@ -20,6 +20,7 @@ Interface for TAP NIOs (UNIX based OSes only). """ import asyncio +import uuid from .nio import NIO import logging @@ -35,25 +36,13 @@ class NIOTAP(NIO): :param tap_device: TAP device name (e.g. tap0) """ - _instance_count = 0 - def __init__(self, hypervisor, tap_device): - # create an unique ID and name - nio_id = NIOTAP._instance_count - NIOTAP._instance_count += 1 - name = 'nio_tap' + str(nio_id) + # create an unique name + name = 'tap-{}'.format(uuid.uuid4()) self._tap_device = tap_device super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_udp.py b/gns3server/modules/dynamips/nios/nio_udp.py index eeb15e32..c7c2b656 100644 --- a/gns3server/modules/dynamips/nios/nio_udp.py +++ b/gns3server/modules/dynamips/nios/nio_udp.py @@ -20,6 +20,7 @@ Interface for UDP NIOs. """ import asyncio +import uuid from .nio import NIO import logging @@ -37,27 +38,15 @@ class NIOUDP(NIO): :param rport: remote port number """ - _instance_count = 0 - def __init__(self, hypervisor, lport, rhost, rport): - # create an unique ID and name - nio_id = NIOUDP._instance_count - NIOUDP._instance_count += 1 - name = 'nio_udp' + str(nio_id) + # create an unique name + name = 'udp-{}'.format(uuid.uuid4()) self._lport = lport self._rhost = rhost self._rport = rport super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_unix.py b/gns3server/modules/dynamips/nios/nio_unix.py index 45fa41c0..048572a4 100644 --- a/gns3server/modules/dynamips/nios/nio_unix.py +++ b/gns3server/modules/dynamips/nios/nio_unix.py @@ -20,6 +20,7 @@ Interface for UNIX NIOs (Unix based OSes only). """ import asyncio +import uuid from .nio import NIO import logging @@ -36,26 +37,14 @@ class NIOUNIX(NIO): :param remote_file: remote UNIX socket filename """ - _instance_count = 0 - def __init__(self, hypervisor, local_file, remote_file): - # create an unique ID and name - nio_id = NIOUNIX._instance_count - NIOUNIX._instance_count += 1 - name = 'nio_unix' + str(nio_id) + # create an unique name + name = 'unix-{}'.format(uuid.uuid4()) self._local_file = local_file self._remote_file = remote_file super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nios/nio_vde.py b/gns3server/modules/dynamips/nios/nio_vde.py index 17a85524..6ac04259 100644 --- a/gns3server/modules/dynamips/nios/nio_vde.py +++ b/gns3server/modules/dynamips/nios/nio_vde.py @@ -20,6 +20,7 @@ Interface for VDE (Virtual Distributed Ethernet) NIOs (Unix based OSes only). """ import asyncio +import uuid from .nio import NIO import logging @@ -36,26 +37,14 @@ class NIOVDE(NIO): :param local_file: VDE local filename """ - _instance_count = 0 - def __init__(self, hypervisor, control_file, local_file): - # create an unique ID and name - nio_id = NIOVDE._instance_count - NIOVDE._instance_count += 1 - name = 'nio_vde' + str(nio_id) + # create an unique name + name = 'vde-{}'.format(uuid.uuid4()) self._control_file = control_file self._local_file = local_file super().__init__(name, hypervisor) - @classmethod - def reset(cls): - """ - Reset the instance count. - """ - - cls._instance_count = 0 - @asyncio.coroutine def create(self): diff --git a/gns3server/modules/dynamips/nodes/atm_switch.py b/gns3server/modules/dynamips/nodes/atm_switch.py index 4064ad07..141293af 100644 --- a/gns3server/modules/dynamips/nodes/atm_switch.py +++ b/gns3server/modules/dynamips/nodes/atm_switch.py @@ -150,6 +150,7 @@ class ATMSwitch(Device): self._nios[port_number] = nio + @asyncio.coroutine def remove_nio(self, port_number): """ Removes the specified NIO as member of this ATM switch. @@ -160,6 +161,23 @@ class ATMSwitch(Device): if port_number not in self._nios: raise DynamipsError("Port {} is not allocated".format(port_number)) + # remove VCs mapped with the port + for source, destination in self._mappings.copy().items(): + if len(source) == 3 and len(destination) == 3: + # remove the virtual channels mapped with this port/nio + source_port, source_vpi, source_vci = source + destination_port, destination_vpi, destination_vci = destination + if port_number == source_port: + yield from self.unmap_pvc(source_port, source_vpi, source_vci, destination_port, destination_vpi, destination_vci) + yield from self.unmap_pvc(destination_port, destination_vpi, destination_vci, source_port, source_vpi, source_vci) + else: + # remove the virtual paths mapped with this port/nio + source_port, source_vpi = source + destination_port, destination_vpi = destination + if port_number == source_port: + yield from self.unmap_vp(source_port, source_vpi, destination_port, destination_vpi) + yield from self.unmap_vp(destination_port, destination_vpi, source_port, source_vpi) + nio = self._nios[port_number] if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) diff --git a/gns3server/modules/dynamips/nodes/frame_relay_switch.py b/gns3server/modules/dynamips/nodes/frame_relay_switch.py index a4bf56e6..c1f06c84 100644 --- a/gns3server/modules/dynamips/nodes/frame_relay_switch.py +++ b/gns3server/modules/dynamips/nodes/frame_relay_switch.py @@ -149,6 +149,7 @@ class FrameRelaySwitch(Device): self._nios[port_number] = nio + @asyncio.coroutine def remove_nio(self, port_number): """ Removes the specified NIO as member of this Frame Relay switch. @@ -161,6 +162,14 @@ class FrameRelaySwitch(Device): if port_number not in self._nios: raise DynamipsError("Port {} is not allocated".format(port_number)) + # remove VCs mapped with the port + for source, destination in self._mappings.copy().items(): + source_port, source_dlci = source + destination_port, destination_dlci = destination + if port_number == source_port: + yield from self.unmap_vc(source_port, source_dlci, destination_port, destination_dlci) + yield from self.unmap_vc(destination_port, destination_dlci, source_port, source_dlci) + nio = self._nios[port_number] if isinstance(nio, NIOUDP): self.manager.port_manager.release_udp_port(nio.lport, self._project) diff --git a/gns3server/modules/dynamips/nodes/router.py b/gns3server/modules/dynamips/nodes/router.py index 568a11f6..1875ab22 100644 --- a/gns3server/modules/dynamips/nodes/router.py +++ b/gns3server/modules/dynamips/nodes/router.py @@ -151,10 +151,7 @@ class Router(BaseVM): "system_id": self._system_id} # return the relative path if the IOS image is in the images_path directory - server_config = self.manager.config.get_section_config("Server") - relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOS", self._image) - if os.path.exists(relative_image): - router_info["image"] = os.path.basename(self._image) + router_info["image"] = self.manager.get_relative_image_path(self._image) # add the slots slot_number = 0 @@ -172,14 +169,6 @@ class Router(BaseVM): return router_info - @classmethod - def reset(cls): - """ - Resets the instance count and the allocated instances list. - """ - - cls._dynamips_ids.clear() - @property def dynamips_id(self): """ @@ -427,9 +416,7 @@ class Router(BaseVM): :param image: path to IOS image file """ - if not os.path.isabs(image): - server_config = self.manager.config.get_section_config("Server") - image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOS", image) + image = self.manager.get_abs_image_path(image) if not os.path.isfile(image): raise DynamipsError("IOS image '{}' is not accessible".format(image)) diff --git a/gns3server/modules/iou/__init__.py b/gns3server/modules/iou/__init__.py index 764d6475..df6ad3c7 100644 --- a/gns3server/modules/iou/__init__.py +++ b/gns3server/modules/iou/__init__.py @@ -91,3 +91,9 @@ class IOU(BaseManager): """ return os.path.join("iou", "device-{}".format(legacy_vm_id)) + + def get_images_directory(self): + """ + Return the full path of the images directory on disk + """ + return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "IOU") diff --git a/gns3server/modules/iou/iou_vm.py b/gns3server/modules/iou/iou_vm.py index d8d47a33..305250e0 100644 --- a/gns3server/modules/iou/iou_vm.py +++ b/gns3server/modules/iou/iou_vm.py @@ -61,25 +61,9 @@ class IOUVM(BaseVM): :param project: Project instance :param manager: Manager instance :param console: TCP console port - :params ethernet_adapters: number of ethernet adapters - :params serial_adapters: number of serial adapters - :params ram: amount of RAM in MB - :params nvram: amount of NVRAM in KB - :params l1_keepalives: always keep the Ethernet interfaces up - :params initial_config: content of the initial configuration file - :params iourc_content: content of the iourc file if no licence is installed on the machine """ - def __init__(self, name, vm_id, project, manager, - console=None, - ram=None, - nvram=None, - use_default_iou_values=None, - ethernet_adapters=None, - serial_adapters=None, - l1_keepalives=None, - initial_config=None, - iourc_content=None): + def __init__(self, name, vm_id, project, manager, console=None): super().__init__(name, vm_id, project, manager, console=console) @@ -94,17 +78,13 @@ class IOUVM(BaseVM): # IOU settings self._ethernet_adapters = [] self._serial_adapters = [] - self.ethernet_adapters = 2 if ethernet_adapters is None else ethernet_adapters # one adapter = 4 interfaces - self.serial_adapters = 2 if serial_adapters is None else serial_adapters # one adapter = 4 interfaces - self._use_default_iou_values = True if use_default_iou_values is None else use_default_iou_values # for RAM & NVRAM values - self._nvram = 128 if nvram is None else nvram # Kilobytes + self.ethernet_adapters = 2 # one adapter = 4 interfaces + self.serial_adapters = 2 # one adapter = 4 interfaces + self._use_default_iou_values = True # for RAM & NVRAM values + self._nvram = 128 # Kilobytes self._initial_config = "" - self._ram = 256 if ram is None else ram # Megabytes - self._l1_keepalives = False if l1_keepalives is None else l1_keepalives # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). - - self.iourc_content = iourc_content - if initial_config is not None: - self.initial_config = initial_config + self._ram = 256 # Megabytes + self._l1_keepalives = False # used to overcome the always-up Ethernet interfaces (not supported by all IOSes). @asyncio.coroutine def close(self): @@ -145,14 +125,7 @@ class IOUVM(BaseVM): :param path: path to the IOU image executable """ - if not os.path.isabs(path): - server_config = self.manager.config.get_section_config("Server") - relative_path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), path) - if not os.path.exists(relative_path): - relative_path = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", path) - path = relative_path - - self._path = path + self._path = self.manager.get_abs_image_path(path) # In 1.2 users uploaded images to the images roots # after the migration their images are inside images/IOU @@ -236,15 +209,11 @@ class IOUVM(BaseVM): "nvram": self._nvram, "l1_keepalives": self._l1_keepalives, "initial_config": self.relative_initial_config_file, - "use_default_iou_values": self._use_default_iou_values, - "iourc_path": self.iourc_path} + "iourc_path": self.iourc_path, + "use_default_iou_values": self._use_default_iou_values} # return the relative path if the IOU image is in the images_path directory - server_config = self.manager.config.get_section_config("Server") - relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "IOU", self.path) - if os.path.exists(relative_image): - iou_vm_info["path"] = os.path.basename(self.path) - + iou_vm_info["path"] = self.manager.get_relative_image_path(self.path) return iou_vm_info @property diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index f1af09f8..b0f882d8 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -252,19 +252,14 @@ class PortManager: project.record_udp_port(port) log.debug("UDP port {} has been reserved".format(port)) - def release_udp_port(self, port, project, force_remove=False): + def release_udp_port(self, port, project): """ Release a specific UDP port number :param port: UDP port number :param project: Project instance - :param force_remove: Force port removal even on Darwnin """ - # A bug with Dynamips on Darwin which doesn't correctly free UDP ports, they are freed only when changing the project - if sys.platform.startswith("darwin") and force_remove is False: - return - if port in self._used_udp_ports: self._used_udp_ports.remove(port) project.remove_udp_port(port) diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index fa71ab51..582ef2ae 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -365,7 +365,7 @@ class Project: for port in self._used_tcp_ports.copy(): port_manager.release_tcp_port(port, self) for port in self._used_udp_ports.copy(): - port_manager.release_udp_port(port, self, force_remove=True) + port_manager.release_udp_port(port, self) @asyncio.coroutine def commit(self): diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index d348894f..e5bf698a 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -112,3 +112,9 @@ class Qemu(BaseManager): """ return os.path.join("qemu", "vm-{}".format(legacy_vm_id)) + + def get_images_directory(self): + """ + Return the full path of the images directory on disk + """ + return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "QEMU") diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 75731d24..420bd5e0 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -148,14 +148,10 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ - if not os.path.isabs(hda_disk_image): - server_config = self.manager.config.get_section_config("Server") - hda_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hda_disk_image) - + self._hda_disk_image = self.manager.get_abs_image_path(hda_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hda disk image path to {disk_image}'.format(name=self._name, id=self._id, - disk_image=hda_disk_image)) - self._hda_disk_image = hda_disk_image + disk_image=self._hda_disk_image)) @property def hdb_disk_image(self): @@ -175,14 +171,10 @@ class QemuVM(BaseVM): :param hdb_disk_image: QEMU hdb disk image path """ - if not os.path.isabs(hdb_disk_image): - server_config = self.manager.config.get_section_config("Server") - hdb_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdb_disk_image) - + self._hdb_disk_image = self.manager.get_abs_image_path(hdb_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hdb disk image path to {disk_image}'.format(name=self._name, id=self._id, - disk_image=hdb_disk_image)) - self._hdb_disk_image = hdb_disk_image + disk_image=self._hdb_disk_image)) @property def hdc_disk_image(self): @@ -202,14 +194,10 @@ class QemuVM(BaseVM): :param hdc_disk_image: QEMU hdc disk image path """ - if not os.path.isabs(hdc_disk_image): - server_config = self.manager.config.get_section_config("Server") - hdc_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdc_disk_image) - + self._hdc_disk_image = self.manager.get_abs_image_path(hdc_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hdc disk image path to {disk_image}'.format(name=self._name, id=self._id, - disk_image=hdc_disk_image)) - self._hdc_disk_image = hdc_disk_image + disk_image=self._hdc_disk_image)) @property def hdd_disk_image(self): @@ -229,14 +217,11 @@ class QemuVM(BaseVM): :param hdd_disk_image: QEMU hdd disk image path """ - if not os.path.isabs(hdd_disk_image): - server_config = self.manager.config.get_section_config("Server") - hdd_disk_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", hdd_disk_image) - + self._hdd_disk_image = hdd_disk_image + self._hdd_disk_image = self.manager.get_abs_image_path(hdd_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hdd disk image path to {disk_image}'.format(name=self._name, id=self._id, - disk_image=hdd_disk_image)) - self._hdd_disk_image = hdd_disk_image + disk_image=self._hdd_disk_image)) @property def adapters(self): @@ -778,9 +763,9 @@ class QemuVM(BaseVM): adapter.add_nio(0, nio) log.info('QEMU VM "{name}" [{id}]: {nio} added to adapter {adapter_number}'.format(name=self._name, - id=self._id, - nio=nio, - adapter_number=adapter_number)) + id=self._id, + nio=nio, + adapter_number=adapter_number)) @asyncio.coroutine def adapter_remove_nio_binding(self, adapter_number): @@ -1068,23 +1053,6 @@ class QemuVM(BaseVM): command.extend(self._graphic()) return command - def _get_relative_disk_image_path(self, disk_image): - """ - Returns a relative image path if the disk image is in the images directory. - - :param disk_image: path to the disk image - - :returns: relative or full path - """ - - if disk_image: - # return the relative path if the disk image is in the images_path directory - server_config = self.manager.config.get_section_config("Server") - relative_image = os.path.join(os.path.expanduser(server_config.get("images_path", "~/GNS3/images")), "QEMU", disk_image) - if os.path.exists(relative_image): - return os.path.basename(disk_image) - return disk_image - def __json__(self): answer = { "project_id": self.project.id, @@ -1095,11 +1063,11 @@ class QemuVM(BaseVM): if field not in answer: answer[field] = getattr(self, field) - answer["hda_disk_image"] = self._get_relative_disk_image_path(self._hda_disk_image) - answer["hdb_disk_image"] = self._get_relative_disk_image_path(self._hdb_disk_image) - answer["hdc_disk_image"] = self._get_relative_disk_image_path(self._hdc_disk_image) - answer["hdd_disk_image"] = self._get_relative_disk_image_path(self._hdd_disk_image) - answer["initrd"] = self._get_relative_disk_image_path(self._initrd) - answer["kernel_image"] = self._get_relative_disk_image_path(self._kernel_image) + answer["hda_disk_image"] = self.manager.get_relative_image_path(self._hda_disk_image) + answer["hdb_disk_image"] = self.manager.get_relative_image_path(self._hdb_disk_image) + answer["hdc_disk_image"] = self.manager.get_relative_image_path(self._hdc_disk_image) + answer["hdd_disk_image"] = self.manager.get_relative_image_path(self._hdd_disk_image) + answer["initrd"] = self.manager.get_relative_image_path(self._initrd) + answer["kernel_image"] = self.manager.get_relative_image_path(self._kernel_image) return answer diff --git a/gns3server/modules/virtualbox/__init__.py b/gns3server/modules/virtualbox/__init__.py index 5ea015e0..a6440072 100644 --- a/gns3server/modules/virtualbox/__init__.py +++ b/gns3server/modules/virtualbox/__init__.py @@ -41,6 +41,7 @@ class VirtualBox(BaseManager): super().__init__() self._vboxmanage_path = None + self._execute_lock = asyncio.Lock() @property def vboxmanage_path(self): @@ -82,34 +83,38 @@ class VirtualBox(BaseManager): @asyncio.coroutine def execute(self, subcommand, args, timeout=60): - vboxmanage_path = self.vboxmanage_path - if not vboxmanage_path: - vboxmanage_path = self.find_vboxmanage() - command = [vboxmanage_path, "--nologo", subcommand] - command.extend(args) - log.debug("Executing VBoxManage with command: {}".format(command)) - try: - vbox_user = self.config.get_section_config("VirtualBox").get("vbox_user") - if vbox_user: - # TODO: test & review this part - sudo_command = "sudo -i -u {}".format(vbox_user) + " ".join(command) - process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - else: - process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) - except (OSError, subprocess.SubprocessError) as e: - raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) + # We use a lock prevent parallel execution due to strange errors + # reported by a user and reproduced by us. + # https://github.com/GNS3/gns3-gui/issues/261 + with (yield from self._execute_lock): + vboxmanage_path = self.vboxmanage_path + if not vboxmanage_path: + vboxmanage_path = self.find_vboxmanage() + command = [vboxmanage_path, "--nologo", subcommand] + command.extend(args) + log.debug("Executing VBoxManage with command: {}".format(command)) + try: + vbox_user = self.config.get_section_config("VirtualBox").get("vbox_user") + if vbox_user: + # TODO: test & review this part + sudo_command = "sudo -i -u {}".format(vbox_user) + " ".join(command) + process = yield from asyncio.create_subprocess_shell(sudo_command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + else: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VirtualBoxError("Could not execute VBoxManage: {}".format(e)) - try: - stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout) - except asyncio.TimeoutError: - raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) + try: + stdout_data, stderr_data = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VirtualBoxError("VBoxManage has timed out after {} seconds!".format(timeout)) - if process.returncode: - # only the first line of the output is useful - vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") - raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error)) + if process.returncode: + # only the first line of the output is useful + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore") + raise VirtualBoxError("VirtualBox has returned an error: {}".format(vboxmanage_error)) - return stdout_data.decode("utf-8", errors="ignore").splitlines() + return stdout_data.decode("utf-8", errors="ignore").splitlines() @asyncio.coroutine def get_list(self): diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index f369287b..428d880c 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -105,9 +105,10 @@ class VirtualBoxVM(BaseVM): results = yield from self.manager.execute("showvminfo", [self._vmname, "--machinereadable"]) for info in results: - name, value = info.split('=', 1) - if name == "VMState": - return value.strip('"') + if '=' in info: + name, value = info.split('=', 1) + if name == "VMState": + return value.strip('"') raise VirtualBoxError("Could not get VM state for {}".format(self._vmname)) @asyncio.coroutine @@ -139,6 +140,8 @@ class VirtualBoxVM(BaseVM): def create(self): yield from self._get_system_properties() + if "API version" not in self._system_properties: + raise VirtualBoxError("Can't access to VirtualBox API Version") if parse_version(self._system_properties["API version"]) < parse_version("4_3"): raise VirtualBoxError("The VirtualBox API version is lower than 4.3") log.info("VirtualBox VM '{name}' [{id}] created".format(name=self.name, id=self.id)) diff --git a/gns3server/modules/vpcs/__init__.py b/gns3server/modules/vpcs/__init__.py index eb6cbe11..6c577bef 100644 --- a/gns3server/modules/vpcs/__init__.py +++ b/gns3server/modules/vpcs/__init__.py @@ -62,9 +62,9 @@ class VPCS(BaseManager): """ vm = self.get_vm(vm_id) - i = self._used_mac_ids[vm_id] - self._free_mac_ids[vm.project.id].insert(0, i) if vm_id in self._used_mac_ids: + i = self._used_mac_ids[vm_id] + self._free_mac_ids[vm.project.id].insert(0, i) del self._used_mac_ids[vm_id] yield from super().close_vm(vm_id, *args, **kwargs) return vm diff --git a/gns3server/schemas/iou.py b/gns3server/schemas/iou.py index a2c6ec2b..743dfcfc 100644 --- a/gns3server/schemas/iou.py +++ b/gns3server/schemas/iou.py @@ -70,8 +70,12 @@ IOU_CREATE_SCHEMA = { "description": "Use default IOU values", "type": ["boolean", "null"] }, + "initial_config": { + "description": "Path to the initial configuration of IOU", + "type": ["string", "null"] + }, "initial_config_content": { - "description": "Initial configuration of the IOU", + "description": "Initial configuration of IOU", "type": ["string", "null"] }, "iourc_content": { @@ -123,8 +127,12 @@ IOU_UPDATE_SCHEMA = { "description": "Always up ethernet interface", "type": ["boolean", "null"] }, + "initial_config": { + "description": "Path to the initial configuration of IOU", + "type": ["string", "null"] + }, "initial_config_content": { - "description": "Initial configuration of the IOU", + "description": "Initial configuration of IOU", "type": ["string", "null"] }, "use_default_iou_values": { diff --git a/tests/modules/iou/test_iou_manager.py b/tests/modules/iou/test_iou_manager.py index 7817a297..84b3b7e2 100644 --- a/tests/modules/iou/test_iou_manager.py +++ b/tests/modules/iou/test_iou_manager.py @@ -17,7 +17,9 @@ import pytest +from unittest.mock import patch import uuid +import os from gns3server.modules.iou import IOU @@ -25,6 +27,15 @@ from gns3server.modules.iou.iou_error import IOUError from gns3server.modules.project_manager import ProjectManager +@pytest.fixture(scope="function") +def iou(port_manager): + # Cleanup the IOU object + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + return iou + + def test_get_application_id(loop, project, port_manager): # Cleanup the IOU object IOU._instance = None @@ -71,3 +82,8 @@ def test_get_application_id_no_id_available(loop, project, port_manager): vm_id = str(uuid.uuid4()) loop.run_until_complete(iou.create_vm("PC {}".format(i), project.id, vm_id)) assert iou.get_application_id(vm_id) == i + + +def test_get_images_directory(iou, tmpdir): + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + assert iou.get_images_directory() == str(tmpdir / "IOU") diff --git a/tests/modules/iou/test_iou_vm.py b/tests/modules/iou/test_iou_vm.py index 8ee3a21f..731f6974 100644 --- a/tests/modules/iou/test_iou_vm.py +++ b/tests/modules/iou/test_iou_vm.py @@ -82,7 +82,8 @@ def test_vm(project, manager): def test_vm_initial_config(project, manager): - vm = IOUVM("test", "00010203-0405-0607-0808-0a0b0c0d0e0f", project, manager, initial_config="hostname %h") + vm = IOUVM("test", "00010203-0405-0607-0808-0a0b0c0d0e0f", project, manager) + vm.initial_config = "hostname %h" assert vm.name == "test" assert vm.initial_config == "hostname test" assert vm.id == "00010203-0405-0607-0808-0a0b0c0d0e0f" diff --git a/tests/modules/test_manager.py b/tests/modules/test_manager.py index a9d32daf..19d360e4 100644 --- a/tests/modules/test_manager.py +++ b/tests/modules/test_manager.py @@ -22,6 +22,15 @@ from unittest.mock import patch from gns3server.modules.vpcs import VPCS +from gns3server.modules.iou import IOU + + +@pytest.fixture(scope="function") +def iou(port_manager): + IOU._instance = None + iou = IOU.instance() + iou.port_manager = port_manager + return iou def test_create_vm_new_topology(loop, project, port_manager): @@ -82,3 +91,33 @@ def test_create_vm_old_topology(loop, project, tmpdir, port_manager): vm_dir = os.path.join(project_dir, "project-files", "vpcs", vm.id) with open(os.path.join(vm_dir, "startup.vpc")) as f: assert f.read() == "1" + + +def test_get_abs_image_path(iou, tmpdir): + os.makedirs(str(tmpdir / "IOU")) + path1 = str(tmpdir / "test1.bin") + open(path1, 'w+').close() + + path2 = str(tmpdir / "IOU" / "test2.bin") + open(path2, 'w+').close() + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + assert iou.get_abs_image_path(path1) == path1 + assert iou.get_abs_image_path(path2) == path2 + assert iou.get_abs_image_path("test2.bin") == path2 + assert iou.get_abs_image_path("../test1.bin") == path1 + + +def test_get_relative_image_path(iou, tmpdir): + os.makedirs(str(tmpdir / "IOU")) + path1 = str(tmpdir / "test1.bin") + open(path1, 'w+').close() + + path2 = str(tmpdir / "IOU" / "test2.bin") + open(path2, 'w+').close() + + with patch("gns3server.config.Config.get_section_config", return_value={"images_path": str(tmpdir)}): + assert iou.get_relative_image_path(path1) == path1 + assert iou.get_relative_image_path(path2) == "test2.bin" + assert iou.get_relative_image_path("test2.bin") == "test2.bin" + assert iou.get_relative_image_path("../test1.bin") == path1