diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 842bb960..68297c25 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -25,6 +25,7 @@ from gns3server.handlers.api.dynamips_vm_handler import DynamipsVMHandler from gns3server.handlers.api.qemu_handler import QEMUHandler from gns3server.handlers.api.virtualbox_handler import VirtualBoxHandler from gns3server.handlers.api.vpcs_handler import VPCSHandler +from gns3server.handlers.api.vmware_handler import VMwareHandler from gns3server.handlers.api.config_handler import ConfigHandler from gns3server.handlers.api.server_handler import ServerHandler from gns3server.handlers.api.file_handler import FileHandler diff --git a/gns3server/handlers/api/virtualbox_handler.py b/gns3server/handlers/api/virtualbox_handler.py index 17d6ce66..f948fbe4 100644 --- a/gns3server/handlers/api/virtualbox_handler.py +++ b/gns3server/handlers/api/virtualbox_handler.py @@ -246,7 +246,7 @@ class VirtualBoxHandler: 404: "Instance doesn't exist" }, description="Resume a suspended VirtualBox VM instance") - def suspend(request, response): + def resume(request, response): vbox_manager = VirtualBox.instance() vm = vbox_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) diff --git a/gns3server/handlers/api/vmware_handler.py b/gns3server/handlers/api/vmware_handler.py new file mode 100644 index 00000000..e8bf35e0 --- /dev/null +++ b/gns3server/handlers/api/vmware_handler.py @@ -0,0 +1,229 @@ +# -*- 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.vmware import VMWARE_CREATE_SCHEMA +from ...schemas.vmware import VMWARE_UPDATE_SCHEMA +from ...schemas.vmware import VMWARE_OBJECT_SCHEMA +from ...modules.vmware import VMware +from ...modules.project_manager import ProjectManager + + +class VMwareHandler: + + """ + API entry points for VMware. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new VMware VM instance", + input=VMWARE_CREATE_SCHEMA, + output=VMWARE_OBJECT_SCHEMA) + def create(request, response): + + vmware_manager = VMware.instance() + vm = yield from vmware_manager.create_vm(request.json.pop("name"), + request.match_info["project_id"], + request.json.get("vm_id"), + request.json.pop("vmx_path"), + request.json.pop("linked_clone"), + console=request.json.get("console", None)) + + # for name, value in request.json.items(): + # if name != "vm_id": + # if hasattr(vm, name) and getattr(vm, name) != value: + # setattr(vm, name, value) + + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/vmware/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Success", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Get a VMware VM instance", + output=VMWARE_OBJECT_SCHEMA) + def show(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + response.json(vm) + + @classmethod + @Route.put( + r"/projects/{project_id}/vmware/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 200: "Instance updated", + 400: "Invalid request", + 404: "Instance doesn't exist", + 409: "Conflict" + }, + description="Update a VMware VM instance", + input=VMWARE_UPDATE_SCHEMA, + output=VMWARE_OBJECT_SCHEMA) + def update(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + + # for name, value in request.json.items(): + # if hasattr(vm, name) and getattr(vm, name) != value: + # setattr(vm, name, value) + + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/vmware/vms/{vm_id}", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Delete a VMware VM instance") + def delete(request, response): + + # check the project_id exists + ProjectManager.instance().get_project(request.match_info["project_id"]) + yield from VMware.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/start", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance started", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Start a VMware VM instance") + def start(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.start() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/stop", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance stopped", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Stop a VMware VM instance") + def stop(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.stop() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/suspend", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance suspended", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Suspend a VMware VM instance") + def suspend(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.suspend() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/resume", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance resumed", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Resume a suspended VMware VM instance") + def resume(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.resume() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/vmware/vms/{vm_id}/reload", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance" + }, + status_codes={ + 204: "Instance reloaded", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Reload a VMware VM instance") + def reload(request, response): + + vmware_manager = VMware.instance() + vm = vmware_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 928bb1a9..7c23503c 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -21,8 +21,9 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips from .qemu import Qemu +from .vmware import VMware -MODULES = [VPCS, VirtualBox, Dynamips, Qemu] +MODULES = [VPCS, VirtualBox, Dynamips, Qemu, VMware] if sys.platform.startswith("linux") or hasattr(sys, "_called_from_test") or os.environ.get("PYTEST_BUILD_DOCUMENTATION") == "1": diff --git a/gns3server/modules/vmware/__init__.py b/gns3server/modules/vmware/__init__.py new file mode 100644 index 00000000..6d876430 --- /dev/null +++ b/gns3server/modules/vmware/__init__.py @@ -0,0 +1,106 @@ +# -*- 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 . + +""" +VMware player/workstation server module. +""" + +import os +import sys +import shutil +import asyncio +import subprocess +import logging + +log = logging.getLogger(__name__) + +from ..base_manager import BaseManager +from .vmware_vm import VMwareVM +from .vmware_error import VMwareError + + +class VMware(BaseManager): + + _VM_CLASS = VMwareVM + + def __init__(self): + + super().__init__() + self._vmrun_path = None + self._host_type = "player" + + @property + def vmrun_path(self): + """ + Returns the path vmrun utility. + + :returns: path + """ + + return self._vmrun_path + + def find_vmrun(self): + + # look for vmrun + vmrun_path = self.config.get_section_config("VMware").get("vmrun_path") + if not vmrun_path: + if sys.platform.startswith("win"): + pass # TODO: use registry to find vmrun + elif sys.platform.startswith("darwin"): + vmrun_path = "/Applications/VMware Fusion.app/Contents/Library/vmrun" + else: + vmrun_path = shutil.which("vmrun") + + if not vmrun_path: + raise VMwareError("Could not find vmrun") + if not os.path.isfile(vmrun_path): + raise VMwareError("vmrun {} is not accessible".format(vmrun_path)) + if not os.access(vmrun_path, os.X_OK): + raise VMwareError("vmrun is not executable") + if os.path.basename(vmrun_path) not in ["vmrun", "vmrun.exe"]: + raise VMwareError("Invalid vmrun executable name {}".format(os.path.basename(vmrun_path))) + + self._vmrun_path = vmrun_path + return vmrun_path + + @asyncio.coroutine + def execute(self, subcommand, args, timeout=60, host_type=None): + + vmrun_path = self.vmrun_path + if not vmrun_path: + vmrun_path = self.find_vmrun() + if host_type is None: + host_type = self._host_type + command = [vmrun_path, "-T", host_type, subcommand] + command.extend(args) + log.debug("Executing vmrun with command: {}".format(command)) + try: + process = yield from asyncio.create_subprocess_exec(*command, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) + except (OSError, subprocess.SubprocessError) as e: + raise VMwareError("Could not execute vmrun: {}".format(e)) + + try: + stdout_data, _ = yield from asyncio.wait_for(process.communicate(), timeout=timeout) + except asyncio.TimeoutError: + raise VMwareError("vmrun has timed out after {} seconds!".format(timeout)) + + if process.returncode: + # vmrun print errors on stdout + vmrun_error = stdout_data.decode("utf-8", errors="ignore") + raise VMwareError("vmrun has returned an error: {}".format(vmrun_error)) + + return stdout_data.decode("utf-8", errors="ignore").splitlines() diff --git a/gns3server/modules/vmware/vmware_error.py b/gns3server/modules/vmware/vmware_error.py new file mode 100644 index 00000000..8a254030 --- /dev/null +++ b/gns3server/modules/vmware/vmware_error.py @@ -0,0 +1,27 @@ +# -*- 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 . + +""" +Custom exceptions for the VirtualBox module. +""" + +from ..vm_error import VMError + + +class VMwareError(VMError): + + pass diff --git a/gns3server/modules/vmware/vmware_vm.py b/gns3server/modules/vmware/vmware_vm.py new file mode 100644 index 00000000..1bc1feee --- /dev/null +++ b/gns3server/modules/vmware/vmware_vm.py @@ -0,0 +1,192 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + +""" +VMware VM instance. +""" + +import sys +import shlex +import re +import os +import tempfile +import json +import socket +import asyncio + +from pkg_resources import parse_version +from .vmware_error import VMwareError +from ..nios.nio_udp import NIOUDP +from ..base_vm import BaseVM + +import logging +log = logging.getLogger(__name__) + + +class VMwareVM(BaseVM): + + """ + VMware VM implementation. + """ + + def __init__(self, name, vm_id, project, manager, vmx_path, linked_clone, console=None): + + super().__init__(name, vm_id, project, manager, console=console) + + self._linked_clone = linked_clone + self._closed = False + + # VMware VM settings + self._headless = False + self._vmx_path = vmx_path + + def __json__(self): + + return {"name": self.name, + "vm_id": self.id, + "console": self.console, + "project_id": self.project.id, + "vmx_path": self.vmx_path, + "headless": self.headless} + + @asyncio.coroutine + def _control_vm(self, subcommand, *additional_args): + + args = [self._vmx_path] + args.extend(additional_args) + result = yield from self.manager.execute(subcommand, args) + log.debug("Control VM '{}' result: {}".format(subcommand, result)) + return result + + @asyncio.coroutine + def start(self): + """ + Starts this VMware VM. + """ + + if self._headless: + yield from self._control_vm("start", "nogui") + else: + yield from self._control_vm("start") + log.info("VMware VM '{name}' [{id}] started".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def stop(self): + """ + Stops this VMware VM. + """ + + yield from self._control_vm("stop") + log.info("VMware VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def suspend(self): + """ + Suspends this VMware VM. + """ + + yield from self._control_vm("suspend") + log.info("VMware VM '{name}' [{id}] stopped".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def resume(self): + """ + Resumes this VMware VM. + """ + + yield from self.start() + log.info("VMware VM '{name}' [{id}] resumed".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def reload(self): + """ + Reloads this VMware VM. + """ + + yield from self._control_vm("reset") + log.info("VMware VM '{name}' [{id}] reloaded".format(name=self.name, id=self.id)) + + @asyncio.coroutine + def close(self): + """ + Closes this VirtualBox VM. + """ + + if self._closed: + # VM is already closed + return + + log.debug("VMware VM '{name}' [{id}] is closing".format(name=self.name, id=self.id)) + if self._console: + self._manager.port_manager.release_tcp_port(self._console, self._project) + self._console = None + + #for adapter in self._ethernet_adapters.values(): + # if adapter is not None: + # for nio in adapter.ports.values(): + # if nio and isinstance(nio, NIOUDP): + # self.manager.port_manager.release_udp_port(nio.lport, self._project) + + yield from self.stop() + + log.info("VirtualBox VM '{name}' [{id}] closed".format(name=self.name, id=self.id)) + self._closed = True + + @property + def headless(self): + """ + Returns either the VM will start in headless mode + + :returns: boolean + """ + + return self._headless + + @headless.setter + def headless(self, headless): + """ + Sets either the VM will start in headless mode + + :param headless: boolean + """ + + if headless: + log.info("VMware VM '{name}' [{id}] has enabled the headless mode".format(name=self.name, id=self.id)) + else: + log.info("VMware VM '{name}' [{id}] has disabled the headless mode".format(name=self.name, id=self.id)) + self._headless = headless + + @property + def vmx_path(self): + """ + Returns the path to the vmx file. + + :returns: VMware vmx file + """ + + return self._vmx_path + + @vmx_path.setter + def vmx_path(self, vmx_path): + """ + Sets the path to the vmx file. + + :param vmx_path: VMware vmx file + """ + + log.info("VMware VM '{name}' [{id}] has set the vmx file path to '{vmx}'".format(name=self.name, id=self.id, vmx=vmx_path)) + self._vmx_path = vmx_path diff --git a/gns3server/schemas/vmware.py b/gns3server/schemas/vmware.py new file mode 100644 index 00000000..959bef5b --- /dev/null +++ b/gns3server/schemas/vmware.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2014 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 . + + +VMWARE_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new VMware VM instance", + "type": "object", + "properties": { + "vm_id": { + "description": "VMware VM instance identifier", + "type": "string", + "minLength": 36, + "maxLength": 36, + "pattern": "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$" + }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vmx_path": { + "description": "path to the vmx file", + "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, + "required": ["name", "vmx_path", "linked_clone"], +} + +VMWARE_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a VMware VM instance", + "type": "object", + "properties": { + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vmx_path": { + "description": "path to the vmx file", + "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, +} + +VMWARE_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "VMware VM instance", + "type": "object", + "properties": { + "name": { + "description": "VMware VM instance name", + "type": "string", + "minLength": 1, + }, + "vm_id": { + "description": "VMware 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}$" + }, + "project_id": { + "description": "Project 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}$" + }, + "vmx_path": { + "description": "path to the vmx file", + "type": "string", + "minLength": 1, + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "vm_id", "project_id"] +}