diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 5f8c40f8..a02f8693 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -25,7 +25,9 @@ from ...schemas.nio import NIO_SCHEMA 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_BINARY_FILTER_SCHEMA from ...schemas.qemu import QEMU_BINARY_LIST_SCHEMA +from ...schemas.qemu import QEMU_CAPABILITY_LIST_SCHEMA from ...schemas.qemu import QEMU_IMAGE_CREATE_SCHEMA from ...schemas.vm import VM_LIST_IMAGES_SCHEMA from ...modules.qemu import Qemu @@ -300,10 +302,11 @@ class QEMUHandler: 404: "Instance doesn't exist" }, description="Get a list of available Qemu binaries", + input=QEMU_BINARY_FILTER_SCHEMA, output=QEMU_BINARY_LIST_SCHEMA) def list_binaries(request, response): - binaries = yield from Qemu.binary_list() + binaries = yield from Qemu.binary_list(request.json.get("archs", None)) response.json(binaries) @classmethod @@ -321,6 +324,21 @@ class QEMUHandler: binaries = yield from Qemu.img_binary_list() response.json(binaries) + @Route.get( + r"/qemu/capabilities", + status_codes={ + 200: "Success" + }, + description="Get a list of Qemu capabilities on this server", + output=QEMU_CAPABILITY_LIST_SCHEMA + ) + def get_capabilities(request, response): + capabilities = {"kvm": []} + kvms = yield from Qemu.get_kvm_archs() + if kvms: + capabilities["kvm"] = kvms + response.json(capabilities) + @classmethod @Route.post( r"/qemu/img", diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index a0666444..3ee441b0 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -21,6 +21,7 @@ Qemu server module. import asyncio import os +import platform import sys import re import subprocess @@ -38,6 +39,33 @@ class Qemu(BaseManager): _VM_CLASS = QemuVM + @staticmethod + @asyncio.coroutine + def get_kvm_archs(): + """ + Gets a list of architectures for which KVM is available on this server. + + :returns: List of architectures for which KVM is available on this server. + """ + kvm = [] + + try: + process = yield from asyncio.create_subprocess_exec("kvm-ok") + yield from process.wait() + except OSError: + return kvm + + if process.returncode == 0: + arch = platform.machine() + if arch == "x86_64": + kvm.append("x86_64") + kvm.append("i386") + elif arch == "i386": + kvm.append("i386") + else: + kvm.append(platform.machine()) + return kvm + @staticmethod def paths_list(): """ @@ -82,7 +110,7 @@ class Qemu(BaseManager): return paths @staticmethod - def binary_list(): + def binary_list(archs=None): """ Gets QEMU binaries list available on the host. @@ -96,9 +124,17 @@ class Qemu(BaseManager): if (f.startswith("qemu-system") or f.startswith("qemu-kvm") or f == "qemu" or f == "qemu.exe") and \ os.access(os.path.join(path, f), os.X_OK) and \ os.path.isfile(os.path.join(path, f)): - qemu_path = os.path.join(path, f) - version = yield from Qemu.get_qemu_version(qemu_path) - qemus.append({"path": qemu_path, "version": version}) + if archs is not None: + for arch in archs: + if f.endswith(arch) or f.endswith("{}.exe".format(arch)) or f.endswith("{}w.exe".format(arch)): + qemu_path = os.path.join(path, f) + version = yield from Qemu.get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + else: + qemu_path = os.path.join(path, f) + version = yield from Qemu.get_qemu_version(qemu_path) + qemus.append({"path": qemu_path, "version": version}) + except OSError: continue diff --git a/gns3server/schemas/qemu.py b/gns3server/schemas/qemu.py index 43af3409..89362fbe 100644 --- a/gns3server/schemas/qemu.py +++ b/gns3server/schemas/qemu.py @@ -601,6 +601,21 @@ QEMU_OBJECT_SCHEMA = { "vm_directory"] } +QEMU_BINARY_FILTER_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a list of qemu capabilities", + "properties": { + "archs": { + "description": "Architectures to filter binaries by", + "type": "array", + "items": { + "enum": QEMU_PLATFORMS + } + } + }, + "additionalProperties": False, +} + QEMU_BINARY_LIST_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Request validation for a list of qemu binaries", @@ -626,6 +641,21 @@ QEMU_BINARY_LIST_SCHEMA = { "additionalProperties": False, } +QEMU_CAPABILITY_LIST_SCHEMA = { + "$schema": "http://json-schema.org/draft-04/schema#", + "description": "Request validation for a list of qemu capabilities", + "properties": { + "kvm": { + "description": "Architectures that KVM is enabled for", + "type": "array", + "items": { + "enum": QEMU_PLATFORMS + } + } + }, + "additionalProperties": False, +} + QEMU_IMAGE_CREATE_SCHEMA = { "$schema": "http://json-schema.org/draft-04/schema#", "description": "Create a new qemu image. Options can be specific to a format. Read qemu-img manual for more information", diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py index de512993..619b13fd 100644 --- a/tests/handlers/api/test_qemu.py +++ b/tests/handlers/api/test_qemu.py @@ -200,8 +200,21 @@ def test_qemu_list_binaries(server, vm): {"path": "/tmp/2", "version": "2.1.0"}] with asyncio_patch("gns3server.modules.qemu.Qemu.binary_list", return_value=ret) as mock: response = server.get("/qemu/binaries".format(project_id=vm["project_id"]), example=True) - assert mock.called + assert mock.called_with(None) + assert response.status == 200 + assert response.json == ret + + +def test_qemu_list_binaries_filter(server, vm): + ret = [ + {"path": "/tmp/x86_64", "version": "2.2.0"}, + {"path": "/tmp/alpha", "version": "2.1.0"}, + {"path": "/tmp/i386", "version": "2.1.0"} + ] + with asyncio_patch("gns3server.modules.qemu.Qemu.binary_list", return_value=ret) as mock: + response = server.get("/qemu/binaries".format(project_id=vm["project_id"]), body={"archs": ["i386"]}, example=True) assert response.status == 200 + assert mock.called_with(["i386"]) assert response.json == ret @@ -312,3 +325,9 @@ def test_create_img_absolute_local(server): response = server.post("/qemu/img", body=body, example=True) assert response.status == 201 + + +def test_capabilities(server): + with asyncio_patch("gns3server.modules.Qemu.get_kvm_archs", return_value=["x86_64"]): + response = server.get("/qemu/capabilities", example=True) + assert response.json["kvm"] == ["x86_64"] diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py index 36ede5bb..38da1cbf 100644 --- a/tests/modules/qemu/test_qemu_manager.py +++ b/tests/modules/qemu/test_qemu_manager.py @@ -20,6 +20,7 @@ import stat import asyncio import sys import pytest +import platform from gns3server.modules.qemu import Qemu from gns3server.modules.qemu.qemu_error import QemuError @@ -58,18 +59,32 @@ def test_binary_list(loop): os.chmod(path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR) with asyncio_patch("gns3server.modules.qemu.subprocess_check_output", return_value="QEMU emulator version 2.2.0, Copyright (c) 2003-2008 Fabrice Bellard") as mock: - qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list())) - if sys.platform.startswith("win"): version = "" else: version = "2.2.0" + qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list())) + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x86"), "version": version} in qemus assert {"path": os.path.join(os.environ["PATH"], "qemu-kvm"), "version": version} in qemus assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x42"), "version": version} in qemus assert {"path": os.path.join(os.environ["PATH"], "hello"), "version": version} not in qemus + qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list(["x86"]))) + + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x86"), "version": version} in qemus + assert {"path": os.path.join(os.environ["PATH"], "qemu-kvm"), "version": version} not in qemus + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x42"), "version": version} not in qemus + assert {"path": os.path.join(os.environ["PATH"], "hello"), "version": version} not in qemus + + qemus = loop.run_until_complete(asyncio.async(Qemu.binary_list(["x86", "x42"]))) + + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x86"), "version": version} in qemus + assert {"path": os.path.join(os.environ["PATH"], "qemu-kvm"), "version": version} not in qemus + assert {"path": os.path.join(os.environ["PATH"], "qemu-system-x42"), "version": version} in qemus + assert {"path": os.path.join(os.environ["PATH"], "hello"), "version": version} not in qemus + def test_img_binary_list(loop): @@ -160,3 +175,21 @@ def test_create_image_exist(loop, tmpdir, fake_qemu_img_binary): with pytest.raises(QemuError): loop.run_until_complete(asyncio.async(Qemu.instance().create_disk(fake_qemu_img_binary, "hda.qcow2", options))) assert not process.called + + +def test_get_kvm_archs_no_kvm(loop): + with asyncio_patch("asyncio.create_subprocess_exec", side_effect=FileNotFoundError('kvm-ok')): + archs = loop.run_until_complete(asyncio.async(Qemu.get_kvm_archs())) + assert archs == [] + + +def test_get_kvm_archs_kvm_ok(loop): + + process = MagicMock() + with asyncio_patch("asyncio.create_subprocess_exec", return_value=process): + process.returncode = 0 + archs = loop.run_until_complete(asyncio.async(Qemu.get_kvm_archs())) + if platform.machine() == 'x86_64': + assert archs == ['x86_64', 'i386'] + else: + assert archs == platform.machine() diff --git a/tests/utils.py b/tests/utils.py index b8994f66..cb3b4f39 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -50,6 +50,11 @@ class _asyncio_patch: future = asyncio.Future() if "return_value" in self.kwargs: future.set_result(self.kwargs["return_value"]) + elif "side_effect" in self.kwargs: + if isinstance(self.kwargs["side_effect"], Exception): + future.set_exception(self.kwargs["side_effect"]) + else: + raise NotImplementedError else: future.set_result(True) return future