Hide keyboard shortcuts

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 

3 

4import errno 

5import os 

6import re 

7import time 

8import struct 

9import zlib 

10import json 

11from pathlib import Path 

12 

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 

19 

20MAX_QCOW_CHAIN_LENGTH: Final = 30 

21 

22QCOW2_DEFAULT_CLUSTER_SIZE: Final = 64 * 1024 # 64 KiB 

23 

24MIN_QCOW_SIZE: Final = QCOW2_DEFAULT_CLUSTER_SIZE 

25 

26MAX_QCOW_SIZE: Final = 16 * 1024 * 1024 * 1024 * 1024 

27 

28QEMU_IMG: Final = "/usr/bin/qemu-img" 

29QCOW2_HELPER = "/opt/xensource/libexec/qcow2_helper" 

30 

31QCOW2_TYPE: Final = "qcow2" 

32RAW_TYPE: Final = "raw" 

33 

34class QCowUtil(CowUtil): 

35 

36 # We followed specifications found here: 

37 # https://github.com/qemu/qemu/blob/master/docs/interop/qcow2.txt 

38 

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 

43 

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 ) 

53 

54 def __init__(self): 

55 self.qcow_read = False 

56 

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]] = {} 

67 

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 

79 

80 def _get_l1_entries(self, file: BinaryIO) -> List[int]: 

81 """Returns the list of all L1 entries. 

82 

83 Args: 

84 file: The qcow2 file object. 

85 

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) 

91 

92 l1_table_size = self.header["l1_size"] * 8 # Each L1 entry is 8 bytes 

93 l1_table = file.read(l1_table_size) 

94 

95 return [ 

96 struct.unpack(">Q", l1_table[i : i + 8])[0] 

97 for i in range(0, len(l1_table), 8) 

98 ] 

99 

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. 

103 

104 Args: 

105 file: The qcow2 file. 

106 l2_offset: the L2 offset where to look for entries 

107 

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) 

114 

115 return [ 

116 struct.unpack(">Q", l2_table[i : i + 8])[0] 

117 for i in range(0, len(l2_table), 8) 

118 ] 

119 

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 "" 

124 

125 file.seek(backing_file_offset) 

126 parent_name = file.read(backing_file_size) 

127 return parent_name.decode("UTF-8") 

128 

129 @staticmethod 

130 def _read_qcow2_header(file: BinaryIO) -> Dict[str, Any]: 

131 """Returns a dict containing some information from QCow2 header. 

132 

133 Args: 

134 file: The qcow2 file object. 

135 

136 Returns: 

137 dict: magic, version, cluster_bits, l1_size and l1_table_offset. 

138 

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 

157 

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]) 

175 

176 if magic != QCowUtil.QCOW2_MAGIC: 

177 raise ValueError("Not a valid QCOW2 file") 

178 

179 parent_name = QCowUtil._read_qcow2_backingfile(file, backing_file_offset, backing_file_size) 

180 

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 } 

193 

194 @staticmethod 

195 def _is_l1_allocated(entry: int) -> bool: 

196 """Checks if the given L1 entry is allocated. 

197 

198 If the offset is 0 then the L2 table and all clusters described 

199 by this L2 table are unallocated. 

200 

201 Args: 

202 entry: L1 entry 

203 

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 

209 

210 @staticmethod 

211 def _is_l2_allocated(entry: int) -> bool: 

212 """Checks if a given entry is allocated. 

213 

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. 

216 

217 Args: 

218 entry: L2 entry 

219 

220 Returns: 

221 bool: Returns True if the L2 entry is allocated, False otherwise 

222 

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 ) 

230 

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. 

234 

235 Args: 

236 l2_entries: A list of L2 entries. 

237 

238 Returns: 

239 A list of all allocated entries 

240 """ 

241 return [entry for entry in l2_entries if QCowUtil._is_l2_allocated(entry)] 

242 

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) 

247 

248 def _get_number_of_allocated_clusters(self) -> int: 

249 """Get the number of allocated clusters. 

