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

1#!/usr/bin/env python3 

2# 

3# Copyright (C) 2024 Vates SAS 

4# 

5# This program is free software: you can redistribute it and/or modify 

6# it under the terms of the GNU General Public License as published by 

7# the Free Software Foundation, either version 3 of the License, or 

8# (at your option) any later version. 

9# This program is distributed in the hope that it will be useful, 

10# but WITHOUT ANY WARRANTY; without even the implied warranty of 

11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

12# GNU General Public License for more details. 

13# 

14# You should have received a copy of the GNU General Public License 

15# along with this program. If not, see <https://www.gnu.org/licenses/>. 

16 

17from sm_typing import Any, Callable, Dict, Final, Optional, Tuple, cast, override 

18from typing import BinaryIO 

19 

20import errno 

21import os 

22import re 

23import time 

24import struct 

25import zlib 

26import json 

27from pathlib import Path 

28 

29import util 

30import xs_errors 

31from blktap2 import TapCtl 

32from cowutil import CowUtil, CowImageInfo 

33from lvmcache import LVMCache 

34from constants import NS_PREFIX_LVM, VG_PREFIX 

35 

36# ------------------------------------------------------------------------------ 

37 

38MAX_QCOW_CHAIN_LENGTH: Final = 30 

39 

40QCOW2_DEFAULT_CLUSTER_SIZE: Final = 64 * 1024 # 64 KiB 

41 

42MIN_QCOW_SIZE: Final = QCOW2_DEFAULT_CLUSTER_SIZE 

43 

44MAX_QCOW_SIZE: Final = 17589500641280 # Max size so that the fully allocated size with metadata is under the max size of EXT4 (17592185061376 bytes fully allocated) 

45 

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

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

48 

49QCOW2_TYPE: Final = "qcow2" 

50RAW_TYPE: Final = "raw" 

51 

52# ------------------------------------------------------------------------------ 

53 

54class QCowUtil(CowUtil): 

55 

56 # We followed specifications found here: 

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

58 

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

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

61 QCOW2_BACKING_FILE_OFFSET = 8 

62 

63 def _read_qcow2(self, path: str): 

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

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

66 self.header = self._read_qcow2_header(qcow2_file) 

67 

68 @staticmethod 

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

70 if backing_file_offset == 0: 

71 return "" 

72 

73 file.seek(backing_file_offset) 

74 parent_name = file.read(backing_file_size) 

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

76 

77 @staticmethod 

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

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

80 

81 Args: 

82 file: The qcow2 file object. 

83 

84 Returns: 

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

86 

87 Raises: 

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

89 """ 

90 # The header is as follow: 

91 # 

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

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

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

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

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

97 # size: u64, // Virtual disk size 

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

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

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

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

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

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

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

105 

106 file.seek(0) 

107 header = file.read(QCowUtil.QCOW2_HEADER_SIZE) 

108 ( 

109 magic, 

110 version, 

111 backing_file_offset, 

112 backing_file_size, 

113 cluster_bits, 

114 size, 

115 _, 

116 l1_size, 

117 l1_table_offset, 

118 refcount_table_offset, 

119 _, 

120 _, 

121 snapshots_offset, 

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

123 

124 if magic != QCowUtil.QCOW2_MAGIC: 

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

126 

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

128 

129 return { 

130 "version": version, 

131 "backing_file_offset": backing_file_offset, 

132 "backing_file_size": backing_file_size, 

133 "virtual_disk_size": size, 

134 "cluster_bits": cluster_bits, 

135 "l1_size": l1_size, 

136 "l1_table_offset": l1_table_offset, 

137 "refcount_table_offset": refcount_table_offset, 

138 "snapshots_offset": snapshots_offset, 

139 "parent": parent_name, 

140 } 

141 

142 @staticmethod 

143 def _move_backing_file( 

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

145 ) -> None: 

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

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

148 if needed. 

149 

150 Args: 

151 f: the file the will be modified 

152 old_offset: the current offset 

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

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

155 

156 Returns: 

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

158 """ 

159 # Read the string at backing_file_offset 

160 f.seek(old_offset) 

161 data = f.read(data_size) 

162 

163 # Write zeros at the original location 

164 f.seek(old_offset) 

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

166 

167 # Write the string to the new location 

168 f.seek(new_offset) 

169 f.write(data) 

170 

171 def _add_or_find_custom_header(self) -> int: 

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

173 

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

175 If the header already exists nothing is done. 

176 

177 Args: 

178 

179 Returns: 

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

181 If data offset is 0 something weird happens. 

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

