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# Copyright (C) Citrix Systems Inc. 

2# 

3# This program is free software; you can redistribute it and/or modify 

4# it under the terms of the GNU Lesser General Public License as published 

5# by the Free Software Foundation; version 2.1 only. 

6# 

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

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

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

10# GNU Lesser General Public License for more details. 

11# 

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

13# along with this program; if not, write to the Free Software Foundation, Inc., 

14# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 

15# 

16# Helper functions pertaining to VHD operations 

17# 

18 

19from sm_typing import Callable, Dict, Final, Optional, cast, override 

20 

21import errno 

22import os 

23import re 

24import zlib 

25 

26import util 

27import xs_errors 

28 

29from cowutil import CowImageInfo, CowUtil 

30 

31# ------------------------------------------------------------------------------ 

32 

33MIN_VHD_SIZE: Final = 2 * 1024 * 1024 

34MAX_VHD_SIZE: Final = 2040 * 1024 * 1024 * 1024 

35VHD_MAX_VOLUME_SIZE: Final = 2 * 1024 * 1024 * 1024 * 1024 

36 

37MAX_VHD_JOURNAL_SIZE: Final = 6 * 1024 * 1024 # 2MB VHD block size, max 2TB VHD size. 

38 

39VHD_BLOCK_SIZE: Final = 2 * 1024 * 1024 

40 

41VHD_FOOTER_SIZE: Final = 512 

42 

43VHD_SECTOR_SIZE: Final = 512 

44 

45MAX_VHD_CHAIN_LENGTH: Final = 30 

46 

47VHD_UTIL: Final = "/usr/bin/vhd-util" 

48 

49OPT_LOG_ERR: Final = "--debug" 

50 

51# ------------------------------------------------------------------------------ 

52 

53class VhdUtil(CowUtil): 

54 @override 

55 def getMinImageSize(self) -> int: 

56 return MIN_VHD_SIZE 

57 

58 @override 

59 def getMaxImageSize(self) -> int: 

60 return MAX_VHD_SIZE 

61 

62 @override 

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

64 return VHD_BLOCK_SIZE 

65 

66 @override 

67 def getFooterSize(self) -> int: 

68 return VHD_FOOTER_SIZE 

69 

70 @override 

71 def getDefaultPreallocationSizeVirt(self) -> int: 

72 return VHD_MAX_VOLUME_SIZE 

73 

74 @override 

75 def getMaxChainLength(self) -> int: 

76 return MAX_VHD_CHAIN_LENGTH 

77 

78 @override 

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

80 """ 

81 Calculate the VHD space overhead (metadata size) for an empty VDI of 

82 size virtual_size. 

83 """ 

84 overhead = 0 

85 size_mb = virtual_size // (1024 * 1024) 

86 

87 # Footer + footer copy + header + possible CoW parent locator fields 

88 overhead = 3 * 1024 

89 

90 # BAT 4 Bytes per block segment 

91 overhead += (size_mb // 2) * 4 

92 overhead = util.roundup(512, overhead) 

93 

94 # BATMAP 1 bit per block segment 

95 overhead += (size_mb // 2) // 8 

96 overhead = util.roundup(4096, overhead) 

97 

98 return overhead 

99 

100 @override 

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

102 num_blocks = virtual_size // VHD_BLOCK_SIZE 

103 if virtual_size % VHD_BLOCK_SIZE: 

104 num_blocks += 1 

105 return num_blocks * 4096 

106 

107 @override 

108 def getInfo( 

109 self, 

110 path: str, 

111 extractUuidFunction: Callable[[str], str], 

112 includeParent: bool = True, 

113 resolveParent: bool = True, 

114 useBackupFooter: bool = False 

115 ) -> CowImageInfo: 

116 """ 

117 Get the VHD info. The parent info may optionally be omitted: vhd-util 

118 tries to verify the parent by opening it, which results in error if the VHD 

119 resides on an inactive LV. 

120 """ 

121 opts = "-vsaf" 

122 if includeParent: 122 ↛ 126line 122 didn't jump to line 126, because the condition on line 122 was never false

123 opts += "p" 

124 if not resolveParent: 124 ↛ 125line 124 didn't jump to line 125, because the condition on line 124 was never true

125 opts += "u" 

126 if useBackupFooter: 

127 opts += "b" 

128 

129 ret = cast(str, self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, opts, "-n", path])) 

130 fields = ret.strip().split("\n") 

131 uuid = extractUuidFunction(path) 

132 vhdInfo = CowImageInfo(uuid) 