250 

251 Args: 

252 self: A QcowInfo object. 

253 

254 Returns: 

255 An integer that is the list of allocated clusters. 

256 """ 

257 assert(self.qcow_read) 

258 

259 allocated_clusters = 0 

260 

261 for l2_entries in self.l1_to_l2.values(): 

262 allocated_clusters += len(self._get_allocated_clusters(l2_entries)) 

263 

264 return allocated_clusters 

265 

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. 

273 

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 

279 

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) 

286 

287 # Write zeros at the original location 

288 f.seek(old_offset) 

289 f.write(b"\x00" * data_size) 

290 

291 # Write the string to the new location 

292 f.seek(new_offset) 

293 f.write(data) 

294 

295 def _add_or_find_custom_header(self) -> int: 

296 """Add custom header at the end of header extensions 

297 

298 It finds the end of the header extensions and add the custom header. 

299 If the header already exists nothing is done. 

300 

301 Args: 

302 

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 

309 

310 header_length = 72 # This is the default value for version 2 images 

311 

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 ) 

319 

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") 

324 

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) 

328 

329 custom_data_offset = 0 

330 

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") 

334 

335 if ext_type == custom_header_type: 

336 # A custom header is already there 

337 custom_data_offset = qcow2_file.tell() 

338 break 

339 

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() 

344 

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() 

351 

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 ) 

358 

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)) 

363 

364 # Restore saved position 

365 qcow2_file.seek(saved_pos) 

366 

367 qcow2_file.seek(-8, 1) 

368 qcow2_file.write(custom_header) 

369 break 

370 

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) 

374 

375 return custom_data_offset 

376 

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)) 

381 

382 def _set_l2_zero(self, b, i): 

383 return b & ~(1 << i) 

384 

385 def _set_l2_one(self, b, i): 

386 return b | (1 << i) 

387 

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 

396 

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) 

409 

410 # ---- 

411 # Implementation of CowUtil 

412 # ---- 

413 

414 @override 

415 def getMinImageSize(self) -> int: 

416 return MIN_QCOW_SIZE 

417 

418 @override 

419 def getMaxImageSize(self) -> int: 

420 return MAX_QCOW_SIZE 

421 

422 @override 

423 def getBlockSize(self, path: str) -> int: 

424 self._read_qcow2(path) 

425 return 1 << self.header["cluster_bits"] 

426 

427 @override 

428 def getFooterSize(self) -> int: 

429 return 0 

430 

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 

435 

436 @override 

437 def getMaxChainLength(self) -> int: 

438 return MAX_QCOW_CHAIN_LENGTH 

439 

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"]) 

449 

450 @override 

451 def calcOverheadBitmap(self, virtual_size: int) -> int: 

452 return 0 #TODO: What do we send back? 

453 

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 uuid = extractUuidFunction(path) 

466 cowinfo = CowImageInfo(uuid) 

467 cowinfo.path = path 

468 cowinfo.sizeVirt = self.header["virtual_disk_size"] 

469 cowinfo.sizePhys = self.getSizePhys(path) 

470 cowinfo.hidden = self.getHidden(path) 

471 cowinfo.sizeAllocated = self.getAllocatedSize(path) 

472 if includeParent: 

473 parent_path = self.header["parent"] 

474 if parent_path != "": 

475 cowinfo.parentPath = parent_path 

476 cowinfo.parentUuid = extractUuidFunction(parent_path) 

477 

478 return cowinfo 

479 

480 @override 

481 def getInfoFromLVM( 

482 self, lvName: str, extractUuidFunction: Callable[[str], str], vgName: str 

483 ) -> Optional[CowImageInfo]: 

484 lvcache = LVMCache(vgName) 

485 return self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName) 

486 

487 def _getInfoLV( 

488 self, lvcache: LVMCache, extractUuidFunction: Callable[[str], str], vgName: str, lvName: str 

489 ) -> Optional[CowImageInfo]: 

