Skip to content

Commit 69c176f

Browse files
committed
feat(qcow2util): Support Qcow2 subclusters
Create Qcow2 with subclusters if the parent support it. calcOverheadEmpty defaults to extended_l2 because it needs more space. Signed-off-by: Anthoine Bourgeois <anthoine.bourgeois@vates.tech>
1 parent 1680f10 commit 69c176f

1 file changed

Lines changed: 40 additions & 4 deletions

File tree

‎drivers/qcow2util.py‎

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
import zlib
2626
import json
2727
from pathlib import Path
28+
from enum import IntFlag
2829

2930
import util
3031
import xs_errors
@@ -59,6 +60,13 @@ class QCowUtil(CowUtil):
5960
QCOW2_MAGIC = 0x514649FB # b"QFI\xfb": Magic number for QCOW2 files
6061
QCOW2_HEADER_SIZE = 104 # In fact the last information we need is at offset 40-47
6162
QCOW2_BACKING_FILE_OFFSET = 8
63+
QCOW2_INCOMPATIBLE_FEATURE_MASK = 0x0000_0000_0000_001F # Bit 0-4
64+
class IncompatibleFeatures(IntFlag):
65+
DIRTY = 0x01
66+
CORRUPT = 0x02
67+
EXTERNAL_DATA_FILE = 0x04
68+
COMPRESSION_TYPE = 0x08
69+
EXTENDED_L2 = 0x10
6270

6371
def _read_qcow2(self, path: str):
6472
with open(path, "rb") as qcow2_file:
@@ -102,6 +110,7 @@ def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]:
102110
# refcount_table_clusters: u32, // Number of clusters for the refcount table
103111
# nb_snapshots: u32, // Number of snapshots in the image
104112
# snapshots_offset: u64, // Offset to the snapshot table
113+
# incompatible_features: u64, // Bitmask of incompatible features
105114

106115
file.seek(0)
107116
header = file.read(QCowUtil.QCOW2_HEADER_SIZE)
@@ -119,11 +128,27 @@ def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]:
119128
_,
120129
_,
121130
snapshots_offset,
122-
) = struct.unpack(">IIQIIQIIQQIIQ", header[:72])
131+
incompatible_features,
132+
) = struct.unpack(">IIQIIQIIQQIIQQ", header[:80])
123133

124134
if magic != QCowUtil.QCOW2_MAGIC:
125135
raise ValueError("Not a valid QCOW2 file")
126136

137+
if incompatible_features & ~QCowUtil.QCOW2_INCOMPATIBLE_FEATURE_MASK:
138+
raise ValueError("QCOW2: Unknown incompatible feature bit set")
139+
140+
if incompatible_features & QCowUtil.IncompatibleFeatures.DIRTY:
141+
raise ValueError("QCOW2: Image is dirty")
142+
143+
if incompatible_features & QCowUtil.IncompatibleFeatures.CORRUPT:
144+
raise ValueError("QCOW2: Image is corrupt; cannot be opened read/write")
145+
146+
if incompatible_features & QCowUtil.IncompatibleFeatures.EXTERNAL_DATA_FILE:
147+
raise ValueError("QCOW2: Image with external data file is unsupported")
148+
149+
if incompatible_features & QCowUtil.IncompatibleFeatures.COMPRESSION_TYPE:
150+
raise ValueError("QCOW2: Image compression is not supported")
151+
127152
parent_name = QCowUtil._read_qcow2_backingfile(file, backing_file_offset, backing_file_size)
128153

129154
return {
@@ -137,6 +162,7 @@ def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]:
137162
"refcount_table_offset": refcount_table_offset,
138163
"snapshots_offset": snapshots_offset,
139164
"parent": parent_name,
165+
"incompatible_features": incompatible_features,
140166
}
141167

142168
@staticmethod
@@ -248,6 +274,14 @@ def _add_or_find_custom_header(self) -> int:
248274

249275
return custom_data_offset
250276

277+
def _has_extended_l2(self, path: str) -> bool:
278+
self._read_qcow2(path)
279+
return bool(self.header["incompatible_features"] & self.IncompatibleFeatures.EXTENDED_L2)
280+
281+
@staticmethod
282+
def _convert_bool_to_qemu_option_value(option: bool) -> str:
283+
return 'on' if option else 'off'
284+
251285
# ----
252286
# Implementation of CowUtil
253287
# ----
@@ -284,7 +318,7 @@ def calcOverheadEmpty(self, virtual_size: int, block_size: Optional[int] = None)
284318
cluster_size = block_size
285319
else:
286320
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}"]
321+
cmd = [QEMU_IMG, "measure", "-O", "qcow2", "--output", "json", "-o", f"cluster_size={cluster_size},extended_l2=on", "--size", f"{virtual_size}"]
288322
output = json.loads(self._ioretry(cmd))
289323
return int(output["required"])
290324

@@ -662,9 +696,11 @@ def snapshot(
662696
if parentRaw:
663697
parent_type = RAW_TYPE
664698
cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE
699+
extended_l2 = False
665700
else:
666701
parent_type = QCOW2_TYPE
667702
cluster_size = self.getBlockSize(parent)
703+
extended_l2 = self._has_extended_l2(parent)
668704
args = ["-f", QCOW2_TYPE, "-F", parent_type, "-b", parent]
669705

670706
if is_mirror_image:
@@ -676,9 +712,9 @@ def snapshot(
676712
# Ensuring we go back to a better cluster_size for performance reasons.
677713
# This limit our images max size to 64TiB.
678714
cluster_size = 16 * 1024 # 16KiB
679-
args.extend(["-o", "extended_l2=on"])
715+
extended_l2 = True
680716

681-
args.extend(["-o", f"cluster_size={cluster_size}"])
717+
args.extend(["-o", f"cluster_size={cluster_size},extended_l2={self._convert_bool_to_qemu_option_value(extended_l2)}"])
682718
cmd.extend(args)
683719
cmd.append(path)
684720

0 commit comments

Comments
 (0)