133 vhdInfo.sizeVirt = int(fields[0]) * 1024 * 1024 

134 vhdInfo.sizePhys = int(fields[1]) 

135 nextIndex = 2 

136 if includeParent: 136 ↛ 141line 136 didn't jump to line 141, because the condition on line 136 was never false

137 if fields[nextIndex].find("no parent") == -1: 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true

138 vhdInfo.parentPath = fields[nextIndex] 

139 vhdInfo.parentUuid = extractUuidFunction(fields[nextIndex]) 

140 nextIndex += 1 

141 vhdInfo.hidden = bool(int(fields[nextIndex].replace("hidden: ", ""))) 

142 vhdInfo.sizeAllocated = self._convertAllocatedSizeToBytes(int(fields[nextIndex+1])) 

143 vhdInfo.path = path 

144 return vhdInfo 

145 

146 @override 

147 def getInfoFromLVM( 

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

149 ) -> Optional[CowImageInfo]: 

150 """ 

151 Get the VHD info. This function does not require the container LV to be 

152 active, but uses LVs & VGs. 

153 """ 

154 ret = cast(str, self._ioretry([VHD_UTIL, "scan", "-f", "-l", vgName, "-m", lvName])) 

155 return self._parseVHDInfo(ret, extractUuidFunction) 

156 

157 @override 

158 def getAllInfoFromVG( 

159 self, 

160 pattern: str, 

161 extractUuidFunction: Callable[[str], str], 

162 vgName: Optional[str] = None, 

163 parents: bool = False, 

164 exitOnError: bool = False 

165 ) -> Dict[str, CowImageInfo]: 

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

167 cmd = [VHD_UTIL, "scan", "-f", "-m", pattern] 

168 if vgName: 

169 cmd.append("-l") 

170 cmd.append(vgName) 

171 if parents: 

172 cmd.append("-a") 

173 try: 

174 ret = cast(str, self._ioretry(cmd)) 

175 except Exception as e: 

176 util.SMlog("WARN: VHD scan failed: output: %s" % e) 

177 ret = cast(str, self._ioretry(cmd + ["-c"])) 

178 util.SMlog("WARN: VHD scan with NOFAIL flag, output: %s" % ret) 

179 for line in ret.split('\n'): 

180 if not line.strip(): 

181 continue 

182 info = self._parseVHDInfo(line, extractUuidFunction) 

183 if info: 

184 if info.error != 0 and exitOnError: 

185 # Just return an empty dict() so the scan will be done 

186 # again by getParentChain. See CA-177063 for details on 

187 # how this has been discovered during the stress tests. 

188 return dict() 

189 result[info.uuid] = info 

190 else: 

191 util.SMlog("WARN: VHD info line doesn't parse correctly: %s" % line) 

192 return result 

193 

194 @override 

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

