Coverage for drivers/vhdutil.py : 51%
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#
19from sm_typing import Callable, Dict, Final, Optional, cast, override
21import errno
22import os
23import re
24import zlib
26import util
27import xs_errors
29from cowutil import CowImageInfo, CowUtil
31# ------------------------------------------------------------------------------
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
37MAX_VHD_JOURNAL_SIZE: Final = 6 * 1024 * 1024 # 2MB VHD block size, max 2TB VHD size.
39VHD_BLOCK_SIZE: Final = 2 * 1024 * 1024
41VHD_FOOTER_SIZE: Final = 512
43VHD_SECTOR_SIZE: Final = 512
45MAX_VHD_CHAIN_LENGTH: Final = 30
47VHD_UTIL: Final = "/usr/bin/vhd-util"
49OPT_LOG_ERR: Final = "--debug"
51# ------------------------------------------------------------------------------
53class VhdUtil(CowUtil):
54 @override
55 def getMinImageSize(self) -> int:
56 return MIN_VHD_SIZE
58 @override
59 def getMaxImageSize(self) -> int:
60 return MAX_VHD_SIZE
62 @override
63 def getBlockSize(self, path: str) -> int:
64 return VHD_BLOCK_SIZE
66 @override
67 def getFooterSize(self) -> int:
68 return VHD_FOOTER_SIZE
70 @override
71 def getDefaultPreallocationSizeVirt(self) -> int:
72 return VHD_MAX_VOLUME_SIZE
74 @override
75 def getMaxChainLength(self) -> int:
76 return MAX_VHD_CHAIN_LENGTH
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)
87 # Footer + footer copy + header + possible CoW parent locator fields
88 overhead = 3 * 1024
90 # BAT 4 Bytes per block segment
91 overhead += (size_mb // 2) * 4
92 overhead = util.roundup(512, overhead)
94 # BATMAP 1 bit per block segment
95 overhead += (size_mb // 2) // 8
96 overhead = util.roundup(4096, overhead)
98 return overhead
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
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"
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
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)
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
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)
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
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"
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)
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()))
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])
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
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])
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"])
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
281 @override
282 def getSizePhys(self, path: str) -> int:
283 return int(self._ioretry([VHD_UTIL, "query", OPT_LOG_ERR, "-s", "-n", path]))
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)
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))
301 @override
302 def getResizeJournalSize(self) -> int:
303 return MAX_VHD_JOURNAL_SIZE
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])
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
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)
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
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)
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)
369 @override
370 def canSnapshotRaw(self, size: int) -> bool:
371 return size <= MAX_VHD_SIZE
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
393 @override
394 def revert(self, path: str, jFile: str) -> None:
395 self._ioretry([VHD_UTIL, "revert", OPT_LOG_ERR, "-n", path, "-j", jFile])
397 @override
398 def repair(self, path: str) -> None:
399 """
400 Repairs a VHD.
401 """
402 self._ioretry([VHD_UTIL, "repair", "-n", path])
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 )
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
419 return util.roundup(VHD_BLOCK_SIZE, size)
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
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])
443 @override
444 def isCoalesceableOnRemote(self) -> bool:
445 return False
447 @override
448 def coalesceOnline(self, path: str) -> int:
449 raise NotImplementedError("Online coalesce not implemented for vhdutil")
451 @override
452 def cancelCoalesceOnline(self, path: str) -> None:
453 raise NotImplementedError("Online coalesce not implemented for vhdutil")
455 @staticmethod
456 def _convertAllocatedSizeToBytes(size: int):
457 # Assume we have standard 2MB allocation blocks
458 return size * 2 * 1024 * 1024
460 @staticmethod
461 def _parseVHDInfo(line: str, extractUuidFunction: Callable[[str], str]) -> Optional[CowImageInfo]:
462 vhdInfo = None
463 valueMap = line.split()
465 try:
466 (key, val) = valueMap[0].split("=")
467 except:
468 return None
470 if key != "vhd":
471 return None
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
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