Support cloning of encrypted qcow2 base image files

Fixes #1921
This commit is contained in:
Brian Candler 2021-07-08 17:43:30 +01:00
parent cdedd53339
commit 1ee3e14bd3
2 changed files with 36 additions and 4 deletions

View File

@ -1675,6 +1675,24 @@ class QemuVM(BaseNode):
try: try:
qemu_img_path = self._get_qemu_img() qemu_img_path = self._get_qemu_img()
command = [qemu_img_path, "create", "-o", "backing_file={}".format(disk_image), "-f", "qcow2", disk] 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) retcode = await self._qemu_img_exec(command)
if retcode: if retcode:
stdout = self.read_qemu_img_stdout() stdout = self.read_qemu_img_stdout()
@ -1845,6 +1863,7 @@ class QemuVM(BaseNode):
log.warning("Qemu image {} is corrupted".format(disk_image)) 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: 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)}) 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: except (OSError, subprocess.SubprocessError) as e:
stdout = self.read_qemu_img_stdout() stdout = self.read_qemu_img_stdout()
raise QemuError("Could not check '{}' disk image: {}\n{}".format(disk_name, e, 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 # The disk exists we check if the clone works
try: try:
qcow2 = Qcow2(disk) qcow2 = Qcow2(disk)
await qcow2.rebase(qemu_img_path, disk_image) await qcow2.validate(qemu_img_path)
except (Qcow2Error, OSError) as e: 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: else:
disk = disk_image disk = disk_image

View File

@ -58,11 +58,12 @@ class Qcow2:
# uint64_t snapshots_offset; # uint64_t snapshots_offset;
# } QCowHeader; # } QCowHeader;
struct_format = ">IIQi" struct_format = ">IIQiiQi"
with open(self._path, 'rb') as f: with open(self._path, 'rb') as f:
content = f.read(struct.calcsize(struct_format)) content = f.read(struct.calcsize(struct_format))
try: 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: except struct.error:
raise Qcow2Error("Invalid file header for {}".format(self._path)) raise Qcow2Error("Invalid file header for {}".format(self._path))
@ -103,3 +104,15 @@ class Qcow2:
if retcode != 0: if retcode != 0:
raise Qcow2Error("Could not rebase the image") raise Qcow2Error("Could not rebase the image")
self._reload() 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")