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 

10from pathlib import Path 

11 

12import util 

13import xs_errors 

14from blktap2 import TapCtl 

15from cowutil import CowUtil, CowImageInfo 

16from lvmcache import LVMCache 

17from constants import NS_PREFIX_LVM, VG_PREFIX 

18 

19MAX_QCOW_CHAIN_LENGTH: Final = 30 

20 

21QCOW_CLUSTER_SIZE: Final = 64 * 1024 # 64 KiB 

22 

23MIN_QCOW_SIZE: Final = QCOW_CLUSTER_SIZE 

24 

25MAX_QCOW_SIZE: Final = 16 * 1024 * 1024 * 1024 * 1024 

26 

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

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

29 

30QCOW2_TYPE: Final = "qcow2" 

31RAW_TYPE: Final = "raw" 

32 

33class QCowUtil(CowUtil): 

34 

35 # We followed specifications found here: 

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

37 

38 QCOW2_MAGIC = 0x514649FB # b"QFI\xfb": Magic number for QCOW2 files 

39 QCOW2_HEADER_SIZE = 104 # In fact the last information we need is at offset 40-47 

40 QCOW2_L2_SIZE = QCOW_CLUSTER_SIZE 

41 QCOW2_BACKING_FILE_OFFSET = 8 

42 

43 ALLOCATED_ENTRY_BIT = ( 

44 0x8000_0000_0000_0000 # Bit 63 is the allocated bit for standard cluster 

45 ) 

46 CLUSTER_TYPE_BIT = 0x4000_0000_0000_0000 # 0 for standard, 1 for compressed cluster 

47 L2_OFFSET_MASK = 0x00FF_FFFF_FFFF_FF00 # Bits 9-55 are offset of L2 table. 

48 CLUSTER_DESCRIPTION_MASK = 0x3FFF_FFFF_FFFF_FFFF # Bit 0-61 is cluster description 

49 STANDARD_CLUSTER_OFFSET_MASK = ( 

50 0x00FF_FFFF_FFFF_FF00 # Bits 9-55 are offset of standard cluster 

51 ) 

52 

53 def __init__(self): 

54 self.qcow_read = False 

55 

56 def _read_qcow2(self, path: str, read_clusters: bool = False): 

57 phys_disk_size = self.getSizePhys(path) 

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

59 self.filename = path # Keep the filename if clean is called 

60 self.header = self._read_qcow2_header(qcow2_file) 

61 if read_clusters: 

62 self.l1 = self._get_l1_entries(qcow2_file) 

63 # The l1_to_l2 allows to get L2 entries for a given L1. If L1 entry 

64 # is not allocated we store an empty list. 

65 self.l1_to_l2: Dict[int, List[int]] = {} 

66 

67 for l1_entry in self.l1: 

68 l2_offset = l1_entry & self.L2_OFFSET_MASK 

69 if l2_offset == 0: 

70 self.l1_to_l2[l1_entry] = [] 

71 elif l2_offset > phys_disk_size: #TODO: This sometime happen for a correct VDI (while coalescing online?) 

72 raise xs_errors.XenError("VDISize", "L2 Offset is bigger than physical disk {}".format(path)) 

73 else: 

74 self.l1_to_l2[l1_entry] = self._get_l2_entries( 

75 qcow2_file, l2_offset 

76 ) 

77 self.qcow_read = True 

78 

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

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

81 

82 Args: 

83 file: The qcow2 file object. 

84 

85 Returns: 

86 list: List of all L1 entries 

87 """ 

88 l1_table_offset = self.header["l1_table_offset"] 

89 file.seek(l1_table_offset) 

90 

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

92 l1_table = file.read(l1_table_size) 

93 

94 return [ 

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

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

97 ] 

98 

99 @staticmethod 

100 def _get_l2_entries(file: BinaryIO, l2_offset: int) -> List[int]: 

101 """Returns the list of all L2 entries at a given L2 offset. 

102 

103 Args: 