183 """ 

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

185 

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

187 custom_header_length = 8 

188 custom_header_data = 0 

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

190 custom_header = struct.pack( 

191 ">IIQ", custom_header_type, custom_header_length, custom_header_data 

192 ) 

193 

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

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

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

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

198 

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

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

201 qcow2_file.seek(header_length) 

202 

203 custom_data_offset = 0 

204 

205 while True: 

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

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

208 

209 if ext_type == custom_header_type: 

210 # A custom header is already there 

211 custom_data_offset = qcow2_file.tell() 

212 break 

213 

214 if ext_type == 0x00000000: 

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

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

217 custom_data_offset = qcow2_file.tell() 

218 

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

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

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

222 if self.header["backing_file_offset"]: 

223 # Keep current position 

224 saved_pos = qcow2_file.tell() 

225 

226 bf_offset = self.header["backing_file_offset"] 

227 bf_size = self.header["backing_file_size"] 

228 bf_new_offset = bf_offset + len(custom_header) 

229 self._move_backing_file( 

230 qcow2_file, bf_offset, bf_new_offset, bf_size 

231 ) 

232 

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

234 self.header["backing_file_offset"] = bf_new_offset 

235 qcow2_file.seek(self.QCOW2_BACKING_FILE_OFFSET) 

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

237 

238 # Restore saved position 

239 qcow2_file.seek(saved_pos) 

240 

241 qcow2_file.seek(-8, 1) 

242 qcow2_file.write(custom_header) 

243 break 

244 

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

246 ext_len = (ext_len + 7) & 0xFFFFFFF8 

247 qcow2_file.seek(ext_len, 1) 

248 

249 return custom_data_offset 

250 

251 # ---- 

252 # Implementation of CowUtil 

253 # ---- 

254 

255 @override 

256 def getMinImageSize(self) -> int: 

257 return MIN_QCOW_SIZE 

258 

259 @override 

260 def getMaxImageSize(self) -> int: 

261 return MAX_QCOW_SIZE 

262 

263 @override 

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

265 self._read_qcow2(path) 

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

267 

268 @override 

269 def getFooterSize(self) -> int: 

270 return 0 

271 

272 @override 

273 def getDefaultPreallocationSizeVirt(self) -> int: 

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

275 return MIN_QCOW_SIZE 

276 

277 @override 

278 def getMaxChainLength(self) -> int: 

279 return MAX_QCOW_CHAIN_LENGTH 

280 

281 @override 

282 def calcOverheadEmpty(self, virtual_size: int, block_size: Optional[int] = None) -> int: 

283 if block_size: 

284 cluster_size = block_size 

285 else: 

286 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}"] 

288 output = json.loads(self._ioretry(cmd)) 

289 return int(output["required"]) 

290 

291 @override 

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

293 """ 

294 We are not impacted, this is only for VHD. We send back 0 for QCOW2. 

295 """ 

296 return 0 

297 

298 @override 

299 def getInfo( 

300 self, 

301 path: str, 

302 extractUuidFunction: Callable[[str], str], 

303 includeParent: bool = True, 

304 resolveParent: bool = True, 

305 useBackupFooter: bool = False 

306 ) -> CowImageInfo: 

307 """ 

308 resolveParent is useful to find the absolute path of a parent since the path stored in VHD is relative. 

309 For linstor, this cause issues since the parent might not exist locally. 

310 We currently do not implement this for QCOW2. 