196 ret = cast(str, self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-p", "-n", path])) 

197 if ret.find("query failed") != -1 or ret.find("Failed opening") != -1: 

198 raise util.SMException("VHD query returned %s" % ret) 

199 if ret.find("no parent") != -1: 

200 return None 

201 return extractUuidFunction(ret) 

202 

203 @override 

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

205 text = util.pread([VHD_UTIL, "read", "-p", "-n", "%s" % path]) 

206 util.SMlog(text) 

207 for line in text.split("\n"): 

208 if line.find("decoded name :") != -1: 

209 val = line.split(":")[1].strip() 

210 vdi = val.replace("--", "-")[-40:] 

211 if vdi[1:].startswith("LV-"): 

212 vdi = vdi[1:] 

213 return vdi 

214 return None 

215 

216 @override 

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

218 """ 

219 Check if the VHD has a parent. A VHD has a parent iff its type is 

220 'Differencing'. This function does not need the parent to actually 

221 be present (e.g. the parent LV to be activated). 

222 """ 

223 ret = cast(str, self._ioretry([VHD_UTIL, "read", OPT_LOG_ERR, "-p", "-n", path])) 

224 # pylint: disable=no-member 

225 m = re.match(r".*Disk type\s+: (\S+) hard disk.*", ret, flags=re.S) 

226 if m: 

227 vhd_type = m.group(1) 

228 assert vhd_type == "Differencing" or vhd_type == "Dynamic" 

229 return vhd_type == "Differencing" 

230 assert False, f"Ill-formed {VHD_UTIL} output detected during VHD parent parsing" 

231 

232 @override 

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

234 normpath = os.path.normpath(parentPath) 

235 cmd = [VHD_UTIL, "modify", OPT_LOG_ERR, "-p", normpath, "-n", path] 

236 if parentRaw: 

237 cmd.append("-m") 

238 self._ioretry(cmd) 

239 

240 @override 

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

242 ret = cast(str, self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-f", "-n", path])) 

243 return bool(int(ret.split(":")[-1].strip())) 

244 

245 @override 

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

247 opt = "1" 

248 if not hidden: 248 ↛ 249line 248 didn't jump to line 249, because the condition on line 248 was never true

249 opt = "0" 

250 self._ioretry([VHD_UTIL, "set", OPT_LOG_ERR, "-n", path, "-f", "hidden", "-v", opt]) 

251 

252 @override 

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

254 ret = self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-v", "-n", path]) 

255 return int(ret) * 1024 * 1024 

256 

257 @override 

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

259 """ 

260 Resize VHD offline 

261 """ 

262 size_mb = size // (1024 * 1024) 

263 self._ioretry([VHD_UTIL, "resize", OPT_LOG_ERR, "-s", str(size_mb), "-n", path, "-j", jFile]) 

264 

265 @override 

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

267 """ 

268 Resize VHD online. 

269 """ 

270 size_mb = size // (1024 * 1024) 

271 self._ioretry([VHD_UTIL, "resize", OPT_LOG_ERR, "-s", str(size_mb), "-n", path, "-f"]) 

272 

273 @override 

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

275 """ 

276 Get the max virtual size for fast resize. 

277 """ 

278 ret = self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-S", "-n", path]) 

279 return int(ret) * 1024 * 1024 

280 

281 @override 

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

283 return int(self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-s", "-n", path])) 

284 

285 @override 

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

287 """ 

288 Set physical utilisation (applicable to VHD's on fixed-size files). 

289 """ 

290 if debug: 

291 cmd = [VHD_UTIL, "modify", OPT_LOG_ERR, "-s", str(size), "-n", path] 

292 else: 

293 cmd = [VHD_UTIL, "modify", "-s", str(size), "-n", path] 

294 self._ioretry(cmd) 

295 

296 @override 

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

298 ret = self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-a", "-n", path]) 

299 return self._convertAllocatedSizeToBytes(int(ret)) 

300 

301 @override 

302 def getResizeJournalSize(self) -> int: 

303 return MAX_VHD_JOURNAL_SIZE 

304 

305 @override 

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

307 """ 

308 Zero out the disk (kill all data inside the VHD file). 

309 """ 

310 self._ioretry([VHD_UTIL, "modify", OPT_LOG_ERR, "-z", "-n", path]) 

311 

312 @override 

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

314 """ 

315 Get the VHD parent chain depth. 

316 """ 

317 text = cast(str, self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-d", "-n", path])) 

318 depth = -1 

319 if text.startswith("chain depth:"): 

320 depth = int(text.split(":")[1].strip()) 

321 return depth 

322 

323 @override 

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

325 text = cast(bytes, self._ioretry([VHD_UTIL, "read", OPT_LOG_ERR, "-B", "-n", path], text=False)) 

326 return zlib.compress(text) 

327 

328 @override 

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

330 """ 

331 Coalesce the VHD, on success it returns the number of bytes coalesced. 

332 """ 

333 text = cast(str, self._ioretry([VHD_UTIL, "coalesce", OPT_LOG_ERR, "-n", path])) 

334 match = re.match(r"^Coalesced (\d+) sectors", text) 

335 if match: 

336 return int(match.group(1)) * VHD_SECTOR_SIZE 

337 return 0 

338 

339 @override 

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

341 cmd = [VHD_UTIL, "create", OPT_LOG_ERR, "-n", path, "-s", str(size // (1024 * 1024))] 

342 if static: 

343 cmd.append("-r") 

344 if msize: 

345 cmd.append("-S") 

346 cmd.append(str(max(msize, size) // (1024 * 1024))) 

347 self._ioretry(cmd) 

348 

349 @override 

350 def snapshot( 

351 self, 

352 path: str, 

353 parent: str, 

354 parentRaw: bool, 

355 msize: int = 0, 

356 checkEmpty: bool = True, 

357 is_mirror_image: bool = False 

358 ) -> None: 

359 cmd = [VHD_UTIL, "snapshot", OPT_LOG_ERR, "-n", path, "-p", parent] 

360 if parentRaw: 

361 cmd.append("-m") 

362 if msize: 

363 cmd.append("-S") 

364 cmd.append(str(msize // (1024 * 1024))) 

365 if not checkEmpty: 

366 cmd.append("-e") 

367 self._ioretry(cmd) 

368 

369 @override 

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

371 return size <= MAX_VHD_SIZE 

372 

373 @override 

374 def check( 

375 self, 

376 path: str, 

377 ignoreMissingFooter: bool = False, 

378 fast: bool = False 

379 ) -> CowUtil.CheckResult: 

380 cmd = [VHD_UTIL, "check", OPT_LOG_ERR, "-n", path] 

381 if ignoreMissingFooter: 

382 cmd.append("-i") 

383 if fast: 

384 cmd.append("-B") 

385 try: 

386 self._ioretry(cmd) 

387 return CowUtil.CheckResult.Success 

388 except util.CommandException as e: 

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

390 return CowUtil.CheckResult.Unavailable 

391 return CowUtil.CheckResult.Fail 

392 

393 @override 

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

395 self._ioretry([VHD_UTIL, "revert", OPT_LOG_ERR, "-n", path, "-j", jFile]) 

396 

397 @override 

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

399 """ 

400 Repairs a VHD. 

401 """ 

402 self._ioretry([VHD_UTIL, "repair", "-n", path]) 

403 

404 @override 

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

406 """ 

407 Take the supplied vhd size, in bytes, and check it is positive and less 

408 that the maximum supported size, rounding up to the next block boundary. 

409 """ 

410 if size < 0 or size > MAX_VHD_SIZE: 

411 raise xs_errors.XenError( 

412 "VDISize", 

413 opterr="VDI size must be between 1 MB and %d MB" % (MAX_VHD_SIZE // (1024 * 1024)) 

414 ) 

415 

416 if size < MIN_VHD_SIZE: 416 ↛ 417line 416 didn't jump to line 417, because the condition on line 416 was never true

417 size = MIN_VHD_SIZE 

418 

419 return util.roundup(VHD_BLOCK_SIZE, size) 

420 

421 @override 

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

423 """ 

424 Extract the hash of the encryption key from the header of an encrypted VHD. 

425 """ 

426 ret = cast(str, self._ioretry([VHD_UTIL, "key", "-p", "-n", path])).strip() 

427 if ret == "none": 

428 return None 

429 vals = ret.split() 

430 if len(vals) != 2: 

431 util.SMlog("***** malformed output from vhd-util for VHD {}: \"{}\"".format(path, ret)) 

432 return None 

433 [_nonce, key_hash] = vals 

434 return key_hash 

435 

436 @override 

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

438 """ 

439 Set the encryption key for a VHD. 

440 """ 

441 self._ioretry([VHD_UTIL, "key", "-s", "-n", path, "-H", key_hash]) 

442 

443 @override 

444 def isCoalesceableOnRemote(self) -> bool: 

445 return False 

446 

447 @override 

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

449 raise NotImplementedError("Online coalesce not implemented for vhdutil") 

450 

451 @override 

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

453 raise NotImplementedError("Online coalesce not implemented for vhdutil") 

454 

455 @staticmethod 

456 def _convertAllocatedSizeToBytes(size: int): 

457 # Assume we have standard 2MB allocation blocks 

458 return size * 2 * 1024 * 1024 

459 

460 @staticmethod 

461 def _parseVHDInfo(line: str, extractUuidFunction: Callable[[str], str]) -> Optional[CowImageInfo]: 

462 vhdInfo = None 

463 valueMap = line.split() 

464 

465 try: 

466 (key, val) = valueMap[0].split("=") 

467 except: 

468 return None 

469 

470 if key != "vhd": 

471 return None 

472 

473 uuid = extractUuidFunction(val) 

474 if not uuid: 

475 util.SMlog("***** malformed output, no UUID: %s" % valueMap) 

476 return None 

477 vhdInfo = CowImageInfo(uuid) 

478 vhdInfo.path = val 

479 

480 for keyval in valueMap: 

481 (key, val) = keyval.split("=") 

482 if key == "scan-error": 

483 vhdInfo.error = line 

484 util.SMlog("***** VHD scan error: %s" % line) 

485 break 

486 elif key == "capacity": 

487 vhdInfo.sizeVirt = int(val) 

488 elif key == "size": 

489 vhdInfo.sizePhys = int(val) 

490 elif key == "hidden": 

491 vhdInfo.hidden = bool(int(val)) 

492 elif key == "parent" and val != "none": 

493 vhdInfo.parentPath = val 

494 vhdInfo.parentUuid = extractUuidFunction(val) 

495 return vhdInfo