From 1ee3e14bd30fd1708eed8d4e30e00504466d943e Mon Sep 17 00:00:00 2001 From: Brian Candler Date: Thu, 8 Jul 2021 17:43:30 +0100 Subject: [PATCH] Support cloning of encrypted qcow2 base image files Fixes #1921 --- gns3server/compute/qemu/qemu_vm.py | 23 +++++++++++++++++++++-- gns3server/compute/qemu/utils/qcow2.py | 17 +++++++++++++++-- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/gns3server/compute/qemu/qemu_vm.py b/gns3server/compute/qemu/qemu_vm.py index 15ad6834..1168e3c2 100644 --- a/gns3server/compute/qemu/qemu_vm.py +++ b/gns3server/compute/qemu/qemu_vm.py @@ -1675,6 +1675,24 @@ class QemuVM(BaseNode): try: qemu_img_path = self._get_qemu_img() command = [qemu_img_path, "create", "-o", "backing_file={}".format(disk_image), "-f", "qcow2", disk] + try: + base_qcow2 = Qcow2(disk_image) + if base_qcow2.crypt_method: + # Workaround for https://gitlab.com/qemu-project/qemu/-/issues/441 + # Also embed a secret name so it doesn't have to be passed to qemu -drive ... + options = { + "encrypt.key-secret": os.path.basename(disk_image), + "driver": "qcow2", + "file": { + "driver": "file", + "filename": disk_image, + }, + } + command = [qemu_img_path, "create", "-b", "json:"+json.dumps(options, separators=(',', ':')), + "-f", "qcow2", "-u", disk, str(base_qcow2.size)] + except Qcow2Error: + pass # non-qcow2 base images are acceptable (e.g. vmdk, raw image) + retcode = await self._qemu_img_exec(command) if retcode: stdout = self.read_qemu_img_stdout() @@ -1845,6 +1863,7 @@ class QemuVM(BaseNode): log.warning("Qemu image {} is corrupted".format(disk_image)) if (await self._qemu_img_exec([qemu_img_path, "check", "-r", "all", "{}".format(disk_image)])) == 2: self.project.emit("log.warning", {"message": "Qemu image '{}' is corrupted and could not be fixed".format(disk_image)}) + # ignore retcode == 1. One reason is that the image is encrypted and there is no encrypt.key-secret available except (OSError, subprocess.SubprocessError) as e: stdout = self.read_qemu_img_stdout() raise QemuError("Could not check '{}' disk image: {}\n{}".format(disk_name, e, stdout)) @@ -1858,9 +1877,9 @@ class QemuVM(BaseNode): # The disk exists we check if the clone works try: qcow2 = Qcow2(disk) - await qcow2.rebase(qemu_img_path, disk_image) + await qcow2.validate(qemu_img_path) except (Qcow2Error, OSError) as e: - raise QemuError("Could not use qcow2 disk image '{}' for {} {}".format(disk_image, disk_name, e)) + raise QemuError("Could not use qcow2 disk image '{}' for {}: {}".format(disk_image, disk_name, e)) else: disk = disk_image diff --git a/gns3server/compute/qemu/utils/qcow2.py b/gns3server/compute/qemu/utils/qcow2.py index efbb54fe..5a4d7979 100644 --- a/gns3server/compute/qemu/utils/qcow2.py +++ b/gns3server/compute/qemu/utils/qcow2.py @@ -58,11 +58,12 @@ class Qcow2: # uint64_t snapshots_offset; # } QCowHeader; - struct_format = ">IIQi" + struct_format = ">IIQiiQi" with open(self._path, 'rb') as f: content = f.read(struct.calcsize(struct_format)) try: - self.magic, self.version, self.backing_file_offset, self.backing_file_size = struct.unpack_from(struct_format, content) + (self.magic, self.version, self.backing_file_offset, self.backing_file_size, + self.cluster_bits, self.size, self.crypt_method) = struct.unpack_from(struct_format, content) except struct.error: raise Qcow2Error("Invalid file header for {}".format(self._path)) @@ -103,3 +104,15 @@ class Qcow2: if retcode != 0: raise Qcow2Error("Could not rebase the image") self._reload() + + async def validate(self, qemu_img): + """ + Run qemu-img info to validate the file and its backing images + + :param qemu_img: Path to the qemu-img binary + """ + command = [qemu_img, "info", "--backing-chain", self._path] + process = await asyncio.create_subprocess_exec(*command) + retcode = await process.wait() + if retcode != 0: + raise Qcow2Error("Could not validate the image")