104 file: The qcow2 file. 

105 l2_offset: the L2 offset where to look for entries 

106 

107 Returns: 

108 list: List of all L2 entries 

109 """ 

110 # The size of L2 is 65536 bytes and each entry is 8 bytes. 

111 file.seek(l2_offset) 

112 l2_table = file.read(QCowUtil.QCOW2_L2_SIZE) 

113 

114 return [ 

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

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

117 ] 

118 

119 @staticmethod 

120 def _read_qcow2_backingfile(file: BinaryIO, backing_file_offset: int , backing_file_size: int) -> str: 

121 if backing_file_offset == 0: 

122 return "" 

123 

124 file.seek(backing_file_offset) 

125 parent_name = file.read(backing_file_size) 

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

127 

128 @staticmethod 

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

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

131 

132 Args: 

133 file: The qcow2 file object. 

134 

135 Returns: 

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

137 

138 Raises: 

139 ValueError: if qcow2 magic is not recognized or cluster size not supported. 

140 """ 

141 # The header is as follow: 

142 # 

143 # magic: u32, // Magic string "QFI\xfb" 

144 # version: u32, // Version (2 or 3) 

145 # backing_file_offset: u64, // Offset to the backing file name 

146 # backing_file_size: u32, // Size of the backing file name 

147 # cluster_bits: u32, // Bits used for addressing within a cluster 

148 # size: u64, // Virtual disk size 

149 # crypt_method: u32, // 0 = no encryption, 1 = AES encryption 

150 # l1_size: u32, // Number of entries in the L1 table 

151 # l1_table_offset: u64, // Offset to the active L1 table 

152 # refcount_table_offset: u64, // Offset to the refcount table 

153 # refcount_table_clusters: u32, // Number of clusters for the refcount table 

154 # nb_snapshots: u32, // Number of snapshots in the image 

155 # snapshots_offset: u64, // Offset to the snapshot table 

156 

157 file.seek(0) 

158 header = file.read(QCowUtil.QCOW2_HEADER_SIZE) 

159 ( 

160 magic, 

161 version, 

162 backing_file_offset, 

163 backing_file_size, 

164 cluster_bits, 

165 size, 

166 _, 

167 l1_size, 

168 l1_table_offset, 

169 refcount_table_offset, 

170 _, 

171 _, 

172 snapshots_offset, 

173 ) = struct.unpack(">IIQIIQIIQQIIQ", header[:72]) 

174 

175 if magic != QCowUtil.QCOW2_MAGIC: 

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

177 

178 if cluster_bits != 16: 

179 raise ValueError("Only default cluster size of 64K is supported") 

180 

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

182 

183 return { 

184 "version": version, 

185 "backing_file_offset": backing_file_offset, 

186 "backing_file_size": backing_file_size, 

187 "virtual_disk_size": size, 

188 "cluster_bits": cluster_bits, 

189 "l1_size": l1_size, 

190 "l1_table_offset": l1_table_offset, 

191 "refcount_table_offset": refcount_table_offset, 

192 "snapshots_offset": snapshots_offset, 

193 "parent": parent_name, 

194 } 

195 

196 @staticmethod 

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

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

199 

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

201 by this L2 table are unallocated. 

202 

203 Args: 

204 entry: L1 entry 

205 

206 Returns: 

207 bool: True if the L1 entry is allocated (ie has a valid offset). 

208 False otherwise. 

209 """ 

210 return (entry & QCowUtil.L2_OFFSET_MASK) != 0 

211 

212 @staticmethod 

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

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

215 

216 Currently we only support standard clusters. And for standard clusters 

217 the bit 63 is set to 1 for allocated ones or offset is not 0. 

218 

219 Args: 

220 entry: L2 entry 

221 

222 Returns: 

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

224 

225 Raises: 

226 raise an exception if the cluster is not a standard one. 

227 """ 

228 assert entry & QCowUtil.CLUSTER_TYPE_BIT == 0 

