diff --git a/gns3server/handlers/__init__.py b/gns3server/handlers/__init__.py index 03e6dcab..5d558245 100644 --- a/gns3server/handlers/__init__.py +++ b/gns3server/handlers/__init__.py @@ -5,4 +5,5 @@ __all__ = ["version_handler", "virtualbox_handler", "dynamips_vm_handler", "dynamips_device_handler", - "iou_handler"] + "iou_handler", + "qemu_handler"] diff --git a/gns3server/handlers/qemu_handler.py b/gns3server/handlers/qemu_handler.py new file mode 100644 index 00000000..a40bfd78 --- /dev/null +++ b/gns3server/handlers/qemu_handler.py @@ -0,0 +1,254 @@ +# -*- 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 os + + +from ..web.route import Route +from ..modules.port_manager import PortManager +from ..schemas.qemu import QEMU_CREATE_SCHEMA +from ..schemas.qemu import QEMU_UPDATE_SCHEMA +from ..schemas.qemu import QEMU_OBJECT_SCHEMA +from ..schemas.qemu import QEMU_NIO_SCHEMA +from ..modules.qemu import Qemu + + +class QEMUHandler: + + """ + API entry points for QEMU. + """ + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/vms", + parameters={ + "project_id": "UUID for the project" + }, + status_codes={ + 201: "Instance created", + 400: "Invalid request", + 409: "Conflict" + }, + description="Create a new Qemu.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"], + request.match_info["project_id"], + request.json.get("vm_id"), + qemu_path=request.json.get("qemu_path"), + console=request.json.get("console"), + monitor=request.json.get("monitor"), + console_host=PortManager.instance().console_host, + monitor_host=PortManager.instance().console_host, + ) + response.set_status(201) + response.json(vm) + + @classmethod + @Route.get( + r"/projects/{project_id}/qemu/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 Qemu.instance", + output=QEMU_OBJECT_SCHEMA) + def show(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_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}/qemu/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 Qemu.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"]) + vm.name = request.json.get("name", vm.name) + vm.console = request.json.get("console", vm.console) + vm.qemu_path = request.json.get("qemu_path", vm.qemu_path) + + response.json(vm) + + @classmethod + @Route.delete( + r"/projects/{project_id}/qemu/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 Qemu.instance") + def delete(request, response): + + yield from Qemu.instance().delete_vm(request.match_info["vm_id"]) + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/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 Qemu.instance") + def start(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_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}/qemu/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 Qemu.instance") + def stop(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_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}/qemu/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 Qemu.instance") + def reload(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.reload() + response.set_status(204) + + @classmethod + @Route.post( + r"/projects/{project_id}/qemu/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="Reload a Qemu.instance") + def suspend(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + yield from vm.suspend() + response.set_status(204) + + @Route.post( + r"/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port where the nio should be added" + }, + status_codes={ + 201: "NIO created", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Add a NIO to a Qemu.instance", + input=QEMU_NIO_SCHEMA, + output=QEMU_NIO_SCHEMA) + def create_nio(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + nio = qemu_manager.create_nio(vm.qemu_path, request.json) + vm.adapter_add_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"]), nio) + response.set_status(201) + response.json(nio) + + @classmethod + @Route.delete( + r"/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio", + parameters={ + "project_id": "UUID for the project", + "vm_id": "UUID for the instance", + "adapter_number": "Network adapter where the nio is located", + "port_number": "Port from where the nio should be removed" + }, + status_codes={ + 204: "NIO deleted", + 400: "Invalid request", + 404: "Instance doesn't exist" + }, + description="Remove a NIO from a Qemu.instance") + def delete_nio(request, response): + + qemu_manager = Qemu.instance() + vm = qemu_manager.get_vm(request.match_info["vm_id"], project_id=request.match_info["project_id"]) + vm.adapter_remove_nio_binding(int(request.match_info["adapter_number"]), int(request.match_info["port_number"])) + response.set_status(204) diff --git a/gns3server/modules/__init__.py b/gns3server/modules/__init__.py index 25c1012f..0f55e396 100644 --- a/gns3server/modules/__init__.py +++ b/gns3server/modules/__init__.py @@ -19,5 +19,6 @@ from .vpcs import VPCS from .virtualbox import VirtualBox from .dynamips import Dynamips from .iou import IOU +from .qemu import Qemu -MODULES = [VPCS, VirtualBox, Dynamips, IOU] +MODULES = [VPCS, VirtualBox, Dynamips, IOU, Qemu] diff --git a/gns3server/modules/base_vm.py b/gns3server/modules/base_vm.py index c5b39405..6a3f29c1 100644 --- a/gns3server/modules/base_vm.py +++ b/gns3server/modules/base_vm.py @@ -51,9 +51,12 @@ class BaseVM: else: self._console = self._manager.port_manager.get_free_console_port() - log.debug("{module}: {name} [{id}] initialized".format(module=self.manager.module_name, - name=self.name, - id=self.id)) + log.debug("{module}: {name} [{id}] initialized. Console port {console}".format( + module=self.manager.module_name, + name=self.name, + id=self.id, + console=self._console + )) def __del__(self): diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index 9cb5960e..01b36e47 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -59,12 +59,8 @@ class QemuVM(BaseVM): :param qemu_id: QEMU 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 :param monitor: TCP monitor port :param monitor_host: IP address to bind for monitor connections - :param monitor_start_port_range: TCP monitor port range start - :param monitor_end_port_range: TCP monitor port range end """ def __init__(self, @@ -76,27 +72,19 @@ class QemuVM(BaseVM): host="127.0.0.1", console=None, console_host="0.0.0.0", - console_start_port_range=5001, - console_end_port_range=5500, monitor=None, - monitor_host="0.0.0.0", - monitor_start_port_range=5501, - monitor_end_port_range=6000): + monitor_host="0.0.0.0"): super().__init__(name, vm_id, project, manager, console=console) self._host = host + self._console_host = console_host self._command = [] self._started = False self._process = None self._cpulimit_process = None self._stdout_file = "" - self._console_host = console_host - self._console_start_port_range = console_start_port_range - self._console_end_port_range = console_end_port_range self._monitor_host = monitor_host - self._monitor_start_port_range = monitor_start_port_range - self._monitor_end_port_range = monitor_end_port_range # QEMU settings self.qemu_path = qemu_path @@ -104,7 +92,6 @@ class QemuVM(BaseVM): self._hdb_disk_image = "" self._options = "" self._ram = 256 - self._console = console self._monitor = monitor self._ethernet_adapters = [] self._adapter_type = "e1000" @@ -629,6 +616,7 @@ class QemuVM(BaseVM): Executes a command with QEMU monitor when this VM is running. :param command: QEMU monitor command (e.g. info status, stop etc.) + :params expected: An array with the string attended (Default None) :param timeout: how long to wait for QEMU monitor :returns: result of the command (Match object or None) @@ -721,11 +709,12 @@ class QemuVM(BaseVM): log.info("QEMU VM is not paused to be resumed, current status is {}".format(vm_status)) @asyncio.coroutine - def port_add_nio_binding(self, adapter_id, nio): + def adapter_add_nio_binding(self, adapter_id, port_id, nio): """ Adds a port NIO binding. :param adapter_id: adapter ID + :param port_id: port ID :param nio: NIO instance to add to the slot/port """ @@ -761,11 +750,12 @@ class QemuVM(BaseVM): adapter_id=adapter_id)) @asyncio.coroutine - def port_remove_nio_binding(self, adapter_id): + def adapter_remove_nio_binding(self, adapter_id, port_id): """ Removes a port NIO binding. :param adapter_id: adapter ID + :param port_id: port ID :returns: NIO instance """ @@ -981,3 +971,12 @@ class QemuVM(BaseVM): command.extend(shlex.split(additional_options)) command.extend(self._network_options()) return command + + def __json__(self): + return { + "vm_id": self.id, + "project_id": self.project.id, + "name": self.name, + "console": self.console, + "monitor": self.monitor + } diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py new file mode 100644 index 00000000..bbd2aabf --- /dev/null +++ b/gns3server/schemas/qemu.py @@ -0,0 +1,332 @@ +# -*- 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 . + + +QEMU_CREATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to create a new QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "Path to QEMU", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["name", "qemu_path"], +} + +QEMU_UPDATE_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to update a QEMU VM instance", + "type": "object", + "properties": { + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": "string", + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", + "type": "string", + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 32, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": "string", + }, + "cloud_path": { + "description": "Path to the image in the cloud object store", + "type": "string", + }, + "legacy_networking": { + "description": "Use QEMU legagy networking commands (-net syntax)", + "type": "boolean", + }, + "cpu_throttling": { + "description": "Percentage of CPU allowed for QEMU", + "minimum": 0, + "maximum": 800, + "type": "integer", + }, + "process_priority": { + "description": "Process priority for QEMU", + "enum": ["realtime", + "very high", + "high", + "normal", + "low", + "very low"] + }, + "options": { + "description": "Additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, +} + +QEMU_START_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation to start a QEMU VM instance", + "type": "object", + "properties": { + "id": { + "description": "QEMU VM instance ID", + "type": "integer" + }, + }, + "additionalProperties": False, + "required": ["id"] +} + +QEMU_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", + "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 + }, + "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 + }, + }, + "oneOf": [ + {"$ref": "#/definitions/UDP"}, + {"$ref": "#/definitions/Ethernet"}, + {"$ref": "#/definitions/TAP"}, + ], + "additionalProperties": True, + "required": ["type"] +} + +QEMU_OBJECT_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a QEMU VM instance", + "type": "object", + "properties": { + "vm_id": { + "description": "QEMU VM uuid", + "type": "string", + "minLength": 1, + }, + "project_id": { + "description": "Project uuid", + "type": "string", + "minLength": 1, + }, + "name": { + "description": "QEMU VM instance name", + "type": "string", + "minLength": 1, + }, + "qemu_path": { + "description": "path to QEMU", + "type": "string", + "minLength": 1, + }, + "hda_disk_image": { + "description": "QEMU hda disk image path", + "type": "string", + }, + "hdb_disk_image": { + "description": "QEMU hdb disk image path", + "type": "string", + }, + "ram": { + "description": "amount of RAM in MB", + "type": "integer" + }, + "adapters": { + "description": "number of adapters", + "type": "integer", + "minimum": 0, + "maximum": 32, + }, + "adapter_type": { + "description": "QEMU adapter type", + "type": "string", + "minLength": 1, + }, + "console": { + "description": "console TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "monitor": { + "description": "monitor TCP port", + "minimum": 1, + "maximum": 65535, + "type": "integer" + }, + "initrd": { + "description": "QEMU initrd path", + "type": "string", + }, + "kernel_image": { + "description": "QEMU kernel image path", + "type": "string", + }, + "kernel_command_line": { + "description": "QEMU kernel command line", + "type": "string", + }, + "cloud_path": { + "description": "Path to the image in the cloud object store", + "type": "string", + }, + "legacy_networking": { + "description": "Use QEMU legagy networking commands (-net syntax)", + "type": "boolean", + }, + "cpu_throttling": { + "description": "Percentage of CPU allowed for QEMU", + "minimum": 0, + "maximum": 800, + "type": "integer", + }, + "process_priority": { + "description": "Process priority for QEMU", + "enum": ["realtime", + "very high", + "high", + "normal", + "low", + "very low"] + }, + "options": { + "description": "Additional QEMU options", + "type": "string", + }, + }, + "additionalProperties": False, + "required": ["vm_id"] +} diff --git a/tests/api/test_qemu.py b/tests/api/test_qemu.py new file mode 100644 index 00000000..25cc9f9b --- /dev/null +++ b/tests/api/test_qemu.py @@ -0,0 +1,169 @@ +# -*- 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 os +import stat +from tests.utils import asyncio_patch +from unittest.mock import patch + + +@pytest.fixture +def fake_qemu_bin(): + + bin_path = os.path.join(os.environ["PATH"], "qemu_x42") + with open(bin_path, "w+") as f: + f.write("1") + os.chmod(bin_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) + return bin_path + + +@pytest.fixture +def base_params(tmpdir, fake_qemu_bin): + """Return standard parameters""" + return {"name": "PC TEST 1", "qemu_path": fake_qemu_bin} + + +@pytest.fixture +def vm(server, project, base_params): + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), base_params) + assert response.status == 201 + return response.json + + +def test_qemu_create(server, project, base_params): + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), base_params) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_create_with_params(server, project, base_params): + params = base_params + + response = server.post("/projects/{project_id}/qemu/vms".format(project_id=project.id), params, example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_get(server, project, vm): + response = server.get("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 200 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}" + assert response.json["name"] == "PC TEST 1" + assert response.json["project_id"] == project.id + + +def test_qemu_start(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.start", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/start".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_stop(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.stop", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/stop".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_reload(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.reload", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/reload".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_suspend(server, vm): + with asyncio_patch("gns3server.modules.qemu.qemu_vm.QemuVM.suspend", return_value=True) as mock: + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/suspend".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_delete(server, vm): + with asyncio_patch("gns3server.modules.qemu.Qemu.delete_vm", return_value=True) as mock: + response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"])) + assert mock.called + assert response.status == 204 + + +def test_qemu_update(server, vm, tmpdir, free_console_port, project): + params = { + "name": "test", + "console": free_console_port, + } + response = server.put("/projects/{project_id}/qemu/vms/{vm_id}".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), params) + assert response.status == 200 + assert response.json["name"] == "test" + assert response.json["console"] == free_console_port + + +def test_qemu_nio_create_udp(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_udp" + + +def test_qemu_nio_create_ethernet(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=True) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + +def test_qemu_nio_create_ethernet_different_port(server, vm): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/0/ports/3/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_generic_ethernet", + "ethernet_device": "eth0", + }, + example=False) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_generic_ethernet" + assert response.json["ethernet_device"] == "eth0" + + +def test_qemu_nio_create_tap(server, vm): + with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): + response = server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_tap", + "tap_device": "test"}) + assert response.status == 201 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" + assert response.json["type"] == "nio_tap" + + +def test_qemu_delete_nio(server, vm): + server.post("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), {"type": "nio_udp", + "lport": 4242, + "rport": 4343, + "rhost": "127.0.0.1"}) + response = server.delete("/projects/{project_id}/qemu/vms/{vm_id}/adapters/1/ports/0/nio".format(project_id=vm["project_id"], vm_id=vm["vm_id"]), example=True) + assert response.status == 204 + assert response.route == "/projects/{project_id}/qemu/vms/{vm_id}/adapters/{adapter_number:\d+}/ports/{port_number:\d+}/nio" diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index 238520d7..63b89572 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -20,6 +20,7 @@ import aiohttp import asyncio import os import stat +import re from tests.utils import asyncio_patch @@ -83,7 +84,7 @@ def test_stop(loop, vm): with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - vm.port_add_nio_binding(0, nio) + vm.adapter_add_nio_binding(0, 0, nio) loop.run_until_complete(asyncio.async(vm.start())) assert vm.is_running() loop.run_until_complete(asyncio.async(vm.stop())) @@ -98,23 +99,32 @@ def test_reload(loop, vm): assert mock.called_with("system_reset") +def test_suspend(loop, vm): + + control_vm_result = MagicMock() + control_vm_result.match.group.decode.return_value = "running" + with asyncio_patch("gns3server.modules.qemu.QemuVM._control_vm", return_value=control_vm_result) as mock: + loop.run_until_complete(asyncio.async(vm.suspend())) + assert mock.called_with("system_reset") + + def test_add_nio_binding_udp(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) assert nio.lport == 4242 -def test_add_nio_binding_tap(vm, loop): +def test_add_nio_binding_ethernet(vm, loop): with patch("gns3server.modules.base_manager.BaseManager._has_privileged_access", return_value=True): - nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_tap", "tap_device": "test"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) - assert nio.tap_device == "test" + nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_generic_ethernet", "ethernet_device": "eth0"}) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + assert nio.ethernet_device == "eth0" def test_port_remove_nio_binding(vm, loop): nio = Qemu.instance().create_nio(vm.qemu_path, {"type": "nio_udp", "lport": 4242, "rport": 4243, "rhost": "127.0.0.1"}) - loop.run_until_complete(asyncio.async(vm.port_add_nio_binding(0, nio))) - loop.run_until_complete(asyncio.async(vm.port_remove_nio_binding(0))) + loop.run_until_complete(asyncio.async(vm.adapter_add_nio_binding(0, 0, nio))) + loop.run_until_complete(asyncio.async(vm.adapter_remove_nio_binding(0, 0))) assert vm._ethernet_adapters[0].ports[0] is None