From 6460e94311be13098d7d8aaf409659480f2467d2 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Fri, 23 Jan 2015 16:38:46 -0700 Subject: [PATCH] More VirtualBox implementation. --- gns3server/handlers/virtualbox_handler.py | 150 +++++++++++++++--- gns3server/modules/port_manager.py | 6 +- .../modules/virtualbox/virtualbox_vm.py | 63 ++++---- gns3server/schemas/virtualbox.py | 96 +++++++++++ gns3server/schemas/vpcs.py | 4 +- 5 files changed, 256 insertions(+), 63 deletions(-) diff --git a/gns3server/handlers/virtualbox_handler.py b/gns3server/handlers/virtualbox_handler.py index 03a2e99e..b0c21334 100644 --- a/gns3server/handlers/virtualbox_handler.py +++ b/gns3server/handlers/virtualbox_handler.py @@ -17,6 +17,8 @@ from ..web.route import Route from ..schemas.virtualbox import VBOX_CREATE_SCHEMA +from ..schemas.virtualbox import VBOX_UPDATE_SCHEMA +from ..schemas.virtualbox import VBOX_NIO_SCHEMA from ..schemas.virtualbox import VBOX_OBJECT_SCHEMA from ..modules.virtualbox import VirtualBox @@ -31,7 +33,7 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox", status_codes={ - 201: "VirtualBox VM instance created", + 201: "Instance created", 400: "Invalid project UUID", 409: "Conflict" }, @@ -46,20 +48,81 @@ class VirtualBoxHandler: request.json.get("uuid"), request.json["vmname"], request.json["linked_clone"], - console=request.json.get("console")) + console=request.json.get("console"), + vbox_user=request.json.get("vbox_user")) response.set_status(201) response.json(vm) + @classmethod + @Route.get( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 200: "Success", + 404: "Instance doesn't exist" + }, + description="Get a VirtualBox VM instance", + output=VBOX_OBJECT_SCHEMA) + def show(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + response.json(vm) + + @classmethod + @Route.put( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 200: "Instance updated", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a VirtualBox VM instance", + input=VBOX_UPDATE_SCHEMA, + output=VBOX_OBJECT_SCHEMA) + def update(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + + for name, value in request.json.items(): + if hasattr(vm, name) and getattr(vm, name) != value: + setattr(vm, name, value) + + # TODO: FINISH UPDATE (adapters). + response.json(vm) + + @classmethod + @Route.delete( + r"/virtualbox/{uuid}", + parameters={ + "uuid": "Instance UUID" + }, + status_codes={ + 204: "Instance deleted", + 404: "Instance doesn't exist" + }, + description="Delete a VirtualBox VM instance") + def delete(request, response): + + yield from VirtualBox.instance().delete_vm(request.match_info["uuid"]) + response.set_status(204) + @classmethod @Route.post( r"/virtualbox/{uuid}/start", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance started", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance started", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Start a VirtualBox VM instance") def start(request, response): @@ -73,12 +136,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/stop", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance stopped", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance stopped", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Stop a VirtualBox VM instance") def stop(request, response): @@ -92,12 +155,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/suspend", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance suspended", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance suspended", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Suspend a VirtualBox VM instance") def suspend(request, response): @@ -111,12 +174,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/resume", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance resumed", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance resumed", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Resume a suspended VirtualBox VM instance") def suspend(request, response): @@ -130,12 +193,12 @@ class VirtualBoxHandler: @Route.post( r"/virtualbox/{uuid}/reload", parameters={ - "uuid": "VirtualBox VM instance UUID" + "uuid": "Instance UUID" }, status_codes={ - 204: "VirtualBox VM instance reloaded", - 400: "Invalid VirtualBox VM instance UUID", - 404: "VirtualBox VM instance doesn't exist" + 204: "Instance reloaded", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" }, description="Reload a VirtualBox VM instance") def suspend(request, response): @@ -144,3 +207,46 @@ class VirtualBoxHandler: vm = vbox_manager.get_vm(request.match_info["uuid"]) yield from vm.reload() response.set_status(204) + + @Route.post( + r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a VirtualBox VM instance", + input=VBOX_NIO_SCHEMA, + output=VBOX_NIO_SCHEMA) + def create_nio(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + nio = vbox_manager.create_nio(vm.vboxmanage_path, request.json) + vm.port_add_nio_binding(int(request.match_info["port_id"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/virtualbox/{uuid}/ports/{port_id:\d+}/nio", + parameters={ + "uuid": "Instance UUID", + "port_id": "ID of the port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid instance UUID", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a VirtualBox VM instance") + def delete_nio(request, response): + + vbox_manager = VirtualBox.instance() + vm = vbox_manager.get_vm(request.match_info["uuid"]) + vm.port_remove_nio_binding(int(request.match_info["port_id"])) + response.set_status(204) diff --git a/gns3server/modules/port_manager.py b/gns3server/modules/port_manager.py index b468dfce..b2316048 100644 --- a/gns3server/modules/port_manager.py +++ b/gns3server/modules/port_manager.py @@ -173,7 +173,8 @@ class PortManager: :param port: TCP port number """ - self._used_tcp_ports.remove(port) + if port in self._used_tcp_ports: + self._used_tcp_ports.remove(port) def get_free_udp_port(self): """ @@ -207,4 +208,5 @@ class PortManager: :param port: UDP port number """ - self._used_udp_ports.remove(port) + if port in self._used_udp_ports: + self._used_udp_ports.remove(port) diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 2bbe91c0..2ec67434 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -70,10 +70,16 @@ class VirtualBoxVM(BaseVM): self._adapter_start_index = 0 self._adapter_type = "Intel PRO/1000 MT Desktop (82540EM)" + if self._console is not None: + self._console = self._manager.port_manager.reserve_console_port(self._console) + else: + self._console = self._manager.port_manager.get_free_console_port() + def __json__(self): return {"name": self.name, "uuid": self.uuid, + "console": self.console, "project_uuid": self.project.uuid, "vmname": self.vmname, "linked_clone": self.linked_clone, @@ -299,6 +305,16 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] reloaded".format(name=self.name, uuid=self.uuid)) log.debug("Reload result: {}".format(result)) + @property + def vboxmanage_path(self): + """ + Returns the path to VBoxManage. + + :returns: path + """ + + return self._vboxmanage_path + @property def console(self): """ @@ -317,13 +333,10 @@ class VirtualBoxVM(BaseVM): :param console: console port (integer) """ - if console in self._allocated_console_ports: - raise VirtualBoxError("Console port {} is already used by another VirtualBox VM".format(console)) - - self._allocated_console_ports.remove(self._console) - self._console = console - self._allocated_console_ports.append(self._console) + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = self._manager.port_manager.reserve_console_port(console) log.info("VirtualBox VM '{name}' [{uuid}]: console port set to {port}".format(name=self.name, uuid=self.uuid, port=console)) @@ -369,12 +382,13 @@ class VirtualBoxVM(BaseVM): self.stop() - if self.console and self.console in self._allocated_console_ports: - self._allocated_console_ports.remove(self.console) + if self._console: + self._manager.port_manager.release_console_port(self._console) + self._console = None if self._linked_clone: hdd_table = [] - if os.path.exists(self._working_dir): + if os.path.exists(self.working_dir): hdd_files = yield from self._get_all_hdd_files() vm_info = self._get_vm_info() for entry, value in vm_info.items(): @@ -398,7 +412,7 @@ class VirtualBoxVM(BaseVM): if hdd_table: try: - hdd_info_file = os.path.join(self._working_dir, self._vmname, "hdd_info.json") + hdd_info_file = os.path.join(self.working_dir, self._vmname, "hdd_info.json") with open(hdd_info_file, "w") as f: # log.info("saving project: {}".format(path)) json.dump(hdd_table, f, indent=4) @@ -408,30 +422,6 @@ class VirtualBoxVM(BaseVM): log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, uuid=self.uuid)) - def delete(self): - """ - Deletes this VirtualBox VM & all files. - """ - - self.stop() - - if self.console: - self._allocated_console_ports.remove(self.console) - - if self._linked_clone: - self._execute("unregistervm", [self._vmname, "--delete"]) - - # try: - # shutil.rmtree(self._working_dir) - # except OSError as e: - # log.error("could not delete VirtualBox VM {name} [id={id}]: {error}".format(name=self._name, - # id=self._id, - # error=e)) - # return - - log.info("VirtualBox VM '{name}' [{uuid}] has been deleted (including associated files)".format(name=self.name, - uuid=self.uuid)) - @property def headless(self): """ @@ -501,8 +491,9 @@ class VirtualBoxVM(BaseVM): """ log.info("VirtualBox VM '{name}' [{uuid}] has set the VM name to '{vmname}'".format(name=self.name, uuid=self.uuid, vmname=vmname)) - if self._linked_clone: - self._modify_vm('--name "{}"'.format(vmname)) + # TODO: test linked clone + #if self._linked_clone: + # yield from self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname @property diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index 3beb7262..5248240d 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -53,11 +53,107 @@ VBOX_CREATE_SCHEMA = { "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", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, }, "additionalProperties": False, "required": ["name", "vmname", "linked_clone", "project_uuid"], } +VBOX_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VirtualBox VM instance", + "type": "object", + "properties": { + "name": { + "description": "VirtualBox VM instance name", + "type": "string", + "minLength": 1, + }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "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, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + }, + "additionalProperties": False, +} + +VBOX_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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + ], + "additionalProperties": True, + "required": ["type"] +} + VBOX_OBJECT_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "VirtualBox VM instance", diff --git a/gns3server/schemas/vpcs.py b/gns3server/schemas/vpcs.py index 437651bb..e4dae510 100644 --- a/gns3server/schemas/vpcs.py +++ b/gns3server/schemas/vpcs.py @@ -95,7 +95,6 @@ VPCS_NIO_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation to add a NIO for a VPCS instance", "type": "object", - "definitions": { "UDP": { "description": "UDP Network Input/Output", @@ -140,13 +139,12 @@ VPCS_NIO_SCHEMA = { "additionalProperties": False }, }, - "oneOf": [ {"$ref": "#/definitions/UDP"}, {"$ref": "#/definitions/TAP"}, ], "additionalProperties": True, - "required": ['type'] + "required": ["type"] } VPCS_OBJECT_SCHEMA = {