490 lvPath = "/dev/{}/{}".format(vgName, lvName) 

491 lvcache.refresh() 

492 if lvName not in lvcache.lvs: 

493 util.SMlog("{} does not exist anymore".format(lvName)) 

494 return None 

495 

496 vdiUuid = extractUuidFunction(lvPath) 

497 srUuid = vgName.replace(VG_PREFIX, "") 

498 

499 ns = NS_PREFIX_LVM + srUuid 

500 lvcache.activate(ns, vdiUuid, lvName, False) 

501 try: 

502 cowinfo = self.getInfo(lvPath, extractUuidFunction) 

503 finally: 

504 lvcache.deactivate(ns, vdiUuid, lvName, False) 

505 return cowinfo 

506 

507 @override 

508 def getAllInfoFromVG( 

509 self, 

510 pattern: str, 

511 extractUuidFunction: Callable[[str], str], 

512 vgName: Optional[str] = None, 

513 parents: bool = False, 

514 exitOnError: bool = False 

515 ) -> Dict[str, CowImageInfo]: 

516 result: Dict[str, CowImageInfo] = dict() 

517 #TODO: handle exitOnError 

518 if vgName: 518 ↛ 519line 518 didn't jump to line 519, because the condition on line 518 was never true

519 reg = re.compile(pattern) 

520 lvcache = LVMCache(vgName) 

521 lvcache.refresh() 

522 # We get size in lvcache.lvs[lvName].size (in bytes) 

523 # We could read the header from the PV directly 

524 lvList = list(lvcache.lvs.keys()) 

525 for lvName in lvList: 

526 # lvinfo = lvcache.lvs[lvName] 

527 if reg.match(lvName): 

528 cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, lvName) 

529 if cowinfo is None: #We get None if the LV stopped existing in the meanwhile 

530 continue 

531 cowinfo.path = lvName # Function CowUtil.getParentChain expect lvName here, otherwise blktap.{_activate,_deactivate} crashes 

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 parentUuid = parent_cowinfo.parentUuid 

542 parentPath = parent_cowinfo.parentPath 

543 parent_cowinfo.path = parentLvName #Same reason as above, some users expect LvName here instead of path 

544 result[parent_cowinfo.uuid] = parent_cowinfo 

545 

546 return result 

547 else: 

548 pattern_p: Path = Path(pattern) 

549 list_qcow = list(pattern_p.parent.glob(pattern_p.name)) 

550 for qcow in list_qcow: 550 ↛ 551line 550 didn't jump to line 551, because the loop on line 550 never started

551 qcow_str = str(qcow) 

552 info = self.getInfo(qcow_str, extractUuidFunction) 

553 result[info.uuid] = info 

554 return result 

555 

556 @override 

557 def getParent(self, path: str, extractUuidFunction: Callable[[str], str]) -> Optional[str]: 

558 parent = self.getParentNoCheck(path) 

559 if parent: 

560 return extractUuidFunction(parent) 

561 return None 

562 

563 @override 

564 def getParentNoCheck(self, path: str) -> Optional[str]: 

565 self._read_qcow2(path) 

566 parent_path = self.header["parent"] 

567 if parent_path == "": 

568 return None 

569 return parent_path 

570 

571 @override 

572 def hasParent(self, path: str) -> bool: 

573 if self.getParentNoCheck(path): 

574 return True 

575 return False 

576 

577 @override 

578 def setParent(self, path: str, parentPath: str, parentRaw: bool) -> None: 

579 pid_openers = util.get_openers_pid(path) 

580 if pid_openers: 

581 util.SMlog("Rebasing while process {} has the VDI opened".format(pid_openers)) 

582 

583 parentType = QCOW2_TYPE 

584 if parentRaw: 

585 parentType = RAW_TYPE 

586 cmd = [QEMU_IMG, "rebase", "-u", "-f", QCOW2_TYPE, "-F", parentType, "-b", parentPath, path] 

587 self._ioretry(cmd) 

588 

589 @override 