229 return (entry & QCowUtil.ALLOCATED_ENTRY_BIT != 0) or ( 

230 entry & QCowUtil.STANDARD_CLUSTER_OFFSET_MASK != 0 

231 ) 

232 

233 @staticmethod 

234 def _get_allocated_clusters(l2_entries: List[int]) -> List[int]: 

235 """Get all allocated clusters in a given list of L2 entries. 

236 

237 Args: 

238 l2_entries: A list of L2 entries. 

239 

240 Returns: 

241 A list of all allocated entries 

242 """ 

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

244 

245 @staticmethod 

246 def _get_cluster_to_byte(clusters: int, cluster_bits: int) -> int: 

247 # (1 << cluster_bits) give cluster size in byte 

248 return clusters * (1 << cluster_bits) 

249 

250 def _get_number_of_allocated_clusters(self) -> int: 

251 """Get the number of allocated clusters. 

252 

253 Args: 

254 self: A QcowInfo object. 

255 

256 Returns: 

257 An integer that is the list of allocated clusters. 

258 """ 

259 assert(self.qcow_read) 

260 

261 allocated_clusters = 0 

262 

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

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

265 

266 return allocated_clusters 

267 

268 @staticmethod 

269 def _move_backing_file( 

270 f: BinaryIO, old_offset: int, new_offset: int, data_size: int 

271 ) -> None: 

272 """Move a number of bytes from old_offset to new_offset and replaces the old 

273 value by 0s. It is up to the caller to save the current position in the file 

274 if needed. 

275 

276 Args: 

277 f: the file the will be modified 

278 old_offset: the current offset 

279 new_offset: the new offset where we want to move data 

280 data_size: Size in bytes of data that we want to move 

281 

282 Returns: 

283 Nothing but the file f is modified and the position in the file also. 

284 """ 

285 # Read the string at backing_file_offset 

286 f.seek(old_offset) 

287 data = f.read(data_size) 

288 

289 # Write zeros at the original location 

290 f.seek(old_offset) 

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

292 

293 # Write the string to the new location 

294 f.seek(new_offset) 

295 f.write(data) 

296 

297 def _add_or_find_custom_header(self) -> int: 

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

299 

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

301 If the header already exists nothing is done. 

302 

303 Args: 

304 

305 Returns: 

306 It returns the data offset where custom header is found or created. 

307 If data offset is 0 something weird happens. 

308 The qcow2 file in self.filename can be modified. 

