Coverage for drivers/qcow2util.py : 26%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/env python3
2#
3# Copyright (C) 2024 Vates SAS
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU General Public License as published by
7# the Free Software Foundation, either version 3 of the License, or
8# (at your option) any later version.
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU General Public License for more details.
13#
14# You should have received a copy of the GNU General Public License
15# along with this program. If not, see <https://www.gnu.org/licenses/>.
17from sm_typing import Any, Callable, Dict, Final, Optional, Tuple, cast, override
18from typing import BinaryIO
20import errno
21import os
22import re
23import time
24import struct
25import zlib
26import json
27from pathlib import Path
29import util
30import xs_errors
31from blktap2 import TapCtl
32from cowutil import CowUtil, CowImageInfo
33from lvmcache import LVMCache
34from constants import NS_PREFIX_LVM, VG_PREFIX
36# ------------------------------------------------------------------------------
38MAX_QCOW_CHAIN_LENGTH: Final = 30
40QCOW2_DEFAULT_CLUSTER_SIZE: Final = 64 * 1024 # 64 KiB
42MIN_QCOW_SIZE: Final = QCOW2_DEFAULT_CLUSTER_SIZE
44MAX_QCOW_SIZE: Final = 17589500641280 # Max size so that the fully allocated size with metadata is under the max size of EXT4 (17592185061376 bytes fully allocated)
46QEMU_IMG: Final = "/usr/bin/qemu-img"
47QCOW2_HELPER = "/opt/xensource/libexec/qcow2_helper"
49QCOW2_TYPE: Final = "qcow2"
50RAW_TYPE: Final = "raw"
52# ------------------------------------------------------------------------------
54class QCowUtil(CowUtil):
56 # We followed specifications found here:
57 # https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt
59 QCOW2_MAGIC = 0x514649FB # b"QFI\xfb": Magic number for QCOW2 files
60 QCOW2_HEADER_SIZE = 104 # In fact the last information we need is at offset 40-47
61 QCOW2_BACKING_FILE_OFFSET = 8
63 def _read_qcow2(self, path: str):
64 with open(path, "rb") as qcow2_file:
65 self.filename = path # Keep the filename if clean is called
66 self.header = self._read_qcow2_header(qcow2_file)
68 @staticmethod
69 def _read_qcow2_backingfile(file: BinaryIO, backing_file_offset: int , backing_file_size: int) -> str:
70 if backing_file_offset == 0:
71 return ""
73 file.seek(backing_file_offset)
74 parent_name = file.read(backing_file_size)
75 return parent_name.decode("UTF-8")
77 @staticmethod
78 def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]:
79 """Returns a dict containing some information from QCow2 header.
81 Args:
82 file: The qcow2 file object.
84 Returns:
85 dict: magic, version, cluster_bits, l1_size and l1_table_offset.
87 Raises:
88 ValueError: if qcow2 magic is not recognized or cluster size not supported.
89 """
90 # The header is as follow:
91 #
92 # magic: u32, // Magic string "QFI\xfb"
93 # version: u32, // Version (2 or 3)
94 # backing_file_offset: u64, // Offset to the backing file name
95 # backing_file_size: u32, // Size of the backing file name
96 # cluster_bits: u32, // Bits used for addressing within a cluster
97 # size: u64, // Virtual disk size
98 # crypt_method: u32, // 0 = no encryption, 1 = AES encryption
99 # l1_size: u32, // Number of entries in the L1 table
100 # l1_table_offset: u64, // Offset to the active L1 table
101 # refcount_table_offset: u64, // Offset to the refcount table
102 # refcount_table_clusters: u32, // Number of clusters for the refcount table
103 # nb_snapshots: u32, // Number of snapshots in the image
104 # snapshots_offset: u64, // Offset to the snapshot table
106 file.seek(0)
107 header = file.read(QCowUtil.QCOW2_HEADER_SIZE)
108 (
109 magic,
110 version,
111 backing_file_offset,
112 backing_file_size,
113 cluster_bits,
114 size,
115 _,
116 l1_size,
117 l1_table_offset,
118 refcount_table_offset,
119 _,
120 _,
121 snapshots_offset,
122 ) = struct.unpack(">IIQIIQIIQQIIQ", header[:72])
124 if magic != QCowUtil.QCOW2_MAGIC:
125 raise ValueError("Not a valid QCOW2 file")
127 parent_name = QCowUtil._read_qcow2_backingfile(file, backing_file_offset, backing_file_size)
129 return {
130 "version": version,
131 "backing_file_offset": backing_file_offset,
132 "backing_file_size": backing_file_size,
133 "virtual_disk_size": size,
134 "cluster_bits": cluster_bits,
135 "l1_size": l1_size,
136 "l1_table_offset": l1_table_offset,
137 "refcount_table_offset": refcount_table_offset,
138 "snapshots_offset": snapshots_offset,
139 "parent": parent_name,
140 }
142 @staticmethod
143 def _move_backing_file(
144 f: BinaryIO, old_offset: int, new_offset: int, data_size: int
145 ) -> None:
146 """Move a number of bytes from old_offset to new_offset and replaces the old
147 value by 0s. It is up to the caller to save the current position in the file
148 if needed.
150 Args:
151 f: the file the will be modified
152 old_offset: the current offset
153 new_offset: the new offset where we want to move data
154 data_size: Size in bytes of data that we want to move
156 Returns:
157 Nothing but the file f is modified and the position in the file also.
158 """
159 # Read the string at backing_file_offset
160 f.seek(old_offset)
161 data = f.read(data_size)
163 # Write zeros at the original location
164 f.seek(old_offset)
165 f.write(b"\x00" * data_size)
167 # Write the string to the new location
168 f.seek(new_offset)
169 f.write(data)
171 def _add_or_find_custom_header(self) -> int:
172 """Add custom header at the end of header extensions
174 It finds the end of the header extensions and add the custom header.
175 If the header already exists nothing is done.
177 Args:
179 Returns:
180 It returns the data offset where custom header is found or created.
181 If data offset is 0 something weird happens.
182 The qcow2 file in self.filename can be modified.
183 """
184 header_length = 72 # This is the default value for version 2 images
186 custom_header_type = 0x76617465 # vate: it is easy to recognize with hexdump -C
187 custom_header_length = 8
188 custom_header_data = 0
189 # We don't need padding because we are already aligned
190 custom_header = struct.pack(
191 ">IIQ", custom_header_type, custom_header_length, custom_header_data
192 )
194 with open(self.filename, "rb+") as qcow2_file:
195 if self.header["version"] == 3:
196 qcow2_file.seek(100) # 100 is the offset of header_length
197 header_length = int.from_bytes(qcow2_file.read(4), "big")
199 # After the image header we found Header extension. So we need to find the end of
200 # the header extension area and add our custom header.
201 qcow2_file.seek(header_length)
203 custom_data_offset = 0
205 while True:
206 ext_type = int.from_bytes(qcow2_file.read(4), "big")
207 ext_len = int.from_bytes(qcow2_file.read(4), "big")
209 if ext_type == custom_header_type:
210 # A custom header is already there
211 custom_data_offset = qcow2_file.tell()
212 break
214 if ext_type == 0x00000000:
215 # End mark found. If we found the end mark it means that we didn't find
216 # the custom header. So we need to add it.
217 custom_data_offset = qcow2_file.tell()
219 # We will overwrite the end marker so rewind a little bit to
220 # write the new type extension and the new length. But if there is
221 # a backing file we need to move it to make some space.
222 if self.header["backing_file_offset"]:
223 # Keep current position
224 saved_pos = qcow2_file.tell()
226 bf_offset = self.header["backing_file_offset"]
227 bf_size = self.header["backing_file_size"]
228 bf_new_offset = bf_offset + len(custom_header)
229 self._move_backing_file(
230 qcow2_file, bf_offset, bf_new_offset, bf_size
231 )
233 # Update the header to match the new backing file offset
234 self.header["backing_file_offset"] = bf_new_offset
235 qcow2_file.seek(self.QCOW2_BACKING_FILE_OFFSET)
236 qcow2_file.write(struct.pack(">Q", bf_new_offset))
238 # Restore saved position
239 qcow2_file.seek(saved_pos)
241 qcow2_file.seek(-8, 1)
242 qcow2_file.write(custom_header)
243 break
245 # Round up the header extension size to the next multiple of 8
246 ext_len = (ext_len + 7) & 0xFFFFFFF8
247 qcow2_file.seek(ext_len, 1)
249 return custom_data_offset
251 # ----
252 # Implementation of CowUtil
253 # ----
255 @override
256 def getMinImageSize(self) -> int:
257 return MIN_QCOW_SIZE
259 @override
260 def getMaxImageSize(self) -> int:
261 return MAX_QCOW_SIZE
263 @override
264 def getBlockSize(self, path: str) -> int:
265 self._read_qcow2(path)
266 return 1 << self.header["cluster_bits"]
268 @override
269 def getFooterSize(self) -> int:
270 return 0
272 @override
273 def getDefaultPreallocationSizeVirt(self) -> int:
274 """vhdutil answer max size (2TiB) here but we don't want to allocate for max size in QCOW2, it would make small LV a lot bigger."""
275 return MIN_QCOW_SIZE
277 @override
278 def getMaxChainLength(self) -> int:
279 return MAX_QCOW_CHAIN_LENGTH
281 @override
282 def calcOverheadEmpty(self, virtual_size: int, block_size: Optional[int] = None) -> int:
283 if block_size:
284 cluster_size = block_size
285 else:
286 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE
287 cmd = [QEMU_IMG, "measure", "-O", "qcow2", "--output", "json", "-o", f"cluster_size={cluster_size}", "--size", f"{virtual_size}"]
288 output = json.loads(self._ioretry(cmd))
289 return int(output["required"])
291 @override
292 def calcOverheadBitmap(self, virtual_size: int) -> int:
293 """
294 We are not impacted, this is only for VHD. We send back 0 for QCOW2.
295 """
296 return 0
298 @override
299 def getInfo(
300 self,
301 path: str,
302 extractUuidFunction: Callable[[str], str],
303 includeParent: bool = True,
304 resolveParent: bool = True,
305 useBackupFooter: bool = False
306 ) -> CowImageInfo:
307 """
308 resolveParent is useful to find the absolute path of a parent since the path stored in VHD is relative.
309 For linstor, this cause issues since the parent might not exist locally.
310 We currently do not implement this for QCOW2.
311 """
312 self._read_qcow2(path)
313 uuid = extractUuidFunction(path)
314 cowinfo = CowImageInfo(uuid)
315 cowinfo.path = path
316 cowinfo.sizeVirt = self.header["virtual_disk_size"]
317 cowinfo.sizePhys = self.getSizePhys(path)
318 cowinfo.hidden = self.getHidden(path)
319 if includeParent:
320 parent_path = self.header["parent"]
321 if parent_path != "":
322 cowinfo.parentPath = parent_path
323 cowinfo.parentUuid = extractUuidFunction(parent_path)
325 return cowinfo
327 @override
328 def getInfoFromLVM(
329 self, lvName: str, extractUuidFunction: Callable[[str], str], vgName: str
330 ) -> Optional[CowImageInfo]:
331 lvcache = LVMCache(vgName)
332 return self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName)
334 def _getInfoLV(
335 self, lvcache: LVMCache, extractUuidFunction: Callable[[str], str], vgName: str, lvName: str
336 ) -> Optional[CowImageInfo]:
337 lvPath = "/dev/{}/{}".format(vgName, lvName)
338 lvcache.refresh()
339 if lvName not in lvcache.lvs:
340 util.SMlog("{} does not exist anymore".format(lvName))
341 return None
343 vdiUuid = extractUuidFunction(lvPath)
344 srUuid = vgName.replace(VG_PREFIX, "")
346 ns = NS_PREFIX_LVM + srUuid
347 lvcache.activate(ns, vdiUuid, lvName, False)
348 try:
349 cowinfo = self.getInfo(lvPath, extractUuidFunction)
350 finally:
351 lvcache.deactivate(ns, vdiUuid, lvName, False)
352 return cowinfo
354 @override
355 def getAllInfoFromVG(
356 self,
357 pattern: str,
358 extractUuidFunction: Callable[[str], str],
359 vgName: Optional[str] = None,
360 parents: bool = False,
361 exitOnError: bool = False
362 ) -> Dict[str, CowImageInfo]:
363 result: Dict[str, CowImageInfo] = dict()
364 #TODO: handle exitOnError
365 if vgName: 365 ↛ 366line 365 didn't jump to line 366, because the condition on line 365 was never true
366 reg = re.compile(pattern)
367 lvcache = LVMCache(vgName)
368 lvcache.refresh()
369 # We get size in lvcache.lvs[lvName].size (in bytes)
370 # We could read the header from the PV directly
371 lvList = list(lvcache.lvs.keys())
372 for lvName in lvList:
373 # lvinfo = lvcache.lvs[lvName]
374 if reg.match(lvName):
375 cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName)
376 if cowinfo is None: #We get None if the LV stopped existing in the meanwhile
377 continue
378 cowinfo.path = lvName # Function CowUtil.getParentChain expect lvName here, otherwise blktap.{_activate,_deactivate} crashes
379 result[cowinfo.uuid] = cowinfo
380 if parents:
381 parentUuid = cowinfo.parentUuid
382 parentPath = cowinfo.parentPath
383 while parentUuid != "":
384 parentLvName = parentPath.split("/")[-1]
385 parent_cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, parentLvName)
386 if parent_cowinfo is None: #Parent disappeared while scanning
387 raise util.SMException("Parent of {} wasn't found during scan".format(lvName))
388 parentUuid = parent_cowinfo.parentUuid
389 parentPath = parent_cowinfo.parentPath
390 parent_cowinfo.path = parentLvName #Same reason as above, some users expect LvName here instead of path
391 result[parent_cowinfo.uuid] = parent_cowinfo
393 return result
394 else:
395 pattern_p: Path = Path(pattern)
396 list_qcow = list(pattern_p.parent.glob(pattern_p.name))
397 for qcow in list_qcow: 397 ↛ 398line 397 didn't jump to line 398, because the loop on line 397 never started
398 qcow_str = str(qcow)
399 info = self.getInfo(qcow_str, extractUuidFunction)
400 result[info.uuid] = info
401 return result
403 @override
404 def getParent(self, path: str, extractUuidFunction: Callable[[str], str]) -> Optional[str]:
405 parent = self.getParentNoCheck(path)
406 if parent:
407 return extractUuidFunction(parent)
408 return None
410 @override
411 def getParentNoCheck(self, path: str) -> Optional[str]:
412 self._read_qcow2(path)
413 parent_path = self.header["parent"]
414 if parent_path == "":
415 return None
416 return parent_path
418 @override
419 def hasParent(self, path: str) -> bool:
420 if self.getParentNoCheck(path):
421 return True
422 return False
424 @override
425 def setParent(self, path: str, parentPath: str, parentRaw: bool) -> None:
426 pid_openers = util.get_openers_pid(path)
427 if pid_openers:
428 util.SMlog("Rebasing while process {} has the VDI opened".format(pid_openers))
430 parentType = QCOW2_TYPE
431 if parentRaw:
432 parentType = RAW_TYPE
433 cmd = [QEMU_IMG, "rebase", "-q", "-u", "-f", QCOW2_TYPE, "-F", parentType, "-b", parentPath, path]
434 self._ioretry(cmd)
436 @override
437 def getHidden(self, path: str) -> bool:
438 """Get hidden property according to the value b
440 Args:
442 Returns:
443 True if hidden is set, False otherwise
444 """
445 self._read_qcow2(path)
446 custom_data_offset = self._add_or_find_custom_header()
447 if custom_data_offset == 0:
448 raise util.SMException("Custom data offset not found... should not reach this")
450 with open(path, "rb") as qcow2_file:
451 qcow2_file.seek(custom_data_offset)
452 hidden = qcow2_file.read(1)
453 if hidden == b"\x00":
454 return False
455 return True
457 @override
458 def setHidden(self, path: str, hidden: bool = True) -> None:
459 """Set hidden property according to the value b
461 Args:
462 bool: True if you want to set the property. False otherwise
464 Returns:
465 nothing. If the custom headers is not found it is created so the
466 qcow file can be modified.
467 """
468 self._read_qcow2(path)
469 custom_data_offset = self._add_or_find_custom_header()
470 if custom_data_offset == 0:
471 raise util.SMException("Custom data offset not found... should not reach this")
473 with open(self.filename, "rb+") as qcow2_file:
474 qcow2_file.seek(custom_data_offset)
475 if hidden:
476 qcow2_file.write(b"\x01")
477 else:
478 qcow2_file.write(b"\x00")
480 @override
481 def getSizeVirt(self, path: str) -> int:
482 self._read_qcow2(path)
483 return self.header['virtual_disk_size']
485 @override
486 def setSizeVirt(self, path: str, size: int, jFile: str) -> None:
487 """
488 size: byte
489 jFile: a journal file used for resizing with VHD, not useful for QCOW2
490 """
491 cmd = [QEMU_IMG, "resize", "-q", path, str(size)]
492 self._ioretry(cmd)
494 @override
495 def setSizeVirtFast(self, path: str, size: int) -> None:
496 self.setSizeVirt(path, size, "")
498 @override
499 def getMaxResizeSize(self, path: str) -> int:
500 return 0
502 @override
503 def getSizePhys(self, path: str) -> int:
504 size = os.stat(path).st_size
505 if size == 0:
506 size = int(self._ioretry(["blockdev", "--getsize64", path]))
507 return size
509 @override
510 def setSizePhys(self, path: str, size: int, debug: bool = True) -> None:
511 pass #We do not store the physical size in the image in QCOW2
513 @override
514 def getAllocatedSize(self, path: str) -> int:
515 cmd = [QCOW2_HELPER, "allocated", path]
516 return int(self._ioretry(cmd))
518 @override
519 def getResizeJournalSize(self) -> int:
520 return 0
522 @override
523 def killData(self, path: str) -> None:
524 """Remove all data and reset L1/L2 table.
526 Args:
527 self: The QcowInfo object.
529 Returns:
530 nothing.
531 """
532 self._read_qcow2(path)
533 # We need to reset L1 entries and then just truncate the file right
534 # after L1 entries
535 with open(self.filename, "r+b") as file:
536 l1_table_offset = self.header["l1_table_offset"]
537 file.seek(l1_table_offset)
539 l1_table_size = (
540 self.header["l1_size"] * 8
541 ) # size in bytes, each entry is 8 bytes
542 file.write(b"\x00" * l1_table_size)
543 file.truncate(l1_table_offset + l1_table_size)
545 @override
546 def getDepth(self, path: str) -> int:
547 cmd = [QEMU_IMG, "info", "--backing-chain", "--output=json", path]
548 ret = str(self._ioretry(cmd))
549 depth = len(re.findall("\"backing-filename\"", ret))+1
550 #chain depth is beginning at one for VHD, meaning a VHD without parent has depth = 1
551 return depth
553 @override
554 def getBlockBitmap(self, path: str) -> bytes:
555 cmd = [QCOW2_HELPER, "bitmap", path]
556 text = cast(bytes, self._ioretry(cmd, text=False))
557 return zlib.compress(text)
559 def _getTapdisk(self, path: str) -> Tuple[int, int]:
560 """
561 Return a tuple of (PID, Minor) for the given path
562 """
563 pid_openers = util.get_openers_pid(path)
564 if pid_openers:
565 if len(pid_openers) > 1:
566 raise xs_errors.XenError("Multiple openers for {}".format(path)) # TODO: There might be multiple PID? Yes, we can have the chain enabled for multiple leaf (i.e. after a clone), taken into account in cleanup.py
567 pid = pid_openers[0]
568 tapdiskList = TapCtl.list(pid=pid)
569 if len(tapdiskList) > 1: #TODO: There might more than one minor for this blktap?
570 raise xs_errors.XenError("TapdiskAlreadyRunning", "There is multiple minor for this tapdisk process")
571 minor = tapdiskList[0]["minor"]
572 return (pid, minor)
573 raise xs_errors.XenError("TapdiskFailed", "No tapdisk process found for {}".format(path))
575 @override
576 def coalesceOnline(self, path: str) -> int:
577 pid, minor = self._getTapdisk(path)
578 logger = util.LoggerCounter(10)
580 try:
581 TapCtl.commit(pid, minor, QCOW2_TYPE, path)
582 # We need to wait for query to return concluded
583 # We are technically ininterruptible since being interrupted will only stop checking if the job is done.
584 # We need to call `tap-ctl cancel` if we are interrupted, it is done in cleanup.py code.
585 except TapCtl.CommandFailure:
586 util.SMlog(f"Commit command failed on tapdisk instance {pid} (m: {minor}). Raising...")
587 raise
589 try:
590 status, nb, total = TapCtl.query(pid, minor)
591 if status == "undefined":
592 util.SMlog("Tapdisk {} (m: {}) coalesce status undefined for {}".format(pid, minor, path))
593 return 0
595 while status != "concluded":
596 time.sleep(1)
597 status, nb, total = TapCtl.query(pid, minor, quiet=True)
598 logger.log("Got status {} for tapdisk {} (m: {})".format(status, pid, minor))
600 if nb < total:
601 # The coalesce didn't finish because we might have been aborted.
602 raise xs_errors.XenError("TapdiskFailed",
603 f"Online coalesce incomplete on tapdisk {pid} (m: {minor}) for {path}: {nb}/{total} bytes")
604 return nb
605 except TapCtl.CommandFailure:
606 util.SMlog(f"Query command failed on tapdisk instance {pid} (m: {minor}). Raising...")
607 raise
609 @override
610 def cancelCoalesceOnline(self, path: str) -> None:
611 pid, minor = self._getTapdisk(path)
613 try:
614 TapCtl.cancel_commit(pid, minor)
615 except TapCtl.CommandFailure:
616 util.SMlog("Cancel command failed on tapdisk instance {}. Raising...".format(pid))
617 raise
619 @override
620 def coalesce(self, path: str) -> int:
621 # -d on commit make it not empty the original image since we don't intend to keep it
622 cmd = [QEMU_IMG, "commit", "-f", QCOW2_TYPE, path, "-d"]
623 ret = cast(str, self._ioretry(cmd)) # Allows to parse for byte coalesced, our qemu-img is supposed to be patched to output it.
624 lines = ret.splitlines()
625 if re.match("Image committed.", lines[-1]):
626 res_line = lines[-2]
627 else:
628 res_line = lines[-1]
630 results = re.match(r"\((\d+)/(\d+)\)", res_line)
631 if results:
632 committed_bytes = int(results.group(1))
633 return committed_bytes
634 raise xs_errors.XenError("TapdiskFailed", "Couldn't get commited size from qemu-img commit call") # TODO: We might not want to raise in this case, it would break if the qemu-img called isn't modified to print the coalesce result even if it succeeded in coalesceing
636 @override
637 def create(self, path: str, size: int, static: bool, msize: int = 0, block_size: Optional[int] = None) -> None:
638 cmd = [QEMU_IMG, "create", "-q", "-f", QCOW2_TYPE, path, str(size)]
639 if static:
640 cmd.extend(["-o", "preallocation=full"])
641 if block_size:
642 cmd.extend(["-o", f"cluster_size={str(block_size)}"])
643 self._ioretry(cmd)
644 self.setHidden(path, False) #We add hidden header at creation
646 @override
647 def snapshot(
648 self,
649 path: str,
650 parent: str,
651 parentRaw: bool,
652 msize: int = 0,
653 checkEmpty: bool = True,
654 is_mirror_image: bool = False
655 ) -> None:
656 # TODO: msize, it's use to preallocate metadata, could we honor this too?
657 # TODO: checkEmpty? If it is False, then the parent could be empty and should still be used for snapshot
658 # But if True, if the parent is empty, we do what? vhd would just use the parent of parent as base, should we emulate this behavior?
660 cmd = [QEMU_IMG, "create", "-q"]
662 if parentRaw:
663 parent_type = RAW_TYPE
664 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE
665 else:
666 parent_type = QCOW2_TYPE
667 cluster_size = self.getBlockSize(parent)
668 args = ["-f", QCOW2_TYPE, "-F", parent_type, "-b", parent]
670 if is_mirror_image:
671 # is_mirror_image override the cluster size to ensure that we have a write of 512b to avoid having to read the parent during a migration.
672 # This is needed because the blkif blocksize is only 512b, as such it will try to only write blocks smaller than the cluster size.
673 # To write a smaller block, we would need to read the parent image cluster then change the 512b block.
674 # The parent being empty during the mirroring phase, reading from it would read zeros and corrupt the cluster.
675 # It also enable extended_l2 for this purpose, this is only done in the snapshot used for the mirror, this configuration will be lost when coalesced in its parent.
676 # Ensuring we go back to a better cluster_size for performance reasons.
677 # This limit our images max size to 64TiB.
678 cluster_size = 16 * 1024 # 16KiB
679 args.extend(["-o", "extended_l2=on"])
681 args.extend(["-o", f"cluster_size={cluster_size}"])
682 cmd.extend(args)
683 cmd.append(path)
685 self._ioretry(cmd)
686 self.setHidden(path, False) #We add hidden header at creation
688 @override
689 def canSnapshotRaw(self, size: int) -> bool:
690 return size <= MAX_QCOW_SIZE
692 @override
693 def check(
694 self,
695 path: str,
696 ignoreMissingFooter: bool = False,
697 fast: bool = False
698 ) -> CowUtil.CheckResult:
699 cmd = [QEMU_IMG, "check", "-q", path]
700 try:
701 self._ioretry(cmd)
702 return CowUtil.CheckResult.Success
703 except util.CommandException as e:
704 if e.code in (errno.EROFS, errno.EMEDIUMTYPE):
705 return CowUtil.CheckResult.Unavailable
706 # 1/EPERM is error in internal during check
707 # 2/ENOENT is QCOW corrupted
708 # 3/ESRCH is QCow has leaked clusters
709 # 63/ENOSR is check unavailable on this image type
710 return CowUtil.CheckResult.Fail
712 @override
713 def revert(self, path: str, jFile: str) -> None:
714 pass #Used to get back from a failed operation using a journal, NOOP for qcow2
716 @override
717 def repair(self, path: str) -> None:
718 cmd = [QEMU_IMG, "check", "-q", "-f", QCOW2_TYPE, "-r", "all", path]
719 self._ioretry(cmd)
721 @override
722 def validateAndRoundImageSize(self, size: int) -> int:
723 if size < 0 or size > MAX_QCOW_SIZE:
724 raise xs_errors.XenError(
725 "VDISize",
726 opterr="VDI size must be between 1 MB and {} MB".format(MAX_QCOW_SIZE // (1024 * 1024))
727 )
729 return util.roundup(QCOW2_DEFAULT_CLUSTER_SIZE, size)
731 @override
732 def getKeyHash(self, path: str) -> Optional[str]:
733 pass
735 @override
736 def setKey(self, path: str, key_hash: str) -> None:
737 pass
739 @override
740 def isCoalesceableOnRemote(self) -> bool:
741 return True