590 def getHidden(self, path: str) -> bool: 

591 """Get hidden property according to the value b 

592 

593 Args: 

594 

595 Returns: 

596 True if hidden is set, False otherwise 

597 """ 

598 self._read_qcow2(path) 

599 custom_data_offset = self._add_or_find_custom_header() 

600 if custom_data_offset == 0: 

601 raise util.SMException("Custom data offset not found... should not reach this") 

602 

603 with open(path, "rb") as qcow2_file: 

604 qcow2_file.seek(custom_data_offset) 

605 hidden = qcow2_file.read(1) 

606 if hidden == b"\x00": 

607 return False 

608 return True 

609 

610 @override 

611 def setHidden(self, path: str, hidden: bool = True) -> None: 

612 """Set hidden property according to the value b 

613 

614 Args: 

615 bool: True if you want to set the property. False otherwise 

616 

617 Returns: 

618 nothing. If the custom headers is not found it is created so the 

619 qcow file can be modified. 

620 """ 

621 self._read_qcow2(path) 

622 custom_data_offset = self._add_or_find_custom_header() 

623 if custom_data_offset == 0: 

624 raise util.SMException("Custom data offset not found... should not reach this") 

625 

626 with open(self.filename, "rb+") as qcow2_file: 

627 qcow2_file.seek(custom_data_offset) 

628 if hidden: 

629 qcow2_file.write(b"\x01") 

630 else: 

631 qcow2_file.write(b"\x00") 

632 

633 @override 

634 def getSizeVirt(self, path: str) -> int: 

635 self._read_qcow2(path) 

636 return self.header['virtual_disk_size'] 

637 

638 @override 

639 def setSizeVirt(self, path: str, size: int, jFile: str) -> None: 

640 """ 

641 size: byte 

642 jFile: a journal file used for resizing with VHD, not useful for QCOW2 

643 """ 

644 cmd = [QEMU_IMG, "resize", path, str(size)] 

645 self._ioretry(cmd) 

646 

647 @override 

648 def setSizeVirtFast(self, path: str, size: int) -> None: 

649 self.setSizeVirt(path, size, "") 

650 

651 @override 

652 def getMaxResizeSize(self, path: str) -> int: 

653 return 0 

654 

655 @override 

656 def getSizePhys(self, path: str) -> int: 

657 size = os.stat(path).st_size 

658 if size == 0: 

659 size = int(self._ioretry(["blockdev", "--getsize64", path])) 

660 return size 

661 

662 @override 

663 def setSizePhys(self, path: str, size: int, debug: bool = True) -> None: 

664 pass #TODO: Doesn't exist for QCow2, do we need to use it? 

665 

666 @override 

667 def getAllocatedSize(self, path: str) -> int: 

668 cmd = [QCOW2_HELPER, "allocated", path] 

669 return int(self._ioretry(cmd)) 

670 

671 @override 

672 def getResizeJournalSize(self) -> int: 

673 return 0 

674 

675 @override 

676 def killData(self, path: str) -> None: 

677 """Remove all data and reset L1/L2 table. 

678 

679 Args: 

680 self: The QcowInfo object. 

681 

682 Returns: 

683 nothing. 

684 """ 

685 self._read_qcow2(path, read_clusters=True) 

686 # We need to reset L1 entries and then just truncate the file right 

687 # after L1 entries 

688 with open(self.filename, "r+b") as file: 

689 l1_table_offset = self.header["l1_table_offset"] 

690 file.seek(l1_table_offset) 

691 

692 l1_table_size = ( 

693 self.header["l1_size"] * 8 

694 ) # size in bytes, each entry is 8 bytes 

695 file.write(b"\x00" * l1_table_size) 

696 file.truncate(l1_table_offset + l1_table_size) 

697 

698 @override 

699 def getDepth(self, path: str) -> int: 

700 cmd = [QEMU_IMG, "info", "--backing-chain", "--output=json", path] 

701 ret = str(self._ioretry(cmd)) 

