Coverage for drivers/lvmcowutil.py : 28%
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/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
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 Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18"""
19Helper functions for LVMSR. This module knows about RAW, VHD and QCOW2 VDI's that live in LV's.
20"""
22from sm_typing import Dict, Final, List, Optional, Tuple, cast
24import os
25import sys
26import time
28import lock
29import util
30import XenAPI # pylint: disable=import-error
32from constants import NS_PREFIX_LVM, VG_LOCATION, VG_PREFIX
33from cowutil import CowImageInfo, CowUtil, getCowUtil
34from journaler import Journaler
35from lvmcache import LVInfo, LVMCache
36from lvutil import calcSizeLV
37from refcounter import RefCounter
38from vditype import VdiType, VDI_COW_TYPES
40# ------------------------------------------------------------------------------
42LOCK_RETRY_ATTEMPTS: Final = 20
44LV_PREFIX: Final = {
45 VdiType.RAW: "LV-",
46 VdiType.VHD: "VHD-",
47 VdiType.QCOW2: "QCOW2-"
48}
50LV_PREFIX_TO_VDI_TYPE: Final = {v: k for k, v in LV_PREFIX.items()}
52# ------------------------------------------------------------------------------
54class VDIInfo:
55 uuid = ""
56 scanError = False
57 vdiType = None
58 lvName = ""
59 sizeLV = -1
60 sizeVirt = -1
61 lvActive = False
62 lvOpen = False
63 lvReadonly = False
64 hidden = False
65 parentUuid = ""
66 refcount = 0
68 def __init__(self, uuid: str):
69 self.uuid = uuid
71# ------------------------------------------------------------------------------
73class LvmCowUtil(object):
74 JOURNAL_INFLATE: Final = "inflate"
75 JOURNAL_RESIZE_TAG: Final = "jvhd"
77 def __init__(self, cowutil: CowUtil):
78 self.cowutil = cowutil
80 def calcVolumeSize(self, sizeVirt: int) -> int:
81 # all LVM COW VDIs have the metadata area preallocated for the maximum
82 # possible virtual size in the VHD case (for fast online VDI.resize)
83 metaOverhead = self.cowutil.calcOverheadEmpty(
84 max(sizeVirt, self.cowutil.getDefaultPreallocationSizeVirt())
85 )
86 bitmapOverhead = self.cowutil.calcOverheadBitmap(sizeVirt)
87 return calcSizeLV(sizeVirt + metaOverhead + bitmapOverhead)
89 def createResizeJournal(self, lvmCache: LVMCache, jName: str) -> str:
90 """
91 Create a LV to hold a VDI resize journal.
92 """
93 size = self.cowutil.getResizeJournalSize()
94 if size <= 0:
95 return ''
96 lvName = self._getResizeJournalVolumeName(jName)
97 lvmCache.create(lvName, size, self.JOURNAL_RESIZE_TAG)
98 return os.path.join(lvmCache.vgPath, lvName)
100 def destroyResizeJournal(self, lvmCache: LVMCache, jName: str) -> None:
101 """
102 Destroy a VDI resize journal.
103 """
104 if jName:
105 lvmCache.remove(self._getResizeJournalVolumeName(jName))
107 @classmethod
108 def getAllResizeJournals(cls, lvmCache: LVMCache) -> List[Tuple[str, str]]:
109 """
110 Get a list of all resize journals in VG vgName as (jName, sjFile) pairs.
111 """
112 journals = []
113 lvList = lvmCache.getTagged(cls.JOURNAL_RESIZE_TAG)
114 for lvName in lvList: 114 ↛ 115line 114 didn't jump to line 115, because the loop on line 114 never started
115 jName = lvName[len(cls.JOURNAL_RESIZE_TAG) + 1:]
116 journals.append((jName, lvName))
117 return journals
119 def setSizeVirt(
120 self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str, size: int, jFile : str
121 ) -> None:
122 """
123 When resizing the image virtual size, we might have to inflate the LV in
124 case the metadata size increases.
125 """
126 lvName = LV_PREFIX[vdiType] + vdiUuid
127 vgName = VG_PREFIX + srUuid
128 path = os.path.join(VG_LOCATION, vgName, lvName)
129 self.inflate(journaler, srUuid, vdiUuid, vdiType, self.calcVolumeSize(size))
130 self.cowutil.setSizeVirt(path, size, jFile)
132 def inflate(self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str, size: int) -> None:
133 """
134 Expand a VDI LV (and its image) to 'size'. If the LV is already bigger
135 than that, it's a no-op. Does not change the virtual size of the VDI.
136 """
137 lvName = LV_PREFIX[vdiType] + vdiUuid
138 vgName = VG_PREFIX + srUuid
139 path = os.path.join(VG_LOCATION, vgName, lvName)
140 lvmCache = journaler.lvmCache
142 currSizeLV = lvmCache.getSize(lvName)
143 newSize = calcSizeLV(size)
144 if newSize <= currSizeLV:
145 return
146 journaler.create(self.JOURNAL_INFLATE, vdiUuid, str(currSizeLV))
147 util.fistpoint.activate("LVHDRT_inflate_after_create_journal", srUuid)
148 lvmCache.setSize(lvName, newSize)
149 util.fistpoint.activate("LVHDRT_inflate_after_setSize", srUuid)
150 footer_size = self.cowutil.getFooterSize()
151 if not util.zeroOut(path, newSize - footer_size, footer_size):
152 raise Exception('failed to zero out image footer')
153 util.fistpoint.activate("LVHDRT_inflate_after_zeroOut", srUuid)
154 self.cowutil.setSizePhys(path, newSize, False)
155 util.fistpoint.activate("LVHDRT_inflate_after_setSizePhys", srUuid)
156 journaler.remove(self.JOURNAL_INFLATE, vdiUuid)
158 def deflate(self, lvmCache: LVMCache, lvName: str, size: int) -> None:
159 """
160 Shrink the LV and the image on it to 'size'. Does not change the
161 virtual size of the VDI.
162 """
163 currSizeLV = lvmCache.getSize(lvName)
164 newSize = calcSizeLV(size)
166 if newSize >= currSizeLV: 166 ↛ 168line 166 didn't jump to line 168, because the condition on line 166 was never false
167 return
168 path = os.path.join(VG_LOCATION, lvmCache.vgName, lvName)
169 # no undo necessary if this fails at any point between now and the end
170 self.cowutil.setSizePhys(path, newSize)
171 lvmCache.setSize(lvName, newSize)
173 def attachThin(self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str) -> None:
174 """
175 Ensure that the VDI LV is expanded to the fully-allocated size.
176 """
177 lvName = LV_PREFIX[vdiType] + vdiUuid
178 vgName = VG_PREFIX + srUuid
179 sr_lock = lock.Lock(lock.LOCK_TYPE_SR, srUuid)
180 lvmCache = journaler.lvmCache
181 self._tryAcquire(sr_lock)
182 lvmCache.refresh()
183 info = self.cowutil.getInfoFromLVM(lvName, self.extractUuid, vgName)
184 if not info:
185 raise Exception(f"unable to get LVM info from {vdiUuid}")
186 newSize = self.calcVolumeSize(info.sizeVirt)
187 currSizeLV = lvmCache.getSize(lvName)
188 if newSize <= currSizeLV:
189 return
190 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
191 try:
192 self.inflate(journaler, srUuid, vdiUuid, vdiType, newSize)
193 finally:
194 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
195 sr_lock.release()
197 def detachThin(self, session: XenAPI.Session, lvmCache: LVMCache, srUuid: str, vdiUuid: str, vdiType: str) -> None:
198 """
199 Shrink the VDI to the minimal size if no one is using it.
200 """
201 lvName = LV_PREFIX[vdiType] + vdiUuid
202 path = os.path.join(VG_LOCATION, VG_PREFIX + srUuid, lvName)
203 sr_lock = lock.Lock(lock.LOCK_TYPE_SR, srUuid)
204 self._tryAcquire(sr_lock)
206 vdiRef = session.xenapi.VDI.get_by_uuid(vdiUuid)
207 vbds = session.xenapi.VBD.get_all_records_where( \
208 "field \"VDI\" = \"%s\"" % vdiRef)
209 numPlugged = 0
210 for vbdRec in vbds.values():
211 if vbdRec["currently_attached"]:
212 numPlugged += 1
214 if numPlugged > 1:
215 raise util.SMException("%s still in use by %d others" % \
216 (vdiUuid, numPlugged - 1))
217 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
218 try:
219 newSize = calcSizeLV(self.cowutil.getSizePhys(path))
220 self.deflate(lvmCache, lvName, newSize)
221 finally:
222 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
223 sr_lock.release()
225 @staticmethod
226 def extractUuid(path: str) -> str:
227 uuid = os.path.basename(path)
228 if uuid.startswith(VG_PREFIX):
229 # we are dealing with realpath
230 uuid = uuid.replace("--", "-")
232 for prefix in LV_PREFIX.values():
233 if uuid.find(prefix) != -1:
234 uuid = uuid.split(prefix)[-1]
235 uuid = uuid.strip()
236 # TODO: validate UUID format
237 return uuid
238 return ''
240 @staticmethod
241 def matchVolume(lvName: str) -> Tuple[Optional[str], Optional[str]]:
242 """
243 Given LV name, return the VDI type and the UUID, or (None, None)
244 if the name doesn't match any known type.
245 """
246 for vdiType, prefix in LV_PREFIX.items():
247 if lvName.startswith(prefix):
248 return (vdiType, lvName.replace(prefix, ""))
249 return (None, None)
251 @classmethod
252 def getVolumeInfo(cls, lvmCache: LVMCache, lvName: Optional[str] = None) -> Dict[str, LVInfo]:
253 """
254 Load LV info for all LVs in the VG or an individual LV.
255 This is a wrapper for lvutil.getLVInfo that filters out LV's that
256 are not LVM COW VDI's and adds the vdi_type information.
257 """
258 allLVs = lvmCache.getLVInfo(lvName)
260 lvs: Dict[str, LVInfo] = dict()
261 for name, lv in allLVs.items(): 261 ↛ 262line 261 didn't jump to line 262, because the loop on line 261 never started
262 vdiType, uuid = cls.matchVolume(name)
263 if not vdiType:
264 continue
265 lv.vdiType = vdiType
266 lvs[cast(str, uuid)] = lv
267 return lvs
269 @classmethod
270 def getVDIInfo(cls, lvmCache: LVMCache) -> Dict[str, VDIInfo]:
271 """
272 Load VDI info (both LV and if the VDI is not raw, VHD/QCOW2 info).
273 """
274 vdis: Dict[str, VDIInfo] = {}
275 lvs = cls.getVolumeInfo(lvmCache)
277 hasCowVdis = False
278 for uuid, lvInfo in lvs.items(): 278 ↛ 279line 278 didn't jump to line 279, because the loop on line 278 never started
279 if VdiType.isCowImage(lvInfo.vdiType):
280 hasCowVdis = True
281 vdiInfo = VDIInfo(uuid)
282 vdiInfo.vdiType = lvInfo.vdiType
283 vdiInfo.lvName = lvInfo.name
284 vdiInfo.sizeLV = lvInfo.size
285 vdiInfo.sizeVirt = lvInfo.size
286 vdiInfo.lvActive = lvInfo.active
287 vdiInfo.lvOpen = lvInfo.open
288 vdiInfo.lvReadonly = lvInfo.readonly
289 vdiInfo.hidden = lvInfo.hidden
290 vdis[uuid] = vdiInfo
292 if not hasCowVdis: 292 ↛ 295line 292 didn't jump to line 295, because the condition on line 292 was never false
293 return vdis
295 scan_result: Dict[str, CowImageInfo] = {}
296 for vdi_type in VDI_COW_TYPES:
297 pattern = "%s*" % LV_PREFIX[vdi_type]
298 scan_result.update(getCowUtil(vdi_type).getAllInfoFromVG(pattern, cls.extractUuid, lvmCache.vgName))
300 uuids = vdis.keys()
301 for uuid in uuids:
302 vdi = vdis[uuid]
303 if VdiType.isCowImage(vdi.vdiType):
304 if not scan_result.get(uuid):
305 lvmCache.refresh()
306 if lvmCache.checkLV(vdi.lvName):
307 util.SMlog("*** COW image info missing: %s" % uuid)
308 vdis[uuid].scanError = True
309 else:
310 util.SMlog("LV disappeared since last scan: %s" % uuid)
311 del vdis[uuid]
312 elif scan_result[uuid].error:
313 util.SMlog("*** cow-scan error: %s" % uuid)
314 vdis[uuid].scanError = True
315 else:
316 vdis[uuid].sizeVirt = scan_result[uuid].sizeVirt
317 vdis[uuid].parentUuid = scan_result[uuid].parentUuid
318 vdis[uuid].hidden = scan_result[uuid].hidden
319 return vdis
321 @staticmethod
322 def refreshVolumeOnSlaves(
323 session: XenAPI.Session, srUuid: str, vgName: str, lvName: str, vdiUuid: str, slaves: List[str]
324 ) -> None:
325 args = {
326 "vgName": vgName,
327 "action1": "activate",
328 "uuid1": vdiUuid,
329 "ns1": NS_PREFIX_LVM + srUuid,
330 "lvName1": lvName,
331 "action2": "refresh",
332 "lvName2": lvName,
333 "action3": "deactivate",
334 "uuid3": vdiUuid,
335 "ns3": NS_PREFIX_LVM + srUuid,
336 "lvName3": lvName
337 }
338 for slave in slaves:
339 util.SMlog("Refreshing %s on slave %s" % (lvName, slave))
340 text = session.xenapi.host.call_plugin(slave, "on-slave", "multi", args)
341 util.SMlog("call-plugin returned: '%s'" % text)
343 @classmethod
344 def refreshVolumeOnAllSlaves(
345 cls, session: XenAPI.Session, srUuid: str, vgName: str, lvName: str, vdiUuid: str
346 ) -> None:
347 cls.refreshVolumeOnSlaves(session, srUuid, vgName, lvName, vdiUuid, util.get_all_slaves(session))
349 @classmethod
350 def _getResizeJournalVolumeName(cls, jName: str) -> str:
351 return f"{cls.JOURNAL_RESIZE_TAG}_{jName}"
353 @staticmethod
354 def _tryAcquire(lock):
355 """
356 We must give up if the SR is locked because it could be locked by the
357 coalesce thread trying to acquire the VDI lock we're holding, so as to
358 avoid deadlock.
359 """
360 for i in range(LOCK_RETRY_ATTEMPTS):
361 gotLock = lock.acquireNoblock()
362 if gotLock:
363 return
364 time.sleep(1)
365 raise util.SRBusyException()
367# ------------------------------------------------------------------------------
369def setInnerNodeRefcounts(lvmCache: LVMCache, srUuid: str) -> List[str]:
370 """
371 [Re]calculate and set the refcounts for inner image nodes based on
372 refcounts of the leaf nodes. We can infer inner node refcounts on slaves
373 directly because they are in use only when VDIs are attached - as opposed
374 to the Master case where the coalesce process can also operate on inner
375 nodes.
376 Return all LVs (paths) that are active but not in use (i.e. that should
377 be deactivated).
378 """
379 vdiInfo = LvmCowUtil.getVDIInfo(lvmCache)
380 for uuid, vdi in vdiInfo.items():
381 vdi.refcount = 0
383 ns = NS_PREFIX_LVM + srUuid
384 for uuid, vdi in vdiInfo.items():
385 if vdi.hidden:
386 continue # only read leaf refcounts
387 refcount = RefCounter.check(uuid, ns)
388 assert(refcount == (0, 0) or refcount == (0, 1))
389 if refcount[1]:
390 vdi.refcount = 1
391 while vdi.parentUuid:
392 vdi = vdiInfo[vdi.parentUuid]
393 vdi.refcount += 1
395 pathsNotInUse = []
396 for uuid, vdi in vdiInfo.items():
397 if vdi.hidden:
398 util.SMlog("Setting refcount for %s to %d" % (uuid, vdi.refcount))
399 RefCounter.set(uuid, vdi.refcount, 0, ns)
400 if vdi.refcount == 0 and vdi.lvActive:
401 path = os.path.join("/dev", lvmCache.vgName, vdi.lvName)
402 pathsNotInUse.append(path)
404 return pathsNotInUse
406# ------------------------------------------------------------------------------
408if __name__ == "__main__": 408 ↛ 410line 408 didn't jump to line 410, because the condition on line 408 was never true
409 # used by the master changeover script
410 cmd = sys.argv[1]
411 if cmd == "fixrefcounts":
412 srUuid = sys.argv[2]
413 try:
414 vgName = VG_PREFIX + srUuid
415 lvmCache = LVMCache(vgName)
416 setInnerNodeRefcounts(lvmCache, srUuid)
417 except:
418 util.logException("setInnerNodeRefcounts")
419 else:
420 util.SMlog("Invalid usage")
421 print("Usage: %s fixrefcounts <sr_uuid>" % sys.argv[0])