From 7fff25a9a92403f333711520c258cd29fb184094 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Mon, 19 Jan 2015 18:30:57 -0700 Subject: [PATCH] UUID support for VMs. Basic VirtualBox support (create, start and stop). Some refactoring for BaseVM class. Updated CURL command in tests. --- gns3server/handlers/virtualbox_handler.py | 80 ++++ gns3server/handlers/vpcs_handler.py | 37 +- gns3server/modules/__init__.py | 3 +- gns3server/modules/base_manager.py | 56 +-- gns3server/modules/base_vm.py | 65 ++- gns3server/modules/project.py | 12 +- .../modules/virtualbox/virtualbox_vm.py | 184 +++++---- gns3server/modules/vpcs/vpcs_device.py | 56 ++- gns3server/schemas/virtualbox.py | 381 +----------------- gns3server/schemas/vpcs.py | 15 +- gns3server/server.py | 1 + tests/api/base.py | 5 +- tests/api/test_virtualbox.py | 41 ++ tests/api/test_vpcs.py | 66 ++- tests/conftest.py | 2 +- 15 files changed, 396 insertions(+), 608 deletions(-) create mode 100644 gns3server/handlers/virtualbox_handler.py create mode 100644 tests/api/test_virtualbox.py diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py new file mode 100644 index 00000000..2b8714dd --- /dev/null +++ b/gns3server/handlers/virtualbox_handler.py @@ -0,0 +1,80 @@ +# -*- 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 ..web.route import Route +from ..schemas.virtualbox import VBOX_CREATE_SCHEMA +from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA +from ..modules.virtualbox import VirtualBox + + +class VirtualBoxHandler: + """ + API entry points for VirtualBox. + """ + + @classmethod + @Route.post( + r"/virtualbox", + status_codes={ + 201: "VirtualBox VM instance created", + 409: "Conflict" + }, + description="Create a new VirtualBox VM instance", + input=VBOX_CREATE_SCHEMA, + output=VBOX_OBJECT_SCHEMA) + def create(request, response): + + vbox_manager = VirtualBox.instance() + vm = yield from vbox_manager.create_vm(request.json["name"], request.json.get("uuid")) + response.json({"name": vm.name, + "uuid": vm.uuid}) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/start", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance started", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Start a VirtualBox VM instance") + def create(request, response): + + vbox_manager = VirtualBox.instance() + yield from vbox_manager.start_vm(request.match_info["uuid"]) + response.json({}) + + @classmethod + @Route.post( + r"/virtualbox/{uuid}/stop", + parameters={ + "uuid": "VirtualBox VM instance UUID" + }, + status_codes={ + 204: "VirtualBox VM instance stopped", + 400: "Invalid VirtualBox VM instance UUID", + 404: "VirtualBox VM instance doesn't exist" + }, + description="Stop a VirtualBox VM instance") + def create(request, response): + + vbox_manager = VirtualBox.instance() + yield from vbox_manager.stop_vm(request.match_info["uuid"]) + response.json({}) diff --git a/gns3server/handlers/vpcs_handler.py b/gns3server/handlers/vpcs_handler.py index 4719f0b0..28e72331 100644 --- a/gns3server/handlers/vpcs_handler.py +++ b/gns3server/handlers/vpcs_handler.py @@ -22,7 +22,7 @@ from ..schemas.vpcs import VPCS_NIO_SCHEMA from ..modules.vpcs import VPCS -class VPCSHandler(object): +class VPCSHandler: """ API entry points for VPCS. """ @@ -40,53 +40,56 @@ class VPCSHandler(object): def create(request, response): vpcs = VPCS.instance() - vm = yield from vpcs.create_vm(request.json["name"]) + vm = yield from vpcs.create_vm(request.json["name"], request.json.get("uuid")) response.json({"name": vm.name, - "id": vm.id, + "uuid": vm.uuid, "console": vm.console}) @classmethod @Route.post( - r"/vpcs/{id:\d+}/start", + r"/vpcs/{uuid}/start", parameters={ - "id": "VPCS instance ID" + "uuid": "VPCS instance UUID" }, status_codes={ 204: "VPCS instance started", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Start a VPCS instance") def create(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.start_vm(int(request.match_info["id"])) + yield from vpcs_manager.start_vm(request.match_info["uuid"]) response.json({}) @classmethod @Route.post( - r"/vpcs/{id:\d+}/stop", + r"/vpcs/{uuid}/stop", parameters={ - "id": "VPCS instance ID" + "uuid": "VPCS instance UUID" }, status_codes={ 204: "VPCS instance stopped", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Stop a VPCS instance") def create(request, response): vpcs_manager = VPCS.instance() - yield from vpcs_manager.stop_vm(int(request.match_info["id"])) + yield from vpcs_manager.stop_vm(request.match_info["uuid"]) response.json({}) @Route.post( - r"/vpcs/{id:\d+}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id}/nio", parameters={ - "id": "VPCS instance ID", + "uuid": "VPCS instance UUID", "port_id": "Id of the port where the nio should be add" }, status_codes={ 201: "NIO created", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Add a NIO to a VPCS", @@ -95,26 +98,26 @@ class VPCSHandler(object): def create_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(int(request.match_info["id"])) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_add_nio_binding(int(request.match_info["port_id"]), request.json) - response.json(nio) @classmethod @Route.delete( - r"/vpcs/{id:\d+}/ports/{port_id}/nio", + r"/vpcs/{uuid}/ports/{port_id}/nio", parameters={ - "id": "VPCS instance ID", - "port_id": "Id of the port where the nio should be remove" + "uuid": "VPCS instance UUID", + "port_id": "ID of the port where the nio should be removed" }, status_codes={ 200: "NIO deleted", + 400: "Invalid VPCS instance UUID", 404: "VPCS instance doesn't exist" }, description="Remove a NIO from a VPCS") def delete_nio(request, response): vpcs_manager = VPCS.instance() - vm = vpcs_manager.get_vm(int(request.match_info["id"])) + vm = vpcs_manager.get_vm(request.match_info["uuid"]) nio = vm.port_remove_nio_binding(int(request.match_info["port_id"])) response.json({}) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 5fc0b897..43b95792 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -17,8 +17,9 @@ import sys from .vpcs import VPCS +from .virtualbox import VirtualBox -MODULES = [VPCS] +MODULES = [VPCS, VirtualBox] #if sys.platform.startswith("linux"): # # IOU runs only on Linux diff --git a/gns3server/modules/base_manager.py b/gns3server/modules/base_manager.py index 42fb1576..71f78d2b 100644 --- a/gns3server/modules/base_manager.py +++ b/gns3server/modules/base_manager.py @@ -19,7 +19,7 @@ import asyncio import aiohttp -from .vm_error import VMError +from uuid import UUID, uuid4 class BaseManager: @@ -63,44 +63,52 @@ class BaseManager: @classmethod @asyncio.coroutine # FIXME: why coroutine? def destroy(cls): + cls._instance = None - def get_vm(self, vm_id): + def get_vm(self, uuid): """ Returns a VM instance. - :param vm_id: VM identifier + :param uuid: VM UUID :returns: VM instance """ - if vm_id not in self._vms: - raise aiohttp.web.HTTPNotFound(text="ID {} doesn't exist".format(vm_id)) - return self._vms[vm_id] + try: + UUID(uuid, version=4) + except ValueError: + raise aiohttp.web.HTTPBadRequest(text="{} is not a valid UUID".format(uuid)) + + if uuid not in self._vms: + raise aiohttp.web.HTTPNotFound(text="UUID {} doesn't exist".format(uuid)) + return self._vms[uuid] @asyncio.coroutine - def create_vm(self, vmname, identifier=None): - if not identifier: - for i in range(1, 1024): - if i not in self._vms: - identifier = i - break - if identifier == 0: - raise VMError("Maximum number of VM instances reached") - else: - if identifier in self._vms: - raise VMError("VM identifier {} is already used by another VM instance".format(identifier)) - vm = self._VM_CLASS(vmname, identifier, self) - yield from vm.wait_for_creation() - self._vms[vm.id] = vm + def create_vm(self, name, uuid=None): + + #TODO: support for old projects with normal IDs. + + #TODO: supports specific args: pass kwargs to VM_CLASS? + + if not uuid: + uuid = str(uuid4()) + + vm = self._VM_CLASS(name, uuid, self) + future = vm.create() + if isinstance(future, asyncio.Future): + yield from future + self._vms[vm.uuid] = vm return vm @asyncio.coroutine - def start_vm(self, vm_id): - vm = self.get_vm(vm_id) + def start_vm(self, uuid): + + vm = self.get_vm(uuid) yield from vm.start() @asyncio.coroutine - def stop_vm(self, vm_id): - vm = self.get_vm(vm_id) + def stop_vm(self, uuid): + + vm = self.get_vm(uuid) yield from vm.stop() diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index 17aa103b..181faa2d 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -15,9 +15,6 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . - -import asyncio -from .vm_error import VMError from ..config import Config import logging @@ -26,64 +23,62 @@ log = logging.getLogger(__name__) class BaseVM: - def __init__(self, name, identifier, manager): + def __init__(self, name, uuid, manager): self._name = name - self._id = identifier - self._created = asyncio.Future() + self._uuid = uuid self._manager = manager self._config = Config.instance() - asyncio.async(self._create()) - log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, - name=self._name, - id=self._id)) #TODO: When delete release console ports - @property - def id(self): + def name(self): """ - Returns the unique ID for this VM. + Returns the name for this VM. - :returns: id (integer) + :returns: name """ - return self._id + return self._name + + @name.setter + def name(self, new_name): + """ + Sets the name of this VM. + + :param new_name: name + """ + + self._name = new_name @property - def name(self): + def uuid(self): """ - Returns the name for this VM. + Returns the UUID for this VM. - :returns: name (string) + :returns: uuid (string) """ - return self._name + return self._uuid - @asyncio.coroutine - def _execute(self, command): + @property + def manager(self): """ - Called when we receive an event. + Returns the manager for this VM. + + :returns: instance of manager """ - raise NotImplementedError + return self._manager - @asyncio.coroutine - def _create(self): + def create(self): """ - Called when the run module is created and ready to receive - commands. It's asynchronous. + Creates the VM. """ - self._created.set_result(True) - log.info("{type} device {name} [id={id}] has been created".format(type=self.__class__.__name__, - name=self._name, - id=self._id)) - def wait_for_creation(self): - return self._created + return - @asyncio.coroutine def start(self): """ Starts the VM process. @@ -91,8 +86,6 @@ class BaseVM: raise NotImplementedError - - @asyncio.coroutine def stop(self): """ Starts the VM process. diff --git a/gns3server/modules/project.py b/gns3server/modules/project.py index 289b18a2..9be2f740 100644 --- a/gns3server/modules/project.py +++ b/gns3server/modules/project.py @@ -43,13 +43,23 @@ class Project: self._path = os.path.join(self._location, self._uuid) if os.path.exists(self._path) is False: os.mkdir(self._path) - os.mkdir(os.path.join(self._path, 'files')) + os.mkdir(os.path.join(self._path, "files")) @property def uuid(self): return self._uuid + @property + def location(self): + + return self._location + + @property + def path(self): + + return self._path + def __json__(self): return { diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index efebb34a..12dfb934 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -28,11 +28,13 @@ import tempfile import json import socket import time +import asyncio from .virtualbox_error import VirtualBoxError from ..adapters.ethernet_adapter import EthernetAdapter from ..attic import find_unused_port from .telnet_server import TelnetServer +from ..base_vm import BaseVM if sys.platform.startswith('win'): import msvcrt @@ -42,55 +44,32 @@ import logging log = logging.getLogger(__name__) -class VirtualBoxVM(object): +class VirtualBoxVM(BaseVM): """ VirtualBox VM implementation. - - :param vboxmanage_path: path to the VBoxManage tool - :param name: name of this VirtualBox VM - :param vmname: name of this VirtualBox VM in VirtualBox itself - :param linked_clone: flag if a linked clone must be created - :param working_dir: path to a working directory - :param vbox_id: VirtalBox VM instance ID - :param console: TCP console port - :param console_host: IP address to bind for console connections - :param console_start_port_range: TCP console port range start - :param console_end_port_range: TCP console port range end """ _instances = [] _allocated_console_ports = [] - def __init__(self, - vboxmanage_path, - vbox_user, - name, - vmname, - linked_clone, - working_dir, - vbox_id=None, - console=None, - console_host="0.0.0.0", - console_start_port_range=4512, - console_end_port_range=5000): - - if not vbox_id: - self._id = 0 - for identifier in range(1, 1024): - if identifier not in self._instances: - self._id = identifier - self._instances.append(self._id) - break - - if self._id == 0: - raise VirtualBoxError("Maximum number of VirtualBox VM instances reached") + def __init__(self, name, uuid, manager): + + super().__init__(name, uuid, manager) + + self._system_properties = {} + + #FIXME: harcoded values + if sys.platform.startswith("win"): + self._vboxmanage_path = r"C:\Program Files\Oracle\VirtualBox\VBoxManage.exe" else: - if vbox_id in self._instances: - raise VirtualBoxError("VirtualBox identifier {} is already used by another VirtualBox VM instance".format(vbox_id)) - self._id = vbox_id - self._instances.append(self._id) + self._vboxmanage_path = "/usr/bin/vboxmanage" + + self._queue = asyncio.Queue() + self._created = asyncio.Future() + self._worker = asyncio.async(self._run()) + + return - self._name = name self._linked_clone = linked_clone self._working_dir = None self._command = [] @@ -158,66 +137,99 @@ class VirtualBoxVM(object): log.info("VirtualBox VM {name} [id={id}] has been created".format(name=self._name, id=self._id)) - def defaults(self): - """ - Returns all the default attribute values for this VirtualBox VM. + @asyncio.coroutine + def _execute(self, subcommand, args, timeout=60): - :returns: default values (dictionary) - """ + command = [self._vboxmanage_path, "--nologo", subcommand] + command.extend(args) + try: + 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)) - vbox_defaults = {"name": self._name, - "vmname": self._vmname, - "adapters": self.adapters, - "adapter_start_index": self._adapter_start_index, - "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", - "console": self._console, - "enable_remote_console": self._enable_remote_console, - "headless": self._headless} + 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)) - return vbox_defaults + if process.returncode: + # only the first line of the output is useful + vboxmanage_error = stderr_data.decode("utf-8", errors="ignore").splitlines()[0] + raise VirtualBoxError(vboxmanage_error) - @property - def id(self): - """ - Returns the unique ID for this VirtualBox VM. + return stdout_data.decode("utf-8", errors="ignore").splitlines() - :returns: id (integer) - """ + @asyncio.coroutine + def _get_system_properties(self): + + properties = yield from self._execute("list", ["systemproperties"]) + for prop in properties: + try: + name, value = prop.split(':', 1) + except ValueError: + continue + self._system_properties[name.strip()] = value.strip() - return self._id + @asyncio.coroutine + def _run(self): - @classmethod - def reset(cls): - """ - Resets allocated instance list. - """ + try: + yield from self._get_system_properties() + self._created.set_result(True) + except VirtualBoxError as e: + self._created.set_exception(e) + return + + while True: + future, subcommand, args = yield from self._queue.get() + try: + yield from self._execute(subcommand, args) + future.set_result(True) + except VirtualBoxError as e: + future.set_exception(e) - cls._instances.clear() - cls._allocated_console_ports.clear() + def create(self): - @property - def name(self): - """ - Returns the name of this VirtualBox VM. + return self._created - :returns: name - """ + def _put(self, item): - return self._name + try: + self._queue.put_nowait(item) + except asyncio.qeues.QueueFull: + raise VirtualBoxError("Queue is full") - @name.setter - def name(self, new_name): + def start(self): + + args = [self._name] + future = asyncio.Future() + self._put((future, "startvm", args)) + return future + + def stop(self): + + args = [self._name, "poweroff"] + future = asyncio.Future() + self._put((future, "controlvm", args)) + return future + + def defaults(self): """ - Sets the name of this VirtualBox VM. + Returns all the default attribute values for this VirtualBox VM. - :param new_name: name + :returns: default values (dictionary) """ - log.info("VirtualBox VM {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) + vbox_defaults = {"name": self._name, + "vmname": self._vmname, + "adapters": self.adapters, + "adapter_start_index": self._adapter_start_index, + "adapter_type": "Intel PRO/1000 MT Desktop (82540EM)", + "console": self._console, + "enable_remote_console": self._enable_remote_console, + "headless": self._headless} - self._name = new_name + return vbox_defaults @property def working_dir(self): @@ -540,7 +552,7 @@ class VirtualBoxVM(object): id=self._id, adapter_type=adapter_type)) - def _execute(self, subcommand, args, timeout=60): + def _old_execute(self, subcommand, args, timeout=60): """ Executes a command with VBoxManage. @@ -831,7 +843,7 @@ class VirtualBoxVM(object): self._serial_pipe.close() self._serial_pipe = None - def start(self): + def old_start(self): """ Starts this VirtualBox VM. """ @@ -864,7 +876,7 @@ class VirtualBoxVM(object): if self._enable_remote_console: self._start_remote_console() - def stop(self): + def old_stop(self): """ Stops this VirtualBox VM. """ diff --git a/gns3server/modules/vpcs/vpcs_device.py b/gns3server/modules/vpcs/vpcs_device.py index 9d36c305..eed301c3 100644 --- a/gns3server/modules/vpcs/vpcs_device.py +++ b/gns3server/modules/vpcs/vpcs_device.py @@ -47,14 +47,14 @@ class VPCSDevice(BaseVM): VPCS device implementation. :param name: name of this VPCS device - :param vpcs_id: VPCS instance ID + :param uuid: VPCS instance UUID :param manager: parent VM Manager :param working_dir: path to a working directory :param console: TCP console port """ - def __init__(self, name, vpcs_id, manager, working_dir=None, console=None): + def __init__(self, name, uuid, manager, working_dir=None, console=None): - super().__init__(name, vpcs_id, manager) + super().__init__(name, uuid, manager) # TODO: Hardcodded for testing #self._working_dir = working_dir @@ -120,17 +120,8 @@ class VPCSDevice(BaseVM): return self._console - @property - def name(self): - """ - Returns the name of this VPCS device. - - :returns: name - """ - - return self._name - - @name.setter + #FIXME: correct way to subclass a property? + @BaseVM.name.setter def name(self, new_name): """ Sets the name of this VPCS device. @@ -151,10 +142,10 @@ class VPCSDevice(BaseVM): except OSError as e: raise VPCSError("Could not amend the configuration {}: {}".format(config_path, e)) - log.info("VPCS {name} [id={id}]: renamed to {new_name}".format(name=self._name, - id=self._id, - new_name=new_name)) - self._name = new_name + log.info("VPCS {name} [{uuid}]: renamed to {new_name}".format(name=self._name, + uuid=self.uuid, + new_name=new_name)) + BaseVM.name = new_name def _check_vpcs_version(self): """ @@ -197,7 +188,7 @@ class VPCSDevice(BaseVM): stderr=subprocess.STDOUT, cwd=self._working_dir, creationflags=flags) - log.info("VPCS instance {} started PID={}".format(self._id, self._process.pid)) + log.info("VPCS instance {} started PID={}".format(self.name, self._process.pid)) self._started = True except (OSError, subprocess.SubprocessError) as e: vpcs_stdout = self.read_vpcs_stdout() @@ -212,7 +203,7 @@ class VPCSDevice(BaseVM): # stop the VPCS process if self.is_running(): - log.info("stopping VPCS instance {} PID={}".format(self._id, self._process.pid)) + log.info("stopping VPCS instance {} PID={}".format(self.name, self._process.pid)) if sys.platform.startswith("win32"): self._process.send_signal(signal.CTRL_BREAK_EVENT) else: @@ -283,10 +274,10 @@ class VPCSDevice(BaseVM): self._ethernet_adapter.add_nio(port_id, nio) - log.info("VPCS {name} [id={id}]: {nio} added to port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) + log.info("VPCS {name} {uuid}]: {nio} added to port {port_id}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_id=port_id)) return nio def port_remove_nio_binding(self, port_id): @@ -304,10 +295,10 @@ class VPCSDevice(BaseVM): nio = self._ethernet_adapter.get_nio(port_id) self._ethernet_adapter.remove_nio(port_id) - log.info("VPCS {name} [id={id}]: {nio} removed from port {port_id}".format(name=self._name, - id=self._id, - nio=nio, - port_id=port_id)) + log.info("VPCS {name} [{uuid}]: {nio} removed from port {port_id}".format(name=self._name, + uuid=self.uuid, + nio=nio, + port_id=port_id)) return nio def _build_command(self): @@ -364,7 +355,8 @@ class VPCSDevice(BaseVM): command.extend(["-e"]) command.extend(["-d", nio.tap_device]) - command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset + #FIXME: find workaround + #command.extend(["-m", str(self._id)]) # the unique ID is used to set the MAC address offset command.extend(["-i", "1"]) # option to start only one VPC instance command.extend(["-F"]) # option to avoid the daemonization of VPCS if self._script_file: @@ -390,6 +382,6 @@ class VPCSDevice(BaseVM): """ self._script_file = script_file - log.info("VPCS {name} [id={id}]: script_file set to {config}".format(name=self._name, - id=self._id, - config=self._script_file)) + log.info("VPCS {name} [{uuid}]: script_file set to {config}".format(name=self._name, + uuid=self.uuid, + config=self._script_file)) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 67c0568c..1dea7163 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -36,69 +36,37 @@ VBOX_CREATE_SCHEMA = { "type": "boolean" }, "vbox_id": { - "description": "VirtualBox VM instance ID", + "description": "VirtualBox VM instance ID (for project created before GNS3 1.3)", "type": "integer" }, - "console": { - "description": "console TCP port", - "minimum": 1, - "maximum": 65535, - "type": "integer" + "uuid": { + "description": "VirtualBox VM instance UUID", + "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", "vmname"], } -VBOX_DELETE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_UPDATE_SCHEMA = { +VBOX_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to update a VirtualBox VM instance", + "description": "VirtualBox VM instance", "type": "object", "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, "name": { "description": "VirtualBox VM instance name", "type": "string", "minLength": 1, }, - "vmname": { - "description": "VirtualBox VM name (in VirtualBox itself)", + "uuid": { + "description": "VirtualBox VM instance UUID", "type": "string", - "minLength": 1, - }, - "adapters": { - "description": "number of adapters", - "type": "integer", - "minimum": 1, - "maximum": 36, # maximum given by the ICH9 chipset in VirtualBox - }, - "adapter_start_index": { - "description": "adapter index from which to start using adapters", - "type": "integer", - "minimum": 0, - "maximum": 35, # maximum given by the ICH9 chipset in VirtualBox - }, - "adapter_type": { - "description": "VirtualBox adapter type", - "type": "string", - "minLength": 1, + "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}$" }, "console": { "description": "console TCP port", @@ -106,327 +74,8 @@ VBOX_UPDATE_SCHEMA = { "maximum": 65535, "type": "integer" }, - "enable_remote_console": { - "description": "enable the remote console", - "type": "boolean" - }, - "headless": { - "description": "headless mode", - "type": "boolean" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_START_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_STOP_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_SUSPEND_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to suspend a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_RELOAD_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to reload a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id"] -} - -VBOX_ALLOCATE_UDP_PORT_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to allocate an UDP port for a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - }, - "additionalProperties": False, - "required": ["id", "port_id"] -} - -VBOX_ADD_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to add a NIO for a VirtualBox VM 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": "VirtualBox VM instance ID", - "type": "integer" - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "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"] -} - - -VBOX_DELETE_NIO_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to delete a NIO for a VirtualBox VM instance", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - }, - "additionalProperties": False, - "required": ["id", "port"] -} - -VBOX_START_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to start a packet capture on a VirtualBox VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, - "capture_file_name": { - "description": "Capture file name", - "type": "string", - "minLength": 1, - }, - }, - "additionalProperties": False, - "required": ["id", "port", "port_id", "capture_file_name"] -} - -VBOX_STOP_CAPTURE_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "description": "Request validation to stop a packet capture on a VirtualBox VM instance port", - "type": "object", - "properties": { - "id": { - "description": "VirtualBox VM instance ID", - "type": "integer" - }, - "port": { - "description": "Port number", - "type": "integer", - "minimum": 0, - "maximum": 36 # maximum given by the ICH9 chipset in VirtualBox - }, - "port_id": { - "description": "Unique port identifier for the VirtualBox VM instance", - "type": "integer" - }, }, "additionalProperties": False, - "required": ["id", "port", "port_id"] + "required": ["name", "uuid"] } diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index c4b7c71c..275320de 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -26,8 +26,8 @@ VPCS_CREATE_SCHEMA = { "type": "string", "minLength": 1, }, - "id": { - "description": "VPCS device instance ID", + "vpcs_id": { + "description": "VPCS device instance ID (for project created before GNS3 1.3)", "type": "integer" }, "uuid": { @@ -117,9 +117,12 @@ VPCS_OBJECT_SCHEMA = { "type": "string", "minLength": 1, }, - "id": { - "description": "VPCS device instance ID", - "type": "integer" + "uuid": { + "description": "VPCS device UUID", + "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}$" }, "console": { "description": "console TCP port", @@ -129,6 +132,6 @@ VPCS_OBJECT_SCHEMA = { }, }, "additionalProperties": False, - "required": ["name", "id", "console"] + "required": ["name", "uuid", "console"] } diff --git a/gns3server/server.py b/gns3server/server.py index 0db04449..d476a003 100644 --- a/gns3server/server.py +++ b/gns3server/server.py @@ -35,6 +35,7 @@ from .modules.port_manager import PortManager #TODO: get rid of * have something generic to automatically import handlers so the routes can be found from gns3server.handlers import * +from gns3server.handlers.virtualbox_handler import VirtualBoxHandler import logging log = logging.getLogger(__name__) diff --git a/tests/api/base.py b/tests/api/base.py index 10a432a4..b63b7674 100644 --- a/tests/api/base.py +++ b/tests/api/base.py @@ -25,6 +25,7 @@ import pytest from aiohttp import web import aiohttp + from gns3server.web.route import Route #TODO: get rid of * from gns3server.handlers import * @@ -95,7 +96,7 @@ class Query: if path is None: return with open(self._example_file_path(method, path), 'w+') as f: - f.write("curl -i -x{} 'http://localhost:8000{}'".format(method, path)) + f.write("curl -i -X {} 'http://localhost:8000{}'".format(method, path)) if body: f.write(" -d '{}'".format(re.sub(r"\n", "", json.dumps(json.loads(body), sort_keys=True)))) f.write("\n\n") @@ -116,7 +117,7 @@ class Query: def _example_file_path(self, method, path): path = re.sub('[^a-z0-9]', '', path) - return "docs/api/examples/{}_{}.txt".format(method.lower(), path) + return "docs/api/examples/{}_{}.txt".format(method.lower(), path) # FIXME: cannot find path when running tests def _get_unused_port(): diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py new file mode 100644 index 00000000..a73700f9 --- /dev/null +++ b/tests/api/test_virtualbox.py @@ -0,0 +1,41 @@ +# -*- 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 tests.utils import asyncio_patch + + +@asyncio_patch("gns3server.modules.VirtualBox.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") +def test_vbox_create(server): + response = server.post("/virtualbox", {"name": "VM1"}, example=False) + assert response.status == 200 + assert response.route == "/virtualbox" + assert response.json["name"] == "VM1" + assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" + + +@asyncio_patch("gns3server.modules.VirtualBox.start_vm", return_value=True) +def test_vbox_start(server): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start", {}, example=False) + assert response.status == 204 + assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/start" + + +@asyncio_patch("gns3server.modules.VirtualBox.stop_vm", return_value=True) +def test_vbox_stop(server): + response = server.post("/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop", {}, example=False) + assert response.status == 204 + assert response.route == "/virtualbox/61d61bdd-aa7d-4912-817f-65a9eb54d3ab/stop" diff --git a/tests/api/test_vpcs.py b/tests/api/test_vpcs.py index 9dbbf94c..12244d3f 100644 --- a/tests/api/test_vpcs.py +++ b/tests/api/test_vpcs.py @@ -15,55 +15,49 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see . -from tests.api.base import server, loop from tests.utils import asyncio_patch -from gns3server import modules from unittest.mock import patch -@asyncio_patch('gns3server.modules.VPCS.create_vm', return_value=84) + +@asyncio_patch("gns3server.modules.VPCS.create_vm", return_value="61d61bdd-aa7d-4912-817f-65a9eb54d3ab") def test_vpcs_create(server): - response = server.post('/vpcs', {'name': 'PC TEST 1'}, example=False) + response = server.post("/vpcs", {"name": "PC TEST 1"}, example=False) assert response.status == 200 - assert response.route == '/vpcs' - assert response.json['name'] == 'PC TEST 1' - assert response.json['id'] == 84 + assert response.route == "/vpcs" + assert response.json["name"] == "PC TEST 1" + assert response.json["uuid"] == "61d61bdd-aa7d-4912-817f-65a9eb54d3ab" +#FIXME def test_vpcs_nio_create_udp(server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_udp', - 'lport': 4242, - 'rport': 4343, - 'rhost': '127.0.0.1' - }, - example=True) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - assert response.json['type'] == 'nio_udp' + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.json["type"] == "nio_udp" + @patch("gns3server.modules.vpcs.vpcs_device.has_privileged_access", return_value=True) def test_vpcs_nio_create_tap(mock, server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_tap', - 'tap_device': 'test', - }) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_tap", + "tap_device": "test"}) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - assert response.json['type'] == 'nio_tap' + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" + assert response.json["type"] == "nio_tap" + +#FIXME def test_vpcs_delete_nio(server): - vm = server.post('/vpcs', {'name': 'PC TEST 1'}) - response = server.post('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), { - 'type': 'nio_udp', - 'lport': 4242, - 'rport': 4343, - 'rhost': '127.0.0.1' - }, - ) - response = server.delete('/vpcs/{}/ports/0/nio'.format(vm.json["id"]), example=True) + vm = server.post("/vpcs", {"name": "PC TEST 1"}) + response = server.post("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/vpcs/{}/ports/0/nio".format(vm.json["uuid"]), example=True) assert response.status == 200 - assert response.route == '/vpcs/{id:\d+}/ports/{port_id}/nio' - - + assert response.route == "/vpcs/{uuid}/ports/{port_id}/nio" diff --git a/tests/conftest.py b/tests/conftest.py index a7ca89ef..3c62baca 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,6 @@ def server(request): cwd = os.path.dirname(os.path.abspath(__file__)) server_script = os.path.join(cwd, "../gns3server/main.py") process = subprocess.Popen([sys.executable, server_script, "--port=8000"]) - time.sleep(1) # give some time for the process to start + #time.sleep(1) # give some time for the process to start request.addfinalizer(process.terminate) return process