311 """ 

312 self._read_qcow2(path) 

313 uuid = extractUuidFunction(path) 

314 cowinfo = CowImageInfo(uuid) 

315 cowinfo.path = path 

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

317 cowinfo.sizePhys = self.getSizePhys(path) 

318 cowinfo.hidden = self.getHidden(path) 

319 if includeParent: 

320 parent_path = self.header["parent"] 

321 if parent_path != "": 

322 cowinfo.parentPath = parent_path 

323 cowinfo.parentUuid = extractUuidFunction(parent_path) 

324 

325 return cowinfo 

326 

327 @override 

328 def getInfoFromLVM( 

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

330 ) -> Optional[CowImageInfo]: 

331 lvcache = LVMCache(vgName) 

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

333 

334 def _getInfoLV( 

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

336 ) -> Optional[CowImageInfo]: 

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

338 lvcache.refresh() 

339 if lvName not in lvcache.lvs: 

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

341 return None 

342 

343 vdiUuid = extractUuidFunction(lvPath) 

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

345 

346 ns = NS_PREFIX_LVM + srUuid 

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

348 try: 

349 cowinfo = self.getInfo(lvPath, extractUuidFunction) 

350 finally: 

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

352 return cowinfo 

353 

354 @override 

355 def getAllInfoFromVG( 

356 self, 

357 pattern: str, 

358 extractUuidFunction: Callable[[str], str], 

359 vgName: Optional[str] = None, 

360 parents: bool = False, 

361 exitOnError: bool = False 

362 ) -> Dict[str, CowImageInfo]: 

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

364 #TODO: handle exitOnError 

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

366 reg = re.compile(pattern) 

367 lvcache = LVMCache(vgName) 

368 lvcache.refresh() 

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

370 # We could read the header from the PV directly 

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

372 for lvName in lvList: 

373 # lvinfo = lvcache.lvs[lvName] 

374 if reg.match(lvName): 

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

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

377 continue 

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

379 result[cowinfo.uuid] = cowinfo 

380 if parents: 

381 parentUuid = cowinfo.parentUuid 

382 parentPath = cowinfo.parentPath 

383 while parentUuid != "": 

384 parentLvName = parentPath.split("/")[-1] 

385 parent_cowinfo = self._getInfoLV(lvcache, extractUuidFunction, vgName, parentLvName) 

386 if parent_cowinfo is None: #Parent disappeared while scanning 

387 raise util.SMException("Parent of {} wasn't found during scan".format(lvName)) 

388 parentUuid = parent_cowinfo.parentUuid 

389 parentPath = parent_cowinfo.parentPath 

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

391 result[parent_cowinfo.uuid] = parent_cowinfo 

392 

393 return result 

394 else: 

395 pattern_p: Path = Path(pattern) 

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

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

398 qcow_str = str(qcow) 

399 info = self.getInfo(qcow_str, extractUuidFunction) 

400 result[info.uuid] = info 

401 return result 

402 

403 @override 

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

405 parent = self.getParentNoCheck(path) 

406 if parent: 

407 return extractUuidFunction(parent) 

408 return None 

409 

410 @override 

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

412 self._read_qcow2(path) 

413 parent_path = self.header["parent"] 

414 if parent_path == "": 

415 return None 

416 return parent_path 

417 

418 @override 

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

420 if self.getParentNoCheck(path): 

421 return True 

422 return False 

423 

424 @override 

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

426 pid_openers = util.get_openers_pid(path) 

427 if pid_openers: 

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

429 

430 parentType = QCOW2_TYPE 

431 if parentRaw: 

432 parentType = RAW_TYPE 

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

434 self._ioretry(cmd) 

435 

436 @override 

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

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

439 

440 Args: 

441 

442 Returns: 

443 True if hidden is set, False otherwise 

444 """ 

445 self._read_qcow2(path) 

446 custom_data_offset = self._add_or_find_custom_header() 

447 if custom_data_offset == 0: 

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

449 

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

451 qcow2_file.seek(custom_data_offset) 

452 hidden = qcow2_file.read(1) 

453 if hidden == b"\x00": 

454 return False 

455 return True 

456 

457 @override 

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

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

460 

461 Args: 

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

463 

464 Returns: 

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

466 qcow file can be modified. 

467 """ 

468 self._read_qcow2(path) 

469 custom_data_offset = self._add_or_find_custom_header() 

470 if custom_data_offset == 0: 

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

472 

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

474 qcow2_file.seek(custom_data_offset) 

475 if hidden: 

476 qcow2_file.write(b"\x01") 

477 else: 

478 qcow2_file.write(b"\x00") 

479 

480 @override 

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

482 self._read_qcow2(path) 

483 return self.header['virtual_disk_size'] 

484 

485 @override 

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

487 """ 

488 size: byte 

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

490 """ 

491 cmd = [QEMU_IMG, "resize", "-q", path, str(size)] 

492 self._ioretry(cmd) 

493 

494 @override 

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

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

497 

498 @override 

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

500 return 0 

501 

502 @override 

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

504 size = os.stat(path).st_size 

505 if size == 0: 

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

507 return size 

508 

509 @override 

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

511 pass #We do not store the physical size in the image in QCOW2 

512 

513 @override 

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

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

516 return int(self._ioretry(cmd)) 

517 

518 @override 

519 def getResizeJournalSize(self) -> int: 

520 return 0 

521 

522 @override 

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

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

525 

526 Args: 

527 self: The QcowInfo object. 

528 

529 Returns: 

530 nothing. 

531 """ 

532 self._read_qcow2(path) 

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

534 # after L1 entries 

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

536 l1_table_offset = self.header["l1_table_offset"] 

537 file.seek(l1_table_offset) 

538 