702 depth = len(re.findall("\"backing-filename\"", ret))+1 

703 #chain depth is beginning at one for VHD, meaning a VHD without parent has depth = 1 

704 return depth 

705 

706 @override 

707 def getBlockBitmap(self, path: str) -> bytes: 

708 cmd = [QCOW2_HELPER, "bitmap", path] 

709 text = cast(bytes, self._ioretry(cmd, text=False)) 

710 return zlib.compress(text) 

711 

712 def _getTapdisk(self, path: str) -> Tuple[int, int]: 

713 """ 

714 Return a tuple of (PID, Minor) for the given path 

715 """ 

716 pid_openers = util.get_openers_pid(path) 

717 if pid_openers: 

718 if len(pid_openers) > 1: 

719 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 

720 pid = pid_openers[0] 

721 tapdiskList = TapCtl.list(pid=pid) 

722 if len(tapdiskList) > 1: #TODO: There might more than one minor for this blktap? 

723 raise xs_errors.XenError("TapdiskAlreadyRunning", "There is multiple minor for this tapdisk process") 

724 minor = tapdiskList[0]["minor"] 

725 return (pid, minor) 

726 raise xs_errors.XenError("TapdiskFailed", "No tapdisk process found for {}".format(path)) 

727 

728 @override 

729 def coalesceOnline(self, path: str) -> int: 

730 pid, minor = self._getTapdisk(path) 

731 logger = util.LoggerCounter(10) 

732 

733 try: 

734 TapCtl.commit(pid, minor, QCOW2_TYPE, path) 

735 # We need to wait for query to return concluded 

736 # We are technically ininterruptible since being interrupted will only stop checking if the job is done. 

737 # We need to call `tap-ctl cancel` if we are interrupted, it is done in cleanup.py code. 

738 

739 status, nb, _ = TapCtl.query(pid, minor) 

740 if status == "undefined": 

741 util.SMlog("Tapdisk {} (m: {}) coalesce status undefined for {}".format(pid, minor, path)) 

742 return 0 

743 

744 while status != "concluded": 

745 time.sleep(1) 

746 status, nb, _ = TapCtl.query(pid, minor, quiet=True) 

747 logger.log("Got status {} for tapdisk {} (m: {})".format(status, pid, minor)) 

748 return nb 

749 except TapCtl.CommandFailure: 

750 util.SMlog("Query command failed on tapdisk instance {}. Raising...".format(pid)) 

751 raise 

752 

753 @override 

754 def cancelCoalesceOnline(self, path: str) -> None: 

755 pid, minor = self._getTapdisk(path) 

756 

757 try: 

758 TapCtl.cancel_commit(pid, minor) 

759 except TapCtl.CommandFailure: 

760 util.SMlog("Cancel command failed on tapdisk instance {}. Raising...".format(pid)) 

761 raise 

762 

763 @override 

764 def coalesce(self, path: str) -> int: 

765 # -d on commit make it not empty the original image since we don't intend to keep it 

766 cmd = [QEMU_IMG, "commit", "-f", QCOW2_TYPE, path, "-d"] 

767 ret = cast(str, self._ioretry(cmd)) # Allows to parse for byte coalesced, our qemu-img is supposed to be patched to output it. 

768 lines = ret.splitlines() 

769 if re.match("Image committed.", lines[-1]): 

770 res_line = lines[-2] 

771 else: 

772 res_line = lines[-1] 

773 

774 results = re.match(r"\((\d+)/(\d+)\)", res_line) 

775 if results: 

776 committed_bytes = int(results.group(1)) 

777 return committed_bytes 

778 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 

779 

780 @override 

781 def create(self, path: str, size: int, static: bool, msize: int = 0, block_size: Optional[int] = None) -> None: 

782 cmd = [QEMU_IMG, "create", "-f", QCOW2_TYPE, path, str(size)] 

783 if static: 

784 cmd.extend(["-o", "preallocation=full"]) 

785 if block_size: 

786 cmd.extend(["-o", f"cluster_size={str(block_size)}"]) 