309 """ 

310 assert self.qcow_read 

311 

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

313 

314 custom_header_type = 0x76617465 # vate: it is easy to recognize with hexdump -C 

315 custom_header_length = 8 

316 custom_header_data = 0 

317 # We don't need padding because we are already aligned 

318 custom_header = struct.pack( 

319 ">IIQ", custom_header_type, custom_header_length, custom_header_data 

320 ) 

321 

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

323 if self.header["version"] == 3: 

324 qcow2_file.seek(100) # 100 is the offset of header_length 

325 header_length = int.from_bytes(qcow2_file.read(4), "big") 

326 

327 # After the image header we found Header extension. So we need to find the end of 

328 # the header extension area and add our custom header. 

329 qcow2_file.seek(header_length) 

330 

331 custom_data_offset = 0 

332 

333 while True: 

334 ext_type = int.from_bytes(qcow2_file.read(4), "big") 

335 ext_len = int.from_bytes(qcow2_file.read(4), "big") 

336 

337 if ext_type == custom_header_type: 

338 # A custom header is already there 

339 custom_data_offset = qcow2_file.tell() 

340 break 

341 

342 if ext_type == 0x00000000: 

343 # End mark found. If we found the end mark it means that we didn't find 

344 # the custom header. So we need to add it. 

345 custom_data_offset = qcow2_file.tell() 

346 

347 # We will overwrite the end marker so rewind a little bit to 

348 # write the new type extension and the new length. But if there is 

349 # a backing file we need to move it to make some space. 

350 if self.header["backing_file_offset"]: 

351 # Keep current position 

352 saved_pos = qcow2_file.tell() 

353 

354 bf_offset = self.header["backing_file_offset"] 

355 bf_size = self.header["backing_file_size"] 

356 bf_new_offset = bf_offset + len(custom_header) 

357 self._move_backing_file( 

358 qcow2_file, bf_offset, bf_new_offset, bf_size 

359 ) 

360 

361 # Update the header to match the new backing file offset 

362 self.header["backing_file_offset"] = bf_new_offset 

363 qcow2_file.seek(self.QCOW2_BACKING_FILE_OFFSET) 

364 qcow2_file.write(struct.pack(">Q", bf_new_offset)) 

365 

366 # Restore saved position 

367 qcow2_file.seek(saved_pos) 

368 

369 qcow2_file.seek(-8, 1) 

370 qcow2_file.write(custom_header) 

371 break 

372 

373 # Round up the header extension size to the next multiple of 8 

374 ext_len = (ext_len + 7) & 0xFFFFFFF8 

375 qcow2_file.seek(ext_len, 1) 

376 

377 return custom_data_offset 

378 

379 def _set_l1_zero(self): 

380 zero = int(0).to_bytes(1, "little") 

381 nb_of_entries_per_cluster = QCOW_CLUSTER_SIZE/8 

382 return list(zero * int(nb_of_entries_per_cluster/8)) 

383 

384 def _set_l2_zero(self, b, i): 

385 return b & ~(1 << i) 

386 

387 def _set_l2_one(self, b, i): 

388 return b | (1 << i) 

389 

390 def _create_bitmap(self) -> bytes: 

391 idx: int = 0 

392 bitmap = list() 

393 b = 0 

394 for l1_entry in self.l1: 

395 if not self._is_l1_allocated(l1_entry): 

396 bitmap.extend(self._set_l1_zero()) 

397 continue 

398 

399 l2_table = self.l1_to_l2[l1_entry] #L2 is cluster_size/8 entries of cluster_size page 

400 for l2_entry in l2_table: 

401 if self._is_l2_allocated(l2_entry): 

402 b = self._set_l2_one(b, idx) 

403 else: 

404 b = self._set_l2_zero(b, idx) 

405 idx += 1 

406 if idx == 8: 

407 bitmap.append(b) 

408 b = 0 

409 idx = 0 

410 return struct.pack("B"*len(bitmap), *bitmap) 

411 

412 # ---- 

413 # Implementation of CowUtil 

414 # ---- 

415 

416 @override 

417 def getMinImageSize(self) -> int: 

418 return MIN_QCOW_SIZE 

419 

420 @override 

421 def getMaxImageSize(self) -> int: 

422 return MAX_QCOW_SIZE 

423 

424 @override 

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

426 return QCOW_CLUSTER_SIZE 

427 

428 @override 

429 def getFooterSize(self) -> int: 

430 return 0 

431 

432 @override 

433 def getDefaultPreallocationSizeVirt(self) -> int: 

434 return MAX_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) -> int: 

442 size_l1 = QCOW_CLUSTER_SIZE 

443 size_header = QCOW_CLUSTER_SIZE 

444 size_l2 = (virtual_size * 8) / QCOW_CLUSTER_SIZE #It is only an estimation 

445 

446 size = size_l1 + size_l2 + size_header 

447 

448 return util.roundup(QCOW_CLUSTER_SIZE, size) 

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

478 

479 return cowinfo 

480 

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) 

487 

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 

496 

497 vdiUuid = extractUuidFunction(lvPath) 

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

499 

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 

507 

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 

553 

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 

560 

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 

568 

569 @override 

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

571 if self.getParentNoCheck(path): 

572 return True 

573 return False 

574 

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

580 

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) 

586 

587 @override 

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

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

590 

591 Args: 

592 

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

600 

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 

607 

608 @override 

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

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

611 

612 Args: 

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

614 

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

623 

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

630 

631 @override 

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

633 self._read_qcow2(path) 

634 return self.header['virtual_disk_size'] 

635 

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) 

644 

645 @override 

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

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

648 

649 @override 

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

651 return 0 

652 

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 

659 

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? 

663 

664 @override 

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

666 cmd = [QCOW2_HELPER, path] 

667 return int(self._ioretry(cmd)) 

668 

669 @override 

670 def getResizeJournalSize(self) -> int: 

671 return 0 

672 

673 @override 

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

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

676 

677 Args: 

678 self: The QcowInfo object. 

679 

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) 

689 

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) 

695 

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 

703 

704 @override 

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

706 self._read_qcow2(path, read_clusters=True) #TODO: Add read L2 info here, we want to use an external application to do this eventually 

707 return zlib.compress(self._create_bitmap()) 

708 

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

710 """ 

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

