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
1from sm_typing import Any, Callable, Dict, Final, List, Optional, Tuple, cast, override
2from typing import BinaryIO
4import errno
5import os
6import re
7import time
8import struct
9import zlib
10import json
11from pathlib import Path
13import util
14import xs_errors
15from blktap2 import TapCtl
16from cowutil import CowUtil, CowImageInfo
17from lvmcache import LVMCache
18from constants import NS_PREFIX_LVM, VG_PREFIX
20MAX_QCOW_CHAIN_LENGTH: Final = 30
22QCOW2_DEFAULT_CLUSTER_SIZE: Final = 64 * 1024 # 64 KiB
24MIN_QCOW_SIZE: Final = QCOW2_DEFAULT_CLUSTER_SIZE
26MAX_QCOW_SIZE: Final = 16 * 1024 * 1024 * 1024 * 1024
28QEMU_IMG: Final = "/usr/bin/qemu-img"
29QCOW2_HELPER = "/opt/xensource/libexec/qcow2_helper"
31QCOW2_TYPE: Final = "qcow2"
32RAW_TYPE: Final = "raw"
34class QCowUtil(CowUtil):
36 # We followed specifications found here:
37 # https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt
39 QCOW2_MAGIC = 0x514649FB # b"QFI\xfb": Magic number for QCOW2 files
40 QCOW2_HEADER_SIZE = 104 # In fact the last information we need is at offset 40-47
41 QCOW2_L2_SIZE = QCOW2_DEFAULT_CLUSTER_SIZE
42 QCOW2_BACKING_FILE_OFFSET = 8
44 ALLOCATED_ENTRY_BIT = (
45 0x8000_0000_0000_0000 # Bit 63 is the allocated bit for standard cluster
46 )
47 CLUSTER_TYPE_BIT = 0x4000_0000_0000_0000 # 0 for standard, 1 for compressed cluster
48 L2_OFFSET_MASK = 0x00FF_FFFF_FFFF_FF00 # Bits 9-55 are offset of L2 table.
49 CLUSTER_DESCRIPTION_MASK = 0x3FFF_FFFF_FFFF_FFFF # Bit 0-61 is cluster description
50 STANDARD_CLUSTER_OFFSET_MASK = (
51 0x00FF_FFFF_FFFF_FF00 # Bits 9-55 are offset of standard cluster
52 )
54 def __init__(self):
55 self.qcow_read = False
57 def _read_qcow2(self, path: str, read_clusters: bool = False):
58 phys_disk_size = self.getSizePhys(path)
59 with open(path, "rb") as qcow2_file:
60 self.filename = path # Keep the filename if clean is called
61 self.header = self._read_qcow2_header(qcow2_file)
62 if read_clusters:
63 self.l1 = self._get_l1_entries(qcow2_file)
64 # The l1_to_l2 allows to get L2 entries for a given L1. If L1 entry
65 # is not allocated we store an empty list.
66 self.l1_to_l2: Dict[int, List[int]] = {}
68 for l1_entry in self.l1:
69 l2_offset = l1_entry & self.L2_OFFSET_MASK
70 if l2_offset == 0:
71 self.l1_to_l2[l1_entry] = []
72 elif l2_offset > phys_disk_size: #TODO: This sometime happen for a correct VDI (while coalescing online?)
73 raise xs_errors.XenError("VDISize", "L2 Offset is bigger than physical disk {}".format(path))
74 else:
75 self.l1_to_l2[l1_entry] = self._get_l2_entries(
76 qcow2_file, l2_offset
77 )
78 self.qcow_read = True
80 def _get_l1_entries(self, file: BinaryIO) -> List[int]:
81 """Returns the list of all L1 entries.
83 Args:
84 file: The qcow2 file object.
86 Returns:
87 list: List of all L1 entries
88 """
89 l1_table_offset = self.header["l1_table_offset"]
90 file.seek(l1_table_offset)
92 l1_table_size = self.header["l1_size"] * 8 # Each L1 entry is 8 bytes
93 l1_table = file.read(l1_table_size)
95 return [
96 struct.unpack(">Q", l1_table[i : i + 8])[0]
97 for i in range(0, len(l1_table), 8)
98 ]
100 @staticmethod
101 def _get_l2_entries(file: BinaryIO, l2_offset: int) -> List[int]:
102 """Returns the list of all L2 entries at a given L2 offset.
104 Args:
105 file: The qcow2 file.
106 l2_offset: the L2 offset where to look for entries
108 Returns:
109 list: List of all L2 entries
110 """
111 # The size of L2 is 65536 bytes and each entry is 8 bytes.
112 file.seek(l2_offset)
113 l2_table = file.read(QCowUtil.QCOW2_L2_SIZE)
115 return [
116 struct.unpack(">Q", l2_table[i : i + 8])[0]
117 for i in range(0, len(l2_table), 8)
118 ]
120 @staticmethod
121 def _read_qcow2_backingfile(file: BinaryIO, backing_file_offset: int , backing_file_size: int) -> str:
122 if backing_file_offset == 0:
123 return ""
125 file.seek(backing_file_offset)
126 parent_name = file.read(backing_file_size)
127 return parent_name.decode("UTF-8")
129 @staticmethod
130 def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]:
131 """Returns a dict containing some information from QCow2 header.
133 Args:
134 file: The qcow2 file object.
136 Returns:
137 dict: magic, version, cluster_bits, l1_size and l1_table_offset.
139 Raises:
140 ValueError: if qcow2 magic is not recognized or cluster size not supported.
141 """
142 # The header is as follow:
143 #
144 # magic: u32, // Magic string "QFI\xfb"
145 # version: u32, // Version (2 or 3)
146 # backing_file_offset: u64, // Offset to the backing file name
147 # backing_file_size: u32, // Size of the backing file name
148 # cluster_bits: u32, // Bits used for addressing within a cluster
149 # size: u64, // Virtual disk size
150 # crypt_method: u32, // 0 = no encryption, 1 = AES encryption
151 # l1_size: u32, // Number of entries in the L1 table
152 # l1_table_offset: u64, // Offset to the active L1 table
153 # refcount_table_offset: u64, // Offset to the refcount table
154 # refcount_table_clusters: u32, // Number of clusters for the refcount table
155 # nb_snapshots: u32, // Number of snapshots in the image
156 # snapshots_offset: u64, // Offset to the snapshot table
158 file.seek(0)
159 header = file.read(QCowUtil.QCOW2_HEADER_SIZE)
160 (
161 magic,
162 version,
163 backing_file_offset,
164 backing_file_size,
165 cluster_bits,
166 size,
167 _,
168 l1_size,
169 l1_table_offset,
170 refcount_table_offset,
171 _,
172 _,
173 snapshots_offset,
174 ) = struct.unpack(">IIQIIQIIQQIIQ", header[:72])
176 if magic != QCowUtil.QCOW2_MAGIC:
177 raise ValueError("Not a valid QCOW2 file")
179 parent_name = QCowUtil._read_qcow2_backingfile(file, backing_file_offset, backing_file_size)
181 return {
182 "version": version,
183 "backing_file_offset": backing_file_offset,
184 "backing_file_size": backing_file_size,
185 "virtual_disk_size": size,
186 "cluster_bits": cluster_bits,
187 "l1_size": l1_size,
188 "l1_table_offset": l1_table_offset,
189 "refcount_table_offset": refcount_table_offset,
190 "snapshots_offset": snapshots_offset,
191 "parent": parent_name,
192 }
194 @staticmethod
195 def _is_l1_allocated(entry: int) -> bool:
196 """Checks if the given L1 entry is allocated.
198 If the offset is 0 then the L2 table and all clusters described
199 by this L2 table are unallocated.
201 Args:
202 entry: L1 entry
204 Returns:
205 bool: True if the L1 entry is allocated (ie has a valid offset).
206 False otherwise.
207 """
208 return (entry & QCowUtil.L2_OFFSET_MASK) != 0
210 @staticmethod
211 def _is_l2_allocated(entry: int) -> bool:
212 """Checks if a given entry is allocated.
214 Currently we only support standard clusters. And for standard clusters
215 the bit 63 is set to 1 for allocated ones or offset is not 0.
217 Args:
218 entry: L2 entry
220 Returns:
221 bool: Returns True if the L2 entry is allocated, False otherwise
223 Raises:
224 raise an exception if the cluster is not a standard one.
225 """
226 assert entry & QCowUtil.CLUSTER_TYPE_BIT == 0
227 return (entry & QCowUtil.ALLOCATED_ENTRY_BIT != 0) or (
228 entry & QCowUtil.STANDARD_CLUSTER_OFFSET_MASK != 0
229 )
231 @staticmethod
232 def _get_allocated_clusters(l2_entries: List[int]) -> List[int]:
233 """Get all allocated clusters in a given list of L2 entries.
235 Args:
236 l2_entries: A list of L2 entries.
238 Returns:
239 A list of all allocated entries
240 """
241 return [entry for entry in l2_entries if QCowUtil._is_l2_allocated(entry)]
243 @staticmethod
244 def _get_cluster_to_byte(clusters: int, cluster_bits: int) -> int:
245 # (1 << cluster_bits) give cluster size in byte
246 return clusters * (1 << cluster_bits)
248 def _get_number_of_allocated_clusters(self) -> int:
249 """Get the number of allocated clusters.
251 Args:
252 self: A QcowInfo object.
254 Returns:
255 An integer that is the list of allocated clusters.
256 """
257 assert(self.qcow_read)
259 allocated_clusters = 0
261 for l2_entries in self.l1_to_l2.values():
262 allocated_clusters += len(self._get_allocated_clusters(l2_entries))
264 return allocated_clusters
266 @staticmethod
267 def _move_backing_file(
268 f: BinaryIO, old_offset: int, new_offset: int, data_size: int
269 ) -> None:
270 """Move a number of bytes from old_offset to new_offset and replaces the old
271 value by 0s. It is up to the caller to save the current position in the file
272 if needed.
274 Args:
275 f: the file the will be modified
276 old_offset: the current offset
277 new_offset: the new offset where we want to move data
278 data_size: Size in bytes of data that we want to move
280 Returns:
281 Nothing but the file f is modified and the position in the file also.
282 """
283 # Read the string at backing_file_offset
284 f.seek(old_offset)
285 data = f.read(data_size)
287 # Write zeros at the original location
288 f.seek(old_offset)
289 f.write(b"\x00" * data_size)
291 # Write the string to the new location
292 f.seek(new_offset)
293 f.write(data)
295 def _add_or_find_custom_header(self) -> int:
296 """Add custom header at the end of header extensions
298 It finds the end of the header extensions and add the custom header.
299 If the header already exists nothing is done.
301 Args:
303 Returns:
304 It returns the data offset where custom header is found or created.
305 If data offset is 0 something weird happens.
306 The qcow2 file in self.filename can be modified.
307 """
308 assert self.qcow_read
310 header_length = 72 # This is the default value for version 2 images
312 custom_header_type = 0x76617465 # vate: it is easy to recognize with hexdump -C
313 custom_header_length = 8
314 custom_header_data = 0
315 # We don't need padding because we are already aligned
316 custom_header = struct.pack(
317 ">IIQ", custom_header_type, custom_header_length, custom_header_data
318 )
320 with open(self.filename, "rb+") as qcow2_file:
321 if self.header["version"] == 3:
322 qcow2_file.seek(100) # 100 is the offset of header_length
323 header_length = int.from_bytes(qcow2_file.read(4), "big")
325 # After the image header we found Header extension. So we need to find the end of
326 # the header extension area and add our custom header.
327 qcow2_file.seek(header_length)
329 custom_data_offset = 0
331 while True:
332 ext_type = int.from_bytes(qcow2_file.read(4), "big")
333 ext_len = int.from_bytes(qcow2_file.read(4), "big")
335 if ext_type == custom_header_type:
336 # A custom header is already there
337 custom_data_offset = qcow2_file.tell()
338 break
340 if ext_type == 0x00000000:
341 # End mark found. If we found the end mark it means that we didn't find
342 # the custom header. So we need to add it.
343 custom_data_offset = qcow2_file.tell()
345 # We will overwrite the end marker so rewind a little bit to
346 # write the new type extension and the new length. But if there is
347 # a backing file we need to move it to make some space.
348 if self.header["backing_file_offset"]:
349 # Keep current position
350 saved_pos = qcow2_file.tell()
352 bf_offset = self.header["backing_file_offset"]
353 bf_size = self.header["backing_file_size"]
354 bf_new_offset = bf_offset + len(custom_header)
355 self._move_backing_file(
356 qcow2_file, bf_offset, bf_new_offset, bf_size
357 )
359 # Update the header to match the new backing file offset
360 self.header["backing_file_offset"] = bf_new_offset
361 qcow2_file.seek(self.QCOW2_BACKING_FILE_OFFSET)
362 qcow2_file.write(struct.pack(">Q", bf_new_offset))
364 # Restore saved position
365 qcow2_file.seek(saved_pos)
367 qcow2_file.seek(-8, 1)
368 qcow2_file.write(custom_header)
369 break
371 # Round up the header extension size to the next multiple of 8
372 ext_len = (ext_len + 7) & 0xFFFFFFF8
373 qcow2_file.seek(ext_len, 1)
375 return custom_data_offset
377 def _set_l1_zero(self):
378 zero = int(0).to_bytes(1, "little")
379 nb_of_entries_per_cluster = QCOW2_DEFAULT_CLUSTER_SIZE/8
380 return list(zero * int(nb_of_entries_per_cluster/8))
382 def _set_l2_zero(self, b, i):
383 return b & ~(1 << i)
385 def _set_l2_one(self, b, i):
386 return b | (1 << i)
388 def _create_bitmap(self) -> bytes:
389 idx: int = 0
390 bitmap = list()
391 b = 0
392 for l1_entry in self.l1:
393 if not self._is_l1_allocated(l1_entry):
394 bitmap.extend(self._set_l1_zero())
395 continue
397 l2_table = self.l1_to_l2[l1_entry] #L2 is cluster_size/8 entries of cluster_size page
398 for l2_entry in l2_table:
399 if self._is_l2_allocated(l2_entry):
400 b = self._set_l2_one(b, idx)
401 else:
402 b = self._set_l2_zero(b, idx)
403 idx += 1
404 if idx == 8:
405 bitmap.append(b)
406 b = 0
407 idx = 0
408 return struct.pack("B"*len(bitmap), *bitmap)
410 # ----
411 # Implementation of CowUtil
412 # ----
414 @override
415 def getMinImageSize(self) -> int:
416 return MIN_QCOW_SIZE
418 @override
419 def getMaxImageSize(self) -> int:
420 return MAX_QCOW_SIZE
422 @override
423 def getBlockSize(self, path: str) -> int:
424 self._read_qcow2(path)
425 return 1 << self.header["cluster_bits"]
427 @override
428 def getFooterSize(self) -> int:
429 return 0
431 @override
432 def getDefaultPreallocationSizeVirt(self) -> int:
433 """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."""
434 return MIN_QCOW_SIZE
436 @override
437 def getMaxChainLength(self) -> int:
438 return MAX_QCOW_CHAIN_LENGTH
440 @override
441 def calcOverheadEmpty(self, virtual_size: int, block_size: Optional[int] = None) -> int:
442 if block_size:
443 cluster_size = block_size
444 else:
445 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE
446 cmd = [QEMU_IMG, "measure", "-O", "qcow2", "--output", "json", "-o", f"cluster_size={cluster_size}", "--size", f"{virtual_size}"]
447 output = json.loads(self._ioretry(cmd))
448 return int(output["required"])
450 @override
451 def calcOverheadBitmap(self, virtual_size: int) -> int:
452 return 0 #TODO: What do we send back?
454 @override
455 def getInfo(
456 self,
457 path: str,
458 extractUuidFunction: Callable[[str], str],
459 includeParent: bool = True,
460 resolveParent: bool = True,
461 useBackupFooter: bool = False
462 ) -> CowImageInfo:
463 #TODO: handle resolveParent
464 self._read_qcow2(path)
465 basename = Path(path).name
466 uuid = extractUuidFunction(basename)
467 cowinfo = CowImageInfo(uuid)
468 cowinfo.path = basename
469 cowinfo.sizeVirt = self.header["virtual_disk_size"]
470 cowinfo.sizePhys = self.getSizePhys(path)
471 cowinfo.hidden = self.getHidden(path)
472 cowinfo.sizeAllocated = self.getAllocatedSize(path)
473 if includeParent:
474 parent_path = self.header["parent"]
475 if parent_path != "":
476 cowinfo.parentPath = parent_path
477 cowinfo.parentUuid = extractUuidFunction(parent_path)
479 return cowinfo
481 @override
482 def getInfoFromLVM(
483 self, lvName: str, extractUuidFunction: Callable[[str], str], vgName: str
484 ) -> Optional[CowImageInfo]:
485 lvcache = LVMCache(vgName)
486 return self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName)
488 def _getInfoLV(
489 self, lvcache: LVMCache, extractUuidFunction: Callable[[str], str], vgName: str, lvName: str
490 ) -> Optional[CowImageInfo]:
491 lvPath = "/dev/{}/{}".format(vgName, lvName)
492 lvcache.refresh()
493 if lvName not in lvcache.lvs:
494 util.SMlog("{} does not exist anymore".format(lvName))
495 return None
497 vdiUuid = extractUuidFunction(lvPath)
498 srUuid = vgName.replace(VG_PREFIX, "")
500 ns = NS_PREFIX_LVM + srUuid
501 lvcache.activate(ns, vdiUuid, lvName, False)
502 try:
503 cowinfo = self.getInfo(lvPath, extractUuidFunction)
504 finally:
505 lvcache.deactivate(ns, vdiUuid, lvName, False)
506 return cowinfo
508 @override
509 def getAllInfoFromVG(
510 self,
511 pattern: str,
512 extractUuidFunction: Callable[[str], str],
513 vgName: Optional[str] = None,
514 parents: bool = False,
515 exitOnError: bool = False
516 ) -> Dict[str, CowImageInfo]:
517 result: Dict[str, CowImageInfo] = dict()
518 #TODO: handle exitOnError
519 if vgName: 519 ↛ 520line 519 didn't jump to line 520, because the condition on line 519 was never true
520 reg = re.compile(pattern)
521 lvcache = LVMCache(vgName)
522 lvcache.refresh()
523 # We get size in lvcache.lvs[lvName].size (in bytes)
524 # We could read the header from the PV directly
525 lvList = list(lvcache.lvs.keys())
526 for lvName in lvList:
527 # lvinfo = lvcache.lvs[lvName]
528 if reg.match(lvName):
529 cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName)
530 if cowinfo is None: #We get None if the LV stopped existing in the meanwhile
531 continue
532 result[cowinfo.uuid] = cowinfo
533 if parents:
534 parentUuid = cowinfo.parentUuid
535 parentPath = cowinfo.parentPath
536 while parentUuid != "":
537 parentLvName = parentPath.split("/")[-1]
538 parent_cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, parentLvName)
539 if parent_cowinfo is None: #Parent disappeared while scanning
540 raise util.SMException("Parent of {} wasn't found during scan".format(lvName))
541 result[parent_cowinfo.uuid] = parent_cowinfo
542 parentUuid = parent_cowinfo.parentUuid
543 parentPath = parent_cowinfo.parentPath
544 return result
545 else:
546 pattern_p: Path = Path(pattern)
547 list_qcow = list(pattern_p.parent.glob(pattern_p.name))
548 for qcow in list_qcow: 548 ↛ 549line 548 didn't jump to line 549, because the loop on line 548 never started
549 qcow_str = str(qcow)
550 info = self.getInfo(qcow_str, extractUuidFunction)
551 result[info.uuid] = info
552 return result
554 @override
555 def getParent(self, path: str, extractUuidFunction: Callable[[str], str]) -> Optional[str]:
556 parent = self.getParentNoCheck(path)
557 if parent:
558 return extractUuidFunction(parent)
559 return None
561 @override
562 def getParentNoCheck(self, path: str) -> Optional[str]:
563 self._read_qcow2(path)
564 parent_path = self.header["parent"]
565 if parent_path == "":
566 return None
567 return parent_path
569 @override
570 def hasParent(self, path: str) -> bool:
571 if self.getParentNoCheck(path):
572 return True
573 return False
575 @override
576 def setParent(self, path: str, parentPath: str, parentRaw: bool) -> None:
577 pid_openers = util.get_openers_pid(path)
578 if pid_openers:
579 util.SMlog("Rebasing while process {} has the VDI opened".format(pid_openers))
581 parentType = QCOW2_TYPE
582 if parentRaw:
583 parentType = RAW_TYPE
584 cmd = [QEMU_IMG, "rebase", "-u", "-f", QCOW2_TYPE, "-F", parentType, "-b", parentPath, path]
585 self._ioretry(cmd)
587 @override
588 def getHidden(self, path: str) -> bool:
589 """Get hidden property according to the value b
591 Args:
593 Returns:
594 True if hidden is set, False otherwise
595 """
596 self._read_qcow2(path)
597 custom_data_offset = self._add_or_find_custom_header()
598 if custom_data_offset == 0:
599 raise util.SMException("Custom data offset not found... should not reach this")
601 with open(path, "rb") as qcow2_file:
602 qcow2_file.seek(custom_data_offset)
603 hidden = qcow2_file.read(1)
604 if hidden == b"\x00":
605 return False
606 return True
608 @override
609 def setHidden(self, path: str, hidden: bool = True) -> None:
610 """Set hidden property according to the value b
612 Args:
613 bool: True if you want to set the property. False otherwise
615 Returns:
616 nothing. If the custom headers is not found it is created so the
617 qcow file can be modified.
618 """
619 self._read_qcow2(path)
620 custom_data_offset = self._add_or_find_custom_header()
621 if custom_data_offset == 0:
622 raise util.SMException("Custom data offset not found... should not reach this")
624 with open(self.filename, "rb+") as qcow2_file:
625 qcow2_file.seek(custom_data_offset)
626 if hidden:
627 qcow2_file.write(b"\x01")
628 else:
629 qcow2_file.write(b"\x00")
631 @override
632 def getSizeVirt(self, path: str) -> int:
633 self._read_qcow2(path)
634 return self.header['virtual_disk_size']
636 @override
637 def setSizeVirt(self, path: str, size: int, jFile: str) -> None:
638 """
639 size: byte
640 jFile: a journal file used for resizing with VHD, not useful for QCOW2
641 """
642 cmd = [QEMU_IMG, "resize", path, str(size)]
643 self._ioretry(cmd)
645 @override
646 def setSizeVirtFast(self, path: str, size: int) -> None:
647 self.setSizeVirt(path, size, "")
649 @override
650 def getMaxResizeSize(self, path: str) -> int:
651 return 0
653 @override
654 def getSizePhys(self, path: str) -> int:
655 size = os.stat(path).st_size
656 if size == 0:
657 size = int(self._ioretry(["blockdev", "--getsize64", path]))
658 return size
660 @override
661 def setSizePhys(self, path: str, size: int, debug: bool = True) -> None:
662 pass #TODO: Doesn't exist for QCow2, do we need to use it?
664 @override
665 def getAllocatedSize(self, path: str) -> int:
666 cmd = [QCOW2_HELPER, "allocated", path]
667 return int(self._ioretry(cmd))
669 @override
670 def getResizeJournalSize(self) -> int:
671 return 0
673 @override
674 def killData(self, path: str) -> None:
675 """Remove all data and reset L1/L2 table.
677 Args:
678 self: The QcowInfo object.
680 Returns:
681 nothing.
682 """
683 self._read_qcow2(path, read_clusters=True)
684 # We need to reset L1 entries and then just truncate the file right
685 # after L1 entries
686 with open(self.filename, "r+b") as file:
687 l1_table_offset = self.header["l1_table_offset"]
688 file.seek(l1_table_offset)
690 l1_table_size = (
691 self.header["l1_size"] * 8
692 ) # size in bytes, each entry is 8 bytes
693 file.write(b"\x00" * l1_table_size)
694 file.truncate(l1_table_offset + l1_table_size)
696 @override
697 def getDepth(self, path: str) -> int:
698 cmd = [QEMU_IMG, "info", "--backing-chain", "--output=json", path]
699 ret = str(self._ioretry(cmd))
700 depth = len(re.findall("\"backing-filename\"", ret))+1
701 #chain depth is beginning at one for VHD, meaning a VHD without parent has depth = 1
702 return depth
704 @override
705 def getBlockBitmap(self, path: str) -> bytes:
706 cmd = [QCOW2_HELPER, "bitmap", path]
707 text = cast(bytes, self._ioretry(cmd, text=False))
708 return zlib.compress(text)
710 def _getTapdisk(self, path: str) -> Tuple[int, int]:
711 """
712 Return a tuple of (PID, Minor) for the given path
713 """
714 pid_openers = util.get_openers_pid(path)
715 if pid_openers:
716 if len(pid_openers) > 1:
717 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
718 pid = pid_openers[0]
719 tapdiskList = TapCtl.list(pid=pid)
720 if len(tapdiskList) > 1: #TODO: There might more than one minor for this blktap?
721 raise xs_errors.XenError("TapdiskAlreadyRunning", "There is multiple minor for this tapdisk process")
722 minor = tapdiskList[0]["minor"]
723 return (pid, minor)
724 raise xs_errors.XenError("TapdiskFailed", "No tapdisk process found for {}".format(path))
726 @override
727 def coalesceOnline(self, path: str) -> int:
728 pid, minor = self._getTapdisk(path)
729 logger = util.LoggerCounter(10)
731 try:
732 TapCtl.commit(pid, minor, QCOW2_TYPE, path)
733 # We need to wait for query to return concluded
734 # We are technically ininterruptible since being interrupted will only stop checking if the job is done.
735 # We need to call `tap-ctl cancel` if we are interrupted, it is done in cleanup.py code.
737 status, nb, _ = TapCtl.query(pid, minor)
738 if status == "undefined":
739 util.SMlog("Tapdisk {} (m: {}) coalesce status undefined for {}".format(pid, minor, path))
740 return 0
742 while status != "concluded":
743 time.sleep(1)
744 status, nb, _ = TapCtl.query(pid, minor, quiet=True)
745 logger.log("Got status {} for tapdisk {} (m: {})".format(status, pid, minor))
746 return nb
747 except TapCtl.CommandFailure:
748 util.SMlog("Query command failed on tapdisk instance {}. Raising...".format(pid))
749 raise
751 @override
752 def cancelCoalesceOnline(self, path: str) -> None:
753 pid, minor = self._getTapdisk(path)
755 try:
756 TapCtl.cancel_commit(pid, minor)
757 except TapCtl.CommandFailure:
758 util.SMlog("Cancel command failed on tapdisk instance {}. Raising...".format(pid))
759 raise
761 @override
762 def coalesce(self, path: str) -> int:
763 # -d on commit make it not empty the original image since we don't intend to keep it
764 cmd = [QEMU_IMG, "commit", "-f", QCOW2_TYPE, path, "-d"]
765 ret = cast(str, self._ioretry(cmd)) # Allows to parse for byte coalesced, our qemu-img is supposed to be patched to output it.
766 lines = ret.splitlines()
767 if re.match("Image committed.", lines[-1]):
768 res_line = lines[-2]
769 else:
770 res_line = lines[-1]
772 results = re.match(r"\((\d+)/(\d+)\)", res_line)
773 if results:
774 committed_bytes = int(results.group(1))
775 return committed_bytes
776 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
778 @override
779 def create(self, path: str, size: int, static: bool, msize: int = 0, block_size: Optional[int] = None) -> None:
780 cmd = [QEMU_IMG, "create", "-f", QCOW2_TYPE, path, str(size)]
781 if static:
782 cmd.extend(["-o", "preallocation=full"])
783 if block_size:
784 cmd.extend(["-o", f"cluster_size={str(block_size)}"])
785 self._ioretry(cmd)
786 self.setHidden(path, False) #We add hidden header at creation
788 @override
789 def snapshot(
790 self,
791 path: str,
792 parent: str,
793 parentRaw: bool,
794 msize: int = 0,
795 checkEmpty: bool = True,
796 is_mirror_image: bool = False
797 ) -> None:
798 # TODO: msize, it's use to preallocate metadata, could we honor this too?
799 # TODO: checkEmpty? If it is False, then the parent could be empty and should still be used for snapshot
800 # 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?
802 cmd = [QEMU_IMG, "create"]
804 if parentRaw:
805 parent_type = RAW_TYPE
806 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE
807 else:
808 parent_type = QCOW2_TYPE
809 cluster_size = self.getBlockSize(parent)
810 args = ["-f", QCOW2_TYPE, "-F", parent_type, "-b", parent]
812 if is_mirror_image:
813 # 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.
814 # This is needed because the blkif blocksize is only 512b, as such it will try to only write blocks smaller than the cluster size.
815 # To write a smaller block, we would need to read the parent image cluster then change the 512b block.
816 # The parent being empty during the mirroring phase, reading from it would read zeros and corrupt the cluster.
817 # 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.
818 # Ensuring we go back to a better cluster_size for performance reasons.
819 # This limit our images max size to 64TiB.
820 cluster_size = 16 * 1024 # 16KiB
821 args.extend(["-o", "extended_l2=on"])
823 args.extend(["-o", f"cluster_size={cluster_size}"])
824 cmd.extend(args)
825 cmd.append(path)
827 self._ioretry(cmd)
828 self.setHidden(path, False) #We add hidden header at creation
830 @override
831 def canSnapshotRaw(self, size: int) -> bool:
832 return True
834 @override
835 def check(
836 self,
837 path: str,
838 ignoreMissingFooter: bool = False,
839 fast: bool = False
840 ) -> CowUtil.CheckResult:
841 cmd = [QEMU_IMG, "check", path]
842 try:
843 self._ioretry(cmd)
844 return CowUtil.CheckResult.Success
845 except util.CommandException as e:
846 if e.code in (errno.EROFS, errno.EMEDIUMTYPE):
847 return CowUtil.CheckResult.Unavailable
848 # 1/EPERM is error in internal during check
849 # 2/ENOENT is QCOW corrupted
850 # 3/ESRCH is QCow has leaked clusters
851 # 63/ENOSR is check unavailable on this image type
852 return CowUtil.CheckResult.Fail
854 @override
855 def revert(self, path: str, jFile: str) -> None:
856 pass #Used to get back from a failed operation using a journal, NOOP for qcow2
858 @override
859 def repair(self, path: str) -> None:
860 cmd = [QEMU_IMG, "check", "-f", QCOW2_TYPE, "-r", "all", path]
861 self._ioretry(cmd)
863 @override
864 def validateAndRoundImageSize(self, size: int) -> int:
865 if size < 0 or size > MAX_QCOW_SIZE:
866 raise xs_errors.XenError(
867 "VDISize",
868 opterr="VDI size must be between {} MB and {} MB".format(MIN_QCOW_SIZE // (1024*1024), MAX_QCOW_SIZE // (1024 * 1024))
869 )
871 return util.roundup(QCOW2_DEFAULT_CLUSTER_SIZE, size)
873 @override
874 def getKeyHash(self, path: str) -> Optional[str]:
875 pass
877 @override
878 def setKey(self, path: str, key_hash: str) -> None:
879 pass
881 @override
882 def isCoalesceableOnRemote(self) -> bool:
883 return True