2525import zlib
2626import json
2727from pathlib import Path
28+ from enum import IntFlag
2829
2930import util
3031import 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