Coverage for drivers/lvmcowutil.py : 26%
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 = "%s_%s" % (self.JOURNAL_RESIZE_TAG, 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 lvName = "%s_%s" % (self.JOURNAL_RESIZE_TAG, jName)
106 lvmCache.remove(lvName)
108 @classmethod
109 def getAllResizeJournals(cls, lvmCache: LVMCache) -> List[Tuple[str, str]]:
110 """
111 Get a list of all resize journals in VG vgName as (jName, sjFile) pairs.
112 """
113 journals = []
114 lvList = lvmCache.getTagged(cls.JOURNAL_RESIZE_TAG)
115 for lvName in lvList: 115 ↛ 116line 115 didn't jump to line 116, because the loop on line 115 never started
116 jName = lvName[len(cls.JOURNAL_RESIZE_TAG) + 1:]
117 journals.append((jName, lvName))
118 return journals
120 def setSizeVirt(
121 self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str, size: int, jFile : str
122 ) -> None:
123 """
124 When resizing the image virtual size, we might have to inflate the LV in
125 case the metadata size increases.
126 """
127 lvName = LV_PREFIX[vdiType] + vdiUuid
128 vgName = VG_PREFIX + srUuid
129 path = os.path.join(VG_LOCATION, vgName, lvName)
130 self.inflate(journaler, srUuid, vdiUuid, vdiType, self.calcVolumeSize(size))
131 self.cowutil.setSizeVirt(path, size, jFile)
133 def inflate(self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str, size: int) -> None:
134 """
135 Expand a VDI LV (and its image) to 'size'. If the LV is already bigger
136 than that, it's a no-op. Does not change the virtual size of the VDI.
137 """
138 lvName = LV_PREFIX[vdiType] + vdiUuid
139 vgName = VG_PREFIX + srUuid
140 path = os.path.join(VG_LOCATION, vgName, lvName)
141 lvmCache = journaler.lvmCache
143 currSizeLV = lvmCache.getSize(lvName)
144 newSize = calcSizeLV(size)
145 if newSize <= currSizeLV:
146 return
147 journaler.create(self.JOURNAL_INFLATE, vdiUuid, str(currSizeLV))
148 util.fistpoint.activate("LVHDRT_inflate_after_create_journal", srUuid)
149 lvmCache.setSize(lvName, newSize)
150 util.fistpoint.activate("LVHDRT_inflate_after_setSize", srUuid)
151 footer_size = self.cowutil.getFooterSize()
152 if not util.zeroOut(path, newSize - footer_size, footer_size):
153 raise Exception('failed to zero out image footer')
154 util.fistpoint.activate("LVHDRT_inflate_after_zeroOut", srUuid)
155 self.cowutil.setSizePhys(path, newSize, False)
156 util.fistpoint.activate("LVHDRT_inflate_after_setSizePhys", srUuid)
157 journaler.remove(self.JOURNAL_INFLATE, vdiUuid)
159 def deflate(self, lvmCache: LVMCache, lvName: str, size: int) -> None:
160 """
161 Shrink the LV and the image on it to 'size'. Does not change the
162 virtual size of the VDI.
163 """
164 currSizeLV = lvmCache.getSize(lvName)
165 newSize = calcSizeLV(size)
167 if newSize >= currSizeLV:
168 return
169 path = os.path.join(VG_LOCATION, lvmCache.vgName, lvName)
170 # no undo necessary if this fails at any point between now and the end
171 self.cowutil.setSizePhys(path, newSize)
172 lvmCache.setSize(lvName, newSize)
174 def attachThin(self, journaler: Journaler, srUuid: str, vdiUuid: str, vdiType: str) -> None:
175 """
176 Ensure that the VDI LV is expanded to the fully-allocated size.
177 """
178 lvName = LV_PREFIX[vdiType] + vdiUuid
179 vgName = VG_PREFIX + srUuid
180 sr_lock = lock.Lock(lock.LOCK_TYPE_SR, srUuid)
181 lvmCache = journaler.lvmCache
182 self._tryAcquire(sr_lock)
183 lvmCache.refresh()
184 info = self.cowutil.getInfoFromLVM(lvName, self.extractUuid, vgName)
185 if not info:
186 raise Exception(f"unable to get LVM info from {vdiUuid}")
187 newSize = self.calcVolumeSize(info.sizeVirt)
188 currSizeLV = lvmCache.getSize(lvName)
189 if newSize <= currSizeLV:
190 return
191 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
192 try:
193 self.inflate(journaler, srUuid, vdiUuid, vdiType, newSize)
194 finally:
195 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
196 sr_lock.release()
198 def detachThin(self, session: XenAPI.Session, lvmCache: LVMCache, srUuid: str, vdiUuid: str, vdiType: str) -> None:
199 """
200 Shrink the VDI to the minimal size if no one is using it.
201 """
202 lvName = LV_PREFIX[vdiType] + vdiUuid
203 path = os.path.join(VG_LOCATION, VG_PREFIX + srUuid, lvName)
204 sr_lock = lock.Lock(lock.LOCK_TYPE_SR, srUuid)
205 self._tryAcquire(sr_lock)
207 vdiRef = session.xenapi.VDI.get_by_uuid(vdiUuid)
208 vbds = session.xenapi.VBD.get_all_records_where( \
209 "field \"VDI\" = \"%s\"" % vdiRef)
210 numPlugged = 0
211 for vbdRec in vbds.values():
212 if vbdRec["currently_attached"]:
213 numPlugged += 1
215 if numPlugged > 1:
216 raise util.SMException("%s still in use by %d others" % \
217 (vdiUuid, numPlugged - 1))
218 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
219 try:
220 newSize = calcSizeLV(self.cowutil.getSizePhys(path))
221 self.deflate(lvmCache, lvName, newSize)
222 finally:
223 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
224 sr_lock.release()
226 @staticmethod
227 def extractUuid(path: str) -> str:
228 uuid = os.path.basename(path)
229 if uuid.startswith(VG_PREFIX):
230 # we are dealing with realpath
231 uuid = uuid.replace("--", "-")
233 for prefix in LV_PREFIX.values():
234 if uuid.find(prefix) != -1:
235 uuid = uuid.split(prefix)[-1]
236 uuid = uuid.strip()
237 # TODO: validate UUID format
238 return uuid
239 return ''
241 @staticmethod
242 def matchVolume(lvName: str) -> Tuple[Optional[str], Optional[str]]:
243 """
244 Given LV name, return the VDI type and the UUID, or (None, None)
245 if the name doesn't match any known type.
246 """
247 for vdiType, prefix in LV_PREFIX.items():
248 if lvName.startswith(prefix):
249 return (vdiType, lvName.replace(prefix, ""))
250 return (None, None)
252 @classmethod
253 def getVolumeInfo(cls, lvmCache: LVMCache, lvName: Optional[str] = None) -> Dict[str, LVInfo]:
254 """
255 Load LV info for all LVs in the VG or an individual LV.
256 This is a wrapper for lvutil.getLVInfo that filters out LV's that
257 are not LVM COW VDI's and adds the vdi_type information.
258 """
259 allLVs = lvmCache.getLVInfo(lvName)
261 lvs: Dict[str, LVInfo] = dict()
262 for name, lv in allLVs.items(): 262 ↛ 263line 262 didn't jump to line 263, because the loop on line 262 never started
263 vdiType, uuid = cls.matchVolume(name)
264 if not vdiType:
265 continue
266 lv.vdiType = vdiType
267 lvs[cast(str, uuid)] = lv
268 return lvs
270 @classmethod
271 def getVDIInfo(cls, lvmCache: LVMCache) -> Dict[str, VDIInfo]:
272 """
273 Load VDI info (both LV and if the VDI is not raw, VHD/QCOW2 info).
274 """
275 vdis: Dict[str, VDIInfo] = {}
276 lvs = cls.getVolumeInfo(lvmCache)
278 hasCowVdis = False
279 for uuid, lvInfo in lvs.items(): 279 ↛ 280line 279 didn't jump to line 280, because the loop on line 279 never started
280 if VdiType.isCowImage(lvInfo.vdiType):
281 hasCowVdis = True
282 vdiInfo = VDIInfo(uuid)
283 vdiInfo.vdiType = lvInfo.vdiType
284 vdiInfo.lvName = lvInfo.name
285 vdiInfo.sizeLV = lvInfo.size
286 vdiInfo.sizeVirt = lvInfo.size
287 vdiInfo.lvActive = lvInfo.active
288 vdiInfo.lvOpen = lvInfo.open
289 vdiInfo.lvReadonly = lvInfo.readonly
290 vdiInfo.hidden = lvInfo.hidden
291 vdis[uuid] = vdiInfo
293 if not hasCowVdis: 293 ↛ 296line 293 didn't jump to line 296, because the condition on line 293 was never false
294 return vdis
296 scan_result: Dict[str, CowImageInfo] = {}
297 for vdi_type in VDI_COW_TYPES:
298 pattern = "%s*" % LV_PREFIX[vdi_type]
299 scan_result.update(getCowUtil(vdi_type).getAllInfoFromVG(pattern, cls.extractUuid, lvmCache.vgName))
301 uuids = vdis.keys()
302 for uuid in uuids:
303 vdi = vdis[uuid]
304 if VdiType.isCowImage(vdi.vdiType):
305 if not scan_result.get(uuid):
306 lvmCache.refresh()
307 if lvmCache.checkLV(vdi.lvName):
308 util.SMlog("*** COW image info missing: %s" % uuid)
309 vdis[uuid].scanError = True
310 else:
311 util.SMlog("LV disappeared since last scan: %s" % uuid)
312 del vdis[uuid]
313 elif scan_result[uuid].error:
314 util.SMlog("*** cow-scan error: %s" % uuid)
315 vdis[uuid].scanError = True
316 else:
317 vdis[uuid].sizeVirt = scan_result[uuid].sizeVirt
318 vdis[uuid].parentUuid = scan_result[uuid].parentUuid
319 vdis[uuid].hidden = scan_result[uuid].hidden
320 return vdis
322 @staticmethod
323 def refreshVolumeOnSlaves(
324 session: XenAPI.Session, srUuid: str, vgName: str, lvName: str, vdiUuid: str, slaves: List[str]
325 ) -> None:
326 args = {
327 "vgName": vgName,
328 "action1": "activate",
329 "uuid1": vdiUuid,
330 "ns1": NS_PREFIX_LVM + srUuid,
331 "lvName1": lvName,
332 "action2": "refresh",
333 "lvName2": lvName,
334 "action3": "deactivate",
335 "uuid3": vdiUuid,
336 "ns3": NS_PREFIX_LVM + srUuid,
337 "lvName3": lvName
338 }
339 for slave in slaves:
340 util.SMlog("Refreshing %s on slave %s" % (lvName, slave))
341 text = session.xenapi.host.call_plugin(slave, "on-slave", "multi", args)
342 util.SMlog("call-plugin returned: '%s'" % text)
344 @classmethod
345 def refreshVolumeOnAllSlaves(
346 cls, session: XenAPI.Session, srUuid: str, vgName: str, lvName: str, vdiUuid: str
347 ) -> None:
348 cls.refreshVolumeOnSlaves(session, srUuid, vgName, lvName, vdiUuid, util.get_all_slaves(session))
350 @staticmethod
351 def _tryAcquire(lock):
352 """
353 We must give up if the SR is locked because it could be locked by the
354 coalesce thread trying to acquire the VDI lock we're holding, so as to
355 avoid deadlock.
356 """
357 for i in range(LOCK_RETRY_ATTEMPTS):
358 gotLock = lock.acquireNoblock()
359 if gotLock:
360 return
361 time.sleep(1)
362 raise util.SRBusyException()
364# ------------------------------------------------------------------------------
366def setInnerNodeRefcounts(lvmCache: LVMCache, srUuid: str) -> List[str]:
367 """
368 [Re]calculate and set the refcounts for inner image nodes based on
369 refcounts of the leaf nodes. We can infer inner node refcounts on slaves
370 directly because they are in use only when VDIs are attached - as opposed
371 to the Master case where the coalesce process can also operate on inner
372 nodes.
373 Return all LVs (paths) that are active but not in use (i.e. that should
374 be deactivated).
375 """
376 vdiInfo = LvmCowUtil.getVDIInfo(lvmCache)
377 for uuid, vdi in vdiInfo.items():
378 vdi.refcount = 0
380 ns = NS_PREFIX_LVM + srUuid
381 for uuid, vdi in vdiInfo.items():
382 if vdi.hidden:
383 continue # only read leaf refcounts
384 refcount = RefCounter.check(uuid, ns)
385 assert(refcount == (0, 0) or refcount == (0, 1))
386 if refcount[1]:
387 vdi.refcount = 1
388 while vdi.parentUuid:
389 vdi = vdiInfo[vdi.parentUuid]
390 vdi.refcount += 1
392 pathsNotInUse = []
393 for uuid, vdi in vdiInfo.items():
394 if vdi.hidden:
395 util.SMlog("Setting refcount for %s to %d" % (uuid, vdi.refcount))
396 RefCounter.set(uuid, vdi.refcount, 0, ns)
397 if vdi.refcount == 0 and vdi.lvActive:
398 path = os.path.join("/dev", lvmCache.vgName, vdi.lvName)
399 pathsNotInUse.append(path)
401 return pathsNotInUse
403# ------------------------------------------------------------------------------
405if __name__ == "__main__": 405 ↛ 407line 405 didn't jump to line 407, because the condition on line 405 was never true
406 # used by the master changeover script
407 cmd = sys.argv[1]
408 if cmd == "fixrefcounts":
409 srUuid = sys.argv[2]
410 try:
411 vgName = VG_PREFIX + srUuid
412 lvmCache = LVMCache(vgName)
413 setInnerNodeRefcounts(lvmCache, srUuid)
414 except:
415 util.logException("setInnerNodeRefcounts")
416 else:
417 util.SMlog("Invalid usage")
418 print("Usage: %s fixrefcounts <sr_uuid>" % sys.argv[0])