787 self._ioretry(cmd) 

788 self.setHidden(path, False) #We add hidden header at creation 

789 

790 @override 

791 def snapshot( 

792 self, 

793 path: str, 

794 parent: str, 

795 parentRaw: bool, 

796 msize: int = 0, 

797 checkEmpty: bool = True, 

798 is_mirror_image: bool = False 

799 ) -> None: 

800 # TODO: msize, it's use to preallocate metadata, could we honor this too? 

801 # TODO: checkEmpty? If it is False, then the parent could be empty and should still be used for snapshot 

802 # 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? 

803 

804 cmd = [QEMU_IMG, "create"] 

805 

806 if parentRaw: 

807 parent_type = RAW_TYPE 

808 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE 

809 else: 

810 parent_type = QCOW2_TYPE 

811 cluster_size = self.getBlockSize(parent) 

812 args = ["-f", QCOW2_TYPE, "-F", parent_type, "-b", parent] 

813 

814 if is_mirror_image: 

815 # 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. 

816 # This is needed because the blkif blocksize is only 512b, as such it will try to only write blocks smaller than the cluster size. 

817 # To write a smaller block, we would need to read the parent image cluster then change the 512b block. 

818 # The parent being empty during the mirroring phase, reading from it would read zeros and corrupt the cluster. 

819 # 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. 

820 # Ensuring we go back to a better cluster_size for performance reasons. 

821 # This limit our images max size to 64TiB. 

822 cluster_size = 16 * 1024 # 16KiB 

823 args.extend(["-o", "extended_l2=on"]) 

824 

825 args.extend(["-o", f"cluster_size={cluster_size}"]) 

826 cmd.extend(args) 

827 cmd.append(path) 

828 

829 self._ioretry(cmd) 

830 self.setHidden(path, False) #We add hidden header at creation 

831 

832 @override 

833 def canSnapshotRaw(self, size: int) -> bool: 

834 return True 

835 

836 @override 

837 def check( 

838 self, 

839 path: str, 

840 ignoreMissingFooter: bool = False, 

841 fast: bool = False 

842 ) -> CowUtil.CheckResult: 

843 cmd = [QEMU_IMG, "check", path] 

844 try: 

845 self._ioretry(cmd) 

846 return CowUtil.CheckResult.Success 

847 except util.CommandException as e: 

848 if e.code in (errno.EROFS, errno.EMEDIUMTYPE): 

849 return CowUtil.CheckResult.Unavailable 

850 # 1/EPERM is error in internal during check 

851 # 2/ENOENT is QCOW corrupted 

852 # 3/ESRCH is QCow has leaked clusters 

853 # 63/ENOSR is check unavailable on this image type 

854 return CowUtil.CheckResult.Fail 

855 

856 @override 

857 def revert(self, path: str, jFile: str) -> None: 

858 pass #Used to get back from a failed operation using a journal, NOOP for qcow2 

859 

860 @override 

861 def repair(self, path: str) -> None: 

862 cmd = [QEMU_IMG, "check", "-f", QCOW2_TYPE, "-r", "all", path] 

863 self._ioretry(cmd) 

864 

865 @override 

866 def validateAndRoundImageSize(self, size: int) -> int: 

867 if size < 0 or size > MAX_QCOW_SIZE: 

868 raise xs_errors.XenError( 

869 "VDISize", 

870 opterr="VDI size must be between {} MB and {} MB".format(MIN_QCOW_SIZE // (1024*1024), MAX_QCOW_SIZE // (1024 * 1024)) 

871 ) 

872 

873 return util.roundup(QCOW2_DEFAULT_CLUSTER_SIZE, size) 

874 

875 @override 

876 def getKeyHash(self, path: str) -> Optional[str]: 

877 pass 

878 

879 @override 

880 def setKey(self, path: str, key_hash: str) -> None: 

881 pass 

882 

883 @override 

884 def isCoalesceableOnRemote(self) -> bool: 

885 return True