539 l1_table_size = ( 

540 self.header["l1_size"] * 8 

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

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

543 file.truncate(l1_table_offset + l1_table_size) 

544 

545 @override 

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

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

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

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

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

551 return depth 

552 

553 @override 

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

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

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

557 return zlib.compress(text) 

558 

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

560 """ 

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

562 """ 

563 pid_openers = util.get_openers_pid(path) 

564 if pid_openers: 

565 if len(pid_openers) > 1: 

566 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 

567 pid = pid_openers[0] 

568 tapdiskList = TapCtl.list(pid=pid) 

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

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

571 minor = tapdiskList[0]["minor"] 

572 return (pid, minor) 

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

574 

575 @override 

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

577 pid, minor = self._getTapdisk(path) 

578 logger = util.LoggerCounter(10) 

579 

580 try: 

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

582 # We need to wait for query to return concluded 

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

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

585 except TapCtl.CommandFailure: 

586 util.SMlog(f"Commit command failed on tapdisk instance {pid} (m: {minor}). Raising...") 

587 raise 

588 

589 try: 

590 status, nb, total = TapCtl.query(pid, minor) 

591 if status == "undefined": 

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

593 return 0 

594 

595 while status != "concluded": 

596 time.sleep(1) 

597 status, nb, total = TapCtl.query(pid, minor, quiet=True) 

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

599 

600 if nb < total: 

601 # The coalesce didn't finish because we might have been aborted. 

602 raise xs_errors.XenError("TapdiskFailed", 

603 f"Online coalesce incomplete on tapdisk {pid} (m: {minor}) for {path}: {nb}/{total} bytes") 

604 return nb 

605 except TapCtl.CommandFailure: 

606 util.SMlog(f"Query command failed on tapdisk instance {pid} (m: {minor}). Raising...") 

607 raise 

608 

609 @override 

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

611 pid, minor = self._getTapdisk(path) 

612 

613 try: 

614 TapCtl.cancel_commit(pid, minor) 

615 except TapCtl.CommandFailure: 

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

617 raise 

618 

619 @override 

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

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

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

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

624 lines = ret.splitlines() 

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

626 res_line = lines[-2] 

627 else: 

628 res_line = lines[-1] 

629 

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

631 if results: 

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

633 return committed_bytes 

634 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 

635 

636 @override 

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

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

639 if static: 

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

641 if block_size: 

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

643 self._ioretry(cmd) 

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

645 

646 @override 

647 def snapshot( 

648 self, 

649 path: str, 

650 parent: str, 

651 parentRaw: bool, 

652 msize: int = 0, 

653 checkEmpty: bool = True, 

654 is_mirror_image: bool = False 

655 ) -> None: 

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

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

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

659 

660 cmd = [QEMU_IMG, "create", "-q"] 

661 

662 if parentRaw: 

663 parent_type = RAW_TYPE 

664 cluster_size = QCOW2_DEFAULT_CLUSTER_SIZE 

665 else: 

666 parent_type = QCOW2_TYPE 

667 cluster_size = self.getBlockSize(parent) 

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

669 

670 if is_mirror_image: 

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

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

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

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

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

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

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

678 cluster_size = 16 * 1024 # 16KiB 

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

680 

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

682 cmd.extend(args) 

683 cmd.append(path) 

684 

685 self._ioretry(cmd) 

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

687 

688 @override 

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

690 return size <= MAX_QCOW_SIZE 

691 

692 @override 

693 def check( 

694 self, 

695 path: str, 

696 ignoreMissingFooter: bool = False, 

697 fast: bool = False 

698 ) -> CowUtil.CheckResult: 

699 cmd = [QEMU_IMG, "check", "-q", path] 

700 try: 

701 self._ioretry(cmd) 

702 return CowUtil.CheckResult.Success 

703 except util.CommandException as e: 

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

705 return CowUtil.CheckResult.Unavailable 

706 # 1/EPERM is error in internal during check 

707 # 2/ENOENT is QCOW corrupted 

708 # 3/ESRCH is QCow has leaked clusters 

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

710 return CowUtil.CheckResult.Fail 

711 

712 @override 

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

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

715 

716 @override 

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

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

719 self._ioretry(cmd) 

720 

721 @override 

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

723 if size < 0 or size > MAX_QCOW_SIZE: 

724 raise xs_errors.XenError( 

725 "VDISize", 

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

727 ) 

728 

729 return util.roundup(QCOW2_DEFAULT_CLUSTER_SIZE, size) 

730 

731 @override 

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

733 pass 

734 

735 @override 

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

737 pass 

738 

739 @override 

740 def isCoalesceableOnRemote(self) -> bool: 

741 return True