diff --git a/gns3server/handlers/api/qemu_handler.py b/gns3server/handlers/api/qemu_handler.py index 6d09f5b4..b6980bbc 100644 --- a/gns3server/handlers/api/qemu_handler.py +++ b/gns3server/handlers/api/qemu_handler.py @@ -316,6 +316,21 @@ class QEMUHandler: binaries = yield from Qemu.img_binary_list() response.json(binaries) + @classmethod + @Route.post( + r"/qemu/img", + status_codes={ + 201: "Image created", + }, + description="Create a Qemu image" + ) + def create_img(request, response): + + qemu_img = request.json.pop("qemu_img") + path = request.json.pop("path") + yield from Qemu.instance().create_disk(qemu_img, path, request.json) + response.set_status(201) + @Route.get( r"/qemu/vms", status_codes={ diff --git a/gns3server/modules/qemu/__init__.py b/gns3server/modules/qemu/__init__.py index 1df45c09..dbd64ec1 100644 --- a/gns3server/modules/qemu/__init__.py +++ b/gns3server/modules/qemu/__init__.py @@ -187,3 +187,32 @@ class Qemu(BaseManager): """ return os.path.join(os.path.expanduser(self.config.get_section_config("Server").get("images_path", "~/GNS3/images")), "QEMU") + @asyncio.coroutine + def create_disk(self, qemu_img, path, options): + """ + Create a qemu disk with qemu-img + + :param qemu_img: qemu-img binary path + :param path: Image path + :param options: Disk image creation options + """ + + try: + img_format = options.pop("format") + img_size = options.pop("size") + + if not os.path.isabs(path): + directory = self.get_images_directory() + os.makedirs(directory, exist_ok=True) + path = os.path.join(directory, os.path.basename(path)) + + command = [qemu_img, "create", "-f", img_format] + for option in sorted(options.keys()): + command.extend(["-o", "{}={}".format(option, options[option])]) + command.append(path) + command.append("{}M".format(img_size)) + + process = yield from asyncio.create_subprocess_exec(*command) + yield from process.wait() + except (OSError, subprocess.SubprocessError) as e: + raise QemuError("Could create disk image {}:{}".format(path, e)) diff --git a/gns3server/modules/qemu/qemu_vm.py b/gns3server/modules/qemu/qemu_vm.py index b8305acf..65b4740a 100644 --- a/gns3server/modules/qemu/qemu_vm.py +++ b/gns3server/modules/qemu/qemu_vm.py @@ -189,7 +189,6 @@ class QemuVM(BaseVM): :param hda_disk_image: QEMU hda disk image path """ - self._hda_disk_image = self.manager.get_abs_image_path(hda_disk_image) log.info('QEMU VM "{name}" [{id}] has set the QEMU hda disk image path to {disk_image}'.format(name=self._name, id=self._id, @@ -1242,31 +1241,3 @@ class QemuVM(BaseVM): answer["kernel_image_md5sum"] = md5sum(self._kernel_image) return answer - - @asyncio.coroutine - def _create_disk(self, name, options): - """ - Create a qemu disk with qemu-img - - :param name: Image name without the extension - :param options: Disk image creation options - :returns: Image name with the extensions - """ - - img_format = options.pop("format") - img_size = options.pop("size") - img_name = "{}.{}".format(name, img_format) - - qemu_img = self._get_qemu_img() - command = [qemu_img, "create", "-f", img_format] - for option in sorted(options.keys()): - command.extend(["-o", "{}={}".format(option, options[option])]) - command.append(os.path.join(self.working_dir, img_name)) - command.append("{}M".format(img_size)) - try: - process = yield from asyncio.create_subprocess_exec(*command) - yield from process.wait() - except (OSError, subprocess.SubprocessError) as e: - raise QemuError("Could create disk image {}:{}".format(name, e)) - - return img_name diff --git a/tests/handlers/api/test_qemu.py b/tests/handlers/api/test_qemu.py index e1d97468..bd5f782c 100644 --- a/tests/handlers/api/test_qemu.py +++ b/tests/handlers/api/test_qemu.py @@ -239,3 +239,20 @@ def test_upload_vm_permission_denied(server, tmpdir): with patch("gns3server.modules.Qemu.get_images_directory", return_value=str(tmpdir),): response = server.post("/qemu/vms/test2", body="TEST", raw=True) assert response.status == 409 + + +def test_create_img(server): + body = { + "qemu_img": "/tmp/qemu-img", + "path": "hda.qcow2", + "format": "qcow2", + "preallocation": "metadata", + "cluster_size": 64, + "refcount_bits": 12, + "lazy_refcounts": "off", + "size": 100 + } + with asyncio_patch("gns3server.modules.Qemu.create_disk"): + response = server.post("/qemu/img", body=body, example=True) + + assert response.status == 201 diff --git a/tests/modules/qemu/test_qemu_manager.py b/tests/modules/qemu/test_qemu_manager.py index 8eb7a60b..c4ba7e18 100644 --- a/tests/modules/qemu/test_qemu_manager.py +++ b/tests/modules/qemu/test_qemu_manager.py @@ -19,9 +19,21 @@ import os import stat import asyncio import sys +import pytest from gns3server.modules.qemu import Qemu from tests.utils import asyncio_patch +from unittest.mock import patch, MagicMock + + +@pytest.fixture +def fake_qemu_img_binary(tmpdir): + + bin_path = str(tmpdir / "qemu-img") + 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 def test_get_qemu_version(loop): @@ -57,6 +69,7 @@ def test_binary_list(loop): 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): files_to_create = ["qemu-img", "qemu-io", "qemu-system-x86", "qemu-system-x42", "qemu-kvm", "hello"] @@ -83,3 +96,52 @@ def test_img_binary_list(loop): def test_get_legacy_vm_workdir(): assert Qemu.get_legacy_vm_workdir(42, "bla") == os.path.join("qemu", "vm-42") + + +def test_create_image_abs_path(loop, tmpdir, fake_qemu_img_binary): + options = { + "format": "qcow2", + "preallocation": "metadata", + "cluster_size": 64, + "refcount_bits": 12, + "lazy_refcounts": "off", + "size": 100 + } + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + loop.run_until_complete(asyncio.async(Qemu.instance().create_disk(fake_qemu_img_binary, str(tmpdir / "hda.qcow2"), options))) + args, kwargs = process.call_args + assert args == ( + fake_qemu_img_binary, + "create", + "-f", + "qcow2", + "-o", + "cluster_size=64", + "-o", + "lazy_refcounts=off", + "-o", + "preallocation=metadata", + "-o", + "refcount_bits=12", + str(tmpdir / "hda.qcow2"), + "100M" + ) + + +def test_create_image_relative_path(loop, tmpdir, fake_qemu_img_binary): + options = { + "format": "raw", + "size": 100 + } + with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: + with patch("gns3server.modules.qemu.Qemu.get_images_directory", return_value=str(tmpdir)): + loop.run_until_complete(asyncio.async(Qemu.instance().create_disk(fake_qemu_img_binary, "hda.qcow2", options))) + args, kwargs = process.call_args + assert args == ( + fake_qemu_img_binary, + "create", + "-f", + "raw", + str(tmpdir / "hda.qcow2"), + "100M" + ) diff --git a/tests/modules/qemu/test_qemu_vm.py b/tests/modules/qemu/test_qemu_vm.py index cff07cea..23df4ccb 100644 --- a/tests/modules/qemu/test_qemu_vm.py +++ b/tests/modules/qemu/test_qemu_vm.py @@ -424,34 +424,3 @@ def test_get_qemu_img_not_exist(vm, tmpdir): vm._qemu_path = str(tmpdir / "qemu-sytem-x86_64") with pytest.raises(QemuError): vm._get_qemu_img() - - -def test_create_image(vm, loop, fake_qemu_img_binary): - options = { - "format": "qcow2", - "preallocation": "metadata", - "cluster_size": 64, - "refcount_bits": 12, - "lazy_refcounts": "off", - "size": 100 - } - with asyncio_patch("asyncio.create_subprocess_exec", return_value=MagicMock()) as process: - filename = loop.run_until_complete(asyncio.async(vm._create_disk("hda", options))) - args, kwargs = process.call_args - assert args == ( - fake_qemu_img_binary, - "create", - "-f", - "qcow2", - "-o", - "cluster_size=64", - "-o", - "lazy_refcounts=off", - "-o", - "preallocation=metadata", - "-o", - "refcount_bits=12", - os.path.join(vm.working_dir, "hda.qcow2"), - "100M" - ) - assert filename == "hda.qcow2"