From 05c0efe39b66bb5856acf6196bbb105cab50d130 Mon Sep 17 00:00:00 2001 From: Jeremy Date: Thu, 22 Jan 2015 19:07:09 -0700 Subject: [PATCH] More VirtualBox work. --- .../modules/virtualbox/virtualbox_vm.py | 89 +++++++++++-------- gns3server/schemas/virtualbox.py | 17 ++++ tests/api/test_virtualbox.py | 7 ++ .../modules/virtualbox/test_virtualbox_vm.py | 77 ++++++++++++++++ 4 files changed, 151 insertions(+), 39 deletions(-) create mode 100644 tests/modules/virtualbox/test_virtualbox_vm.py diff --git a/gns3server/modules/virtualbox/virtualbox_vm.py b/gns3server/modules/virtualbox/virtualbox_vm.py index 6ef5c2d2..47c5d8e3 100644 --- a/gns3server/modules/virtualbox/virtualbox_vm.py +++ b/gns3server/modules/virtualbox/virtualbox_vm.py @@ -75,19 +75,17 @@ class VirtualBoxVM(BaseVM): def __json__(self): - # TODO: send more info - # {"name": self._name, - # "vmname": self._vmname, - # "adapters": self.adapters, + # TODO: send adapters info # "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} return {"name": self.name, "uuid": self.uuid, - "project_uuid": self.project.uuid} + "project_uuid": self.project.uuid, + "vmname": self.vmname, + "linked_clone": self.linked_clone, + "headless": self.headless, + "enable_remote_console": self.enable_remote_console} @asyncio.coroutine def _execute(self, subcommand, args, timeout=60): @@ -167,8 +165,7 @@ class VirtualBoxVM(BaseVM): args = shlex.split(params) yield from self._execute("modifyvm", [self._vmname] + args) - @asyncio.coroutine - def create(self): + def _find_vboxmanage(self): # look for VBoxManage self._vboxmanage_path = self.manager.config.get_section_config("VirtualBox").get("vboxmanage_path") @@ -185,22 +182,27 @@ class VirtualBoxVM(BaseVM): if not self._vboxmanage_path: raise VirtualBoxError("Could not find VBoxManage") + if not os.path.isfile(self._vboxmanage_path): + raise VirtualBoxError("VBoxManage {} is not accessible".format(self._vboxmanage_path)) if not os.access(self._vboxmanage_path, os.X_OK): raise VirtualBoxError("VBoxManage is not executable") + @asyncio.coroutine + def create(self): + + self._find_vboxmanage() yield from self._get_system_properties() 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}' [{uuid}] created".format(name=self.name, uuid=self.uuid)) if self._linked_clone: - # TODO: finish linked clone support if self.uuid and os.path.isdir(os.path.join(self.working_dir, self._vmname)): vbox_file = os.path.join(self.working_dir, self._vmname, self._vmname + ".vbox") - self._execute("registervm", [vbox_file]) - self._reattach_hdds() + yield from self._execute("registervm", [vbox_file]) + yield from self._reattach_hdds() else: - self._create_linked_clone() + yield from self._create_linked_clone() @asyncio.coroutine def start(self): @@ -323,14 +325,15 @@ class VirtualBoxVM(BaseVM): self._console = console self._allocated_console_ports.append(self._console) - log.info("VirtualBox VM {name} [id={id}]: console port set to {port}".format(name=self._name, - id=self._id, - port=console)) + log.info("VirtualBox VM '{name}' [{uuid}]: console port set to {port}".format(name=self.name, + uuid=self.uuid, + port=console)) + @asyncio.coroutine def _get_all_hdd_files(self): hdds = [] - properties = self._execute("list", ["hdds"]) + properties = yield from self._execute("list", ["hdds"]) for prop in properties: try: name, value = prop.split(':', 1) @@ -340,33 +343,32 @@ class VirtualBoxVM(BaseVM): hdds.append(value.strip()) return hdds + @asyncio.coroutine def _reattach_hdds(self): - 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") try: with open(hdd_info_file, "r") as f: - # log.info("loading project: {}".format(path)) hdd_table = json.load(f) except OSError as e: raise VirtualBoxError("Could not read HDD info file: {}".format(e)) for hdd_info in hdd_table: - hdd_file = os.path.join(self._working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) + hdd_file = os.path.join(self.working_dir, self._vmname, "Snapshots", hdd_info["hdd"]) if os.path.exists(hdd_file): log.debug("reattaching hdd {}".format(hdd_file)) - self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], - hdd_info["port"], - hdd_info["device"], - hdd_file)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium "{}"'.format(hdd_info["controller"], + hdd_info["port"], + hdd_info["device"], + hdd_file)) - def delete(self): + @asyncio.coroutine + def close(self): """ - Deletes this VirtualBox VM. + Closes this VirtualBox VM. """ self.stop() - if self._id in self._instances: - self._instances.remove(self._id) if self.console and self.console in self._allocated_console_ports: self._allocated_console_ports.remove(self.console) @@ -374,7 +376,7 @@ class VirtualBoxVM(BaseVM): if self._linked_clone: hdd_table = [] if os.path.exists(self._working_dir): - hdd_files = self._get_all_hdd_files() + hdd_files = yield from self._get_all_hdd_files() vm_info = self._get_vm_info() for entry, value in vm_info.items(): match = re.search("^([\s\w]+)\-(\d)\-(\d)$", entry) @@ -383,7 +385,7 @@ class VirtualBoxVM(BaseVM): port = match.group(2) device = match.group(3) if value in hdd_files: - self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) + yield from self._storage_attach('--storagectl "{}" --port {} --device {} --type hdd --medium none'.format(controller, port, device)) hdd_table.append( { "hdd": os.path.basename(value), @@ -404,17 +406,15 @@ class VirtualBoxVM(BaseVM): except OSError as e: raise VirtualBoxError("Could not write HDD info file: {}".format(e)) - log.info("VirtualBox VM {name} [id={id}] has been deleted".format(name=self._name, - id=self._id)) + log.info("VirtualBox VM '{name}' [{uuid}] closed".format(name=self.name, + uuid=self.uuid)) - def clean_delete(self): + def delete(self): """ Deletes this VirtualBox VM & all files. """ self.stop() - if self._id in self._instances: - self._instances.remove(self._id) if self.console: self._allocated_console_ports.remove(self.console) @@ -506,6 +506,16 @@ class VirtualBoxVM(BaseVM): self._modify_vm('--name "{}"'.format(vmname)) self._vmname = vmname + @property + def linked_clone(self): + """ + Returns either the VM is a linked clone. + + :returns: boolean + """ + + return self._linked_clone + @property def adapters(self): """ @@ -651,6 +661,7 @@ class VirtualBoxVM(BaseVM): args = [self._vmname, "--uartmode1", "server", pipe_name] yield from self._execute("modifyvm", args) + @asyncio.coroutine def _storage_attach(self, params): """ Change storage medium in this VM. @@ -659,7 +670,7 @@ class VirtualBoxVM(BaseVM): """ args = shlex.split(params) - self._execute("storageattach", [self._vmname] + args) + yield from self._execute("storageattach", [self._vmname] + args) @asyncio.coroutine def _get_nic_attachements(self, maximum_adapters): @@ -760,9 +771,9 @@ class VirtualBoxVM(BaseVM): "--options", "link", "--name", - self._name, + self.name, "--basefolder", - self._working_dir, + self.working_dir, "--register"] result = yield from self._execute("clonevm", args) diff --git a/gns3server/schemas/virtualbox.py b/gns3server/schemas/virtualbox.py index d272efe1..760516f6 100644 --- a/gns3server/schemas/virtualbox.py +++ b/gns3server/schemas/virtualbox.py @@ -82,6 +82,23 @@ VBOX_OBJECT_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}$" }, + "vmname": { + "description": "VirtualBox VM name (in VirtualBox itself)", + "type": "string", + "minLength": 1, + }, + "linked_clone": { + "description": "either the VM is a linked clone or not", + "type": "boolean" + }, + "enable_remote_console": { + "description": "enable the remote console", + "type": "boolean" + }, + "headless": { + "description": "headless mode", + "type": "boolean" + }, "console": { "description": "console TCP port", "minimum": 1, diff --git a/tests/api/test_virtualbox.py b/tests/api/test_virtualbox.py index 05dbfc0e..5b148b3d 100644 --- a/tests/api/test_virtualbox.py +++ b/tests/api/test_virtualbox.py @@ -70,3 +70,10 @@ def test_vbox_resume(server, vm): response = server.post("/virtualbox/{}/resume".format(vm["uuid"])) assert mock.called assert response.status == 204 + + +def test_vbox_reload(server, vm): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM.reload", return_value=True) as mock: + response = server.post("/virtualbox/{}/reload".format(vm["uuid"])) + assert mock.called + assert response.status == 204 diff --git a/tests/modules/virtualbox/test_virtualbox_vm.py b/tests/modules/virtualbox/test_virtualbox_vm.py new file mode 100644 index 00000000..a81e8cf3 --- /dev/null +++ b/tests/modules/virtualbox/test_virtualbox_vm.py @@ -0,0 +1,77 @@ +# -*- 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 . + +import pytest +import asyncio +import tempfile +from tests.utils import asyncio_patch + +from unittest.mock import patch, MagicMock +from gns3server.modules.virtualbox.virtualbox_vm import VirtualBoxVM +from gns3server.modules.virtualbox.virtualbox_error import VirtualBoxError +from gns3server.modules.virtualbox import VirtualBox + + +@pytest.fixture(scope="module") +def manager(port_manager): + m = VirtualBox.instance() + m.port_manager = port_manager + return m + + +@pytest.fixture(scope="function") +def vm(project, manager): + return VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + + +def test_vm(project, manager): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + assert vm.name == "test" + assert vm.uuid == "00010203-0405-0607-0809-0a0b0c0d0e0f" + assert vm.vmname == "test" + assert vm.linked_clone is False + + +@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": "/bin/test_fake"}) +def test_vm_invalid_vboxmanage_path(project, manager): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) + vm._find_vboxmanage() + + +tmpfile = tempfile.NamedTemporaryFile() +@patch("gns3server.config.Config.get_section_config", return_value={"vboxmanage_path": tmpfile.name}) +def test_vm_non_executable_vboxmanage_path(project, manager, loop): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0e", project, manager, "test", False) + vm._find_vboxmanage() + + +def test_vm_valid_virtualbox_api_version(loop, project, manager): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_3"]): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + loop.run_until_complete(asyncio.async(vm.create())) + + +def test_vm_invalid_virtualbox_api_version(loop, project, manager): + with asyncio_patch("gns3server.modules.virtualbox.virtualbox_vm.VirtualBoxVM._execute", return_value=["API version: 4_2"]): + with pytest.raises(VirtualBoxError): + vm = VirtualBoxVM("test", "00010203-0405-0607-0809-0a0b0c0d0e0f", project, manager, "test", False) + loop.run_until_complete(asyncio.async(vm.create())) + + +# TODO: find a way to test start, stop, suspend, resume and reload