712 """ 

713 pid_openers = util.get_openers_pid(path) 

714 if pid_openers: 

715 if len(pid_openers) > 1: 

716 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 

717 pid = pid_openers[0] 

718 tapdiskList = TapCtl.list(pid=pid) 

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

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

721 minor = tapdiskList[0]["minor"] 

722 return (pid, minor) 

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

724 

725 @override 

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

727 pid, minor = self._getTapdisk(path) 

728 logger = util.LoggerCounter(10) 

729 

730 try: 

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

732 # We need to wait for query to return concluded 

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

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

735 

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

737 if status == "undefined": 

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

739 return 0 

740 

741 while status != "concluded": 

742 time.sleep(1) 

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

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

745 return nb 

746 except TapCtl.CommandFailure: 

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

748 raise 

749 

750 @override 

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

752 pid, minor = self._getTapdisk(path) 

753 

754 try: 

755 TapCtl.cancel_commit(pid, minor) 

756 except TapCtl.CommandFailure: 

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

758 raise 

759 

760 @override 

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

762 allocated_blocks = self.getAllocatedSize(path) 

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)) #TODO: parse for byte coalesced, our qemu-img is supposed to be patched to output it. 

766 return allocated_blocks 

767 

768 @override 

769 def create(self, path: str, size: int, static: bool, msize: int = 0) -> None: 

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

771 if static: 

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

773 self._ioretry(cmd) 

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

775 

776 @override 

777 def snapshot( 

778 self, 

779 path: str, 

780 parent: str, 

781 parentRaw: bool, 

782 msize: int = 0, 

783 checkEmpty: bool = True 

784 ) -> None: 

785 parent_type = QCOW2_TYPE 

786 if parentRaw: 

787 parent_type = RAW_TYPE 

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

789 cmd = [QEMU_IMG, "create", "-f", QCOW2_TYPE, "-b", parent, "-F", parent_type, path] 

790 self._ioretry(cmd) 

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

792 

793 @override 

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

795 return True 

796 

797 @override 

798 def check( 

799 self, 

800 path: str, 

801 ignoreMissingFooter: bool = False, 

802 fast: bool = False 

803 ) -> CowUtil.CheckResult: 

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

805 try: 

806 self._ioretry(cmd) 

807 return CowUtil.CheckResult.Success 

808 except util.CommandException as e: 

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

810 return CowUtil.CheckResult.Unavailable 

811 # 1/EPERM is error in internal during check 

812 # 2/ENOENT is QCOW corrupted 

813 # 3/ESRCH is QCow has leaked clusters 

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

815 return CowUtil.CheckResult.Fail 

816 

817 @override 

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

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

820 

821 @override 

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

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

824 self._ioretry(cmd) 

825 

826 @override 

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

828 if size < 0 or size > MAX_QCOW_SIZE: 

829 raise xs_errors.XenError( 

830 "VDISize", 

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

832 ) 

833 

834 return util.roundup(QCOW_CLUSTER_SIZE, size) 

835 

836 @override 

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

838 pass 

839 

840 @override 

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

842 pass 

843 

844 @override 

845 def isCoalesceableOnRemote(self) -> bool: 

846 return True