Coverage for libs/sm/lvhdutil.py: 27%
226 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
1#
2# Copyright (C) Citrix Systems Inc.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published
6# by the Free Software Foundation; version 2.1 only.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17"""Helper functions for LVHD SR. This module knows about RAW and VHD VDI's
18that live in LV's."""
19import os
20import sys
21import time
23from sm.core import util
24from sm import vhdutil
25from sm.core.lock import Lock
26from sm.refcounter import RefCounter
28MSIZE_MB = 2 * 1024 * 1024 # max virt size for fast resize
29MSIZE = int(MSIZE_MB * 1024 * 1024)
31VG_LOCATION = "/dev"
32VG_PREFIX = "VG_XenStorage-"
33LVM_SIZE_INCREMENT = 4 * 1024 * 1024
35LV_PREFIX = {
36 vhdutil.VDI_TYPE_VHD: "VHD-",
37 vhdutil.VDI_TYPE_RAW: "LV-",
38}
39VDI_TYPES = [vhdutil.VDI_TYPE_VHD, vhdutil.VDI_TYPE_RAW]
41JRN_INFLATE = "inflate"
43JVHD_TAG = "jvhd"
45LOCK_RETRY_ATTEMPTS = 20
47# ref counting for VDI's: we need a ref count for LV activation/deactivation
48# on the master
49NS_PREFIX_LVM = "lvm-"
52class VDIInfo:
53 uuid = ""
54 scanError = False
55 vdiType = None
56 lvName = ""
57 sizeLV = -1
58 sizeVirt = -1
59 lvActive = False
60 lvOpen = False
61 lvReadonly = False
62 hidden = False
63 parentUuid = ""
65 def __init__(self, uuid):
66 self.uuid = uuid
69def matchLV(lvName):
70 """given LV name, return the VDI type and the UUID, or (None, None)
71 if the name doesn't match any known type"""
72 for vdiType in VDI_TYPES:
73 prefix = LV_PREFIX[vdiType]
74 if lvName.startswith(prefix):
75 return (vdiType, lvName.replace(prefix, ""))
76 return (None, None)
79def extractUuid(path):
80 uuid = os.path.basename(path)
81 if uuid.startswith(VG_PREFIX):
82 # we are dealing with realpath
83 uuid = uuid.replace("--", "-")
85 for t in VDI_TYPES:
86 if uuid.find(LV_PREFIX[t]) != -1:
87 uuid = uuid.split(LV_PREFIX[t])[-1]
88 uuid = uuid.strip()
89 # TODO: validate UUID format
90 return uuid
91 return None
94def calcSizeLV(sizeVHD):
95 return util.roundup(LVM_SIZE_INCREMENT, sizeVHD)
98def calcSizeVHDLV(sizeVirt):
99 # all LVHD VDIs have the metadata area preallocated for the maximum
100 # possible virtual size (for fast online VDI.resize)
101 metaOverhead = vhdutil.calcOverheadEmpty(MSIZE)
102 bitmapOverhead = vhdutil.calcOverheadBitmap(sizeVirt)
103 return calcSizeLV(sizeVirt + metaOverhead + bitmapOverhead)
106def getLVInfo(lvmCache, lvName=None):
107 """Load LV info for all LVs in the VG or an individual LV.
108 This is a wrapper for lvutil.getLVInfo that filters out LV's that
109 are not LVHD VDI's and adds the vdi_type information"""
110 allLVs = lvmCache.getLVInfo(lvName)
112 lvs = dict()
113 for lvName, lv in allLVs.items(): 113 ↛ 114line 113 didn't jump to line 114, because the loop on line 113 never started
114 vdiType, uuid = matchLV(lvName)
115 if not vdiType:
116 continue
117 lv.vdiType = vdiType
118 lvs[uuid] = lv
119 return lvs
122def getVDIInfo(lvmCache):
123 """Load VDI info (both LV and if the VDI is not raw, VHD info)"""
124 vdis = {}
125 lvs = getLVInfo(lvmCache)
127 haveVHDs = False
128 for uuid, lvInfo in lvs.items(): 128 ↛ 129line 128 didn't jump to line 129, because the loop on line 128 never started
129 if lvInfo.vdiType == vhdutil.VDI_TYPE_VHD:
130 haveVHDs = True
131 vdiInfo = VDIInfo(uuid)
132 vdiInfo.vdiType = lvInfo.vdiType
133 vdiInfo.lvName = lvInfo.name
134 vdiInfo.sizeLV = lvInfo.size
135 vdiInfo.sizeVirt = lvInfo.size
136 vdiInfo.lvActive = lvInfo.active
137 vdiInfo.lvOpen = lvInfo.open
138 vdiInfo.lvReadonly = lvInfo.readonly
139 vdiInfo.hidden = lvInfo.hidden
140 vdis[uuid] = vdiInfo
142 if haveVHDs: 142 ↛ 143line 142 didn't jump to line 143, because the condition on line 142 was never true
143 pattern = "%s*" % LV_PREFIX[vhdutil.VDI_TYPE_VHD]
144 vhds = vhdutil.getAllVHDs(pattern, extractUuid, lvmCache.vgName)
145 uuids = vdis.keys()
146 for uuid in uuids:
147 vdi = vdis[uuid]
148 if vdi.vdiType == vhdutil.VDI_TYPE_VHD:
149 if not vhds.get(uuid):
150 lvmCache.refresh()
151 if lvmCache.checkLV(vdi.lvName):
152 util.SMlog("*** VHD info missing: %s" % uuid)
153 vdis[uuid].scanError = True
154 else:
155 util.SMlog("LV disappeared since last scan: %s" % uuid)
156 del vdis[uuid]
157 elif vhds[uuid].error:
158 util.SMlog("*** vhd-scan error: %s" % uuid)
159 vdis[uuid].scanError = True
160 else:
161 vdis[uuid].sizeVirt = vhds[uuid].sizeVirt
162 vdis[uuid].parentUuid = vhds[uuid].parentUuid
163 vdis[uuid].hidden = vhds[uuid].hidden
164 return vdis
167def inflate(journaler, srUuid, vdiUuid, size):
168 """Expand a VDI LV (and its VHD) to 'size'. If the LV is already bigger
169 than that, it's a no-op. Does not change the virtual size of the VDI"""
170 lvName = LV_PREFIX[vhdutil.VDI_TYPE_VHD] + vdiUuid
171 vgName = VG_PREFIX + srUuid
172 path = os.path.join(VG_LOCATION, vgName, lvName)
173 lvmCache = journaler.lvmCache
175 currSizeLV = lvmCache.getSize(lvName)
176 newSize = calcSizeLV(size)
177 if newSize <= currSizeLV:
178 return
179 journaler.create(JRN_INFLATE, vdiUuid, str(currSizeLV))
180 util.fistpoint.activate("LVHDRT_inflate_after_create_journal", srUuid)
181 lvmCache.setSize(lvName, newSize)
182 util.fistpoint.activate("LVHDRT_inflate_after_setSize", srUuid)
183 if not util.zeroOut(path, newSize - vhdutil.VHD_FOOTER_SIZE,
184 vhdutil.VHD_FOOTER_SIZE):
185 raise Exception('failed to zero out VHD footer')
186 util.fistpoint.activate("LVHDRT_inflate_after_zeroOut", srUuid)
187 vhdutil.setSizePhys(path, newSize, False)
188 util.fistpoint.activate("LVHDRT_inflate_after_setSizePhys", srUuid)
189 journaler.remove(JRN_INFLATE, vdiUuid)
192def deflate(lvmCache, lvName, size):
193 """Shrink the LV and the VHD on it to 'size'. Does not change the
194 virtual size of the VDI"""
195 currSizeLV = lvmCache.getSize(lvName)
196 newSize = calcSizeLV(size)
197 if newSize >= currSizeLV: 197 ↛ 199line 197 didn't jump to line 199, because the condition on line 197 was never false
198 return
199 path = os.path.join(VG_LOCATION, lvmCache.vgName, lvName)
200 # no undo necessary if this fails at any point between now and the end
201 vhdutil.setSizePhys(path, newSize)
202 lvmCache.setSize(lvName, newSize)
205def setSizeVirt(journaler, srUuid, vdiUuid, size, jFile):
206 """When resizing the VHD virtual size, we might have to inflate the LV in
207 case the metadata size increases"""
208 lvName = LV_PREFIX[vhdutil.VDI_TYPE_VHD] + vdiUuid
209 vgName = VG_PREFIX + srUuid
210 path = os.path.join(VG_LOCATION, vgName, lvName)
211 inflate(journaler, srUuid, vdiUuid, calcSizeVHDLV(size))
212 vhdutil.setSizeVirt(path, size, jFile)
215def _tryAcquire(lock):
216 """We must give up if the SR is locked because it could be locked by the
217 coalesce thread trying to acquire the VDI lock we're holding, so as to
218 avoid deadlock"""
219 for i in range(LOCK_RETRY_ATTEMPTS):
220 gotLock = lock.acquireNoblock()
221 if gotLock:
222 return
223 time.sleep(1)
224 raise util.SRBusyException()
227def attachThin(journaler, srUuid, vdiUuid):
228 """Ensure that the VDI LV is expanded to the fully-allocated size"""
229 lvName = LV_PREFIX[vhdutil.VDI_TYPE_VHD] + vdiUuid
230 vgName = VG_PREFIX + srUuid
231 lock = Lock(vhdutil.LOCK_TYPE_SR, srUuid)
232 lvmCache = journaler.lvmCache
233 _tryAcquire(lock)
234 lvmCache.refresh()
235 vhdInfo = vhdutil.getVHDInfoLVM(lvName, extractUuid, vgName)
236 newSize = calcSizeVHDLV(vhdInfo.sizeVirt)
237 currSizeLV = lvmCache.getSize(lvName)
238 if newSize <= currSizeLV:
239 return
240 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
241 try:
242 inflate(journaler, srUuid, vdiUuid, newSize)
243 finally:
244 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
245 lock.release()
248def detachThin(session, lvmCache, srUuid, vdiUuid):
249 """Shrink the VDI to the minimal size if no one is using it"""
250 lvName = LV_PREFIX[vhdutil.VDI_TYPE_VHD] + vdiUuid
251 path = os.path.join(VG_LOCATION, VG_PREFIX + srUuid, lvName)
252 lock = Lock(vhdutil.LOCK_TYPE_SR, srUuid)
253 _tryAcquire(lock)
255 vdiRef = session.xenapi.VDI.get_by_uuid(vdiUuid)
256 vbds = session.xenapi.VBD.get_all_records_where( \
257 "field \"VDI\" = \"%s\"" % vdiRef)
258 numPlugged = 0
259 for vbdRec in vbds.values():
260 if vbdRec["currently_attached"]:
261 numPlugged += 1
263 if numPlugged > 1:
264 raise util.SMException("%s still in use by %d others" % \
265 (vdiUuid, numPlugged - 1))
266 lvmCache.activate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
267 try:
268 newSize = calcSizeLV(vhdutil.getSizePhys(path))
269 deflate(lvmCache, lvName, newSize)
270 finally:
271 lvmCache.deactivate(NS_PREFIX_LVM + srUuid, vdiUuid, lvName, False)
272 lock.release()
275def createVHDJournalLV(lvmCache, jName, size):
276 """Create a LV to hold a VHD journal"""
277 lvName = "%s_%s" % (JVHD_TAG, jName)
278 lvmCache.create(lvName, size, JVHD_TAG)
279 return os.path.join(lvmCache.vgPath, lvName)
282def deleteVHDJournalLV(lvmCache, jName):
283 """Delete a VHD journal LV"""
284 lvName = "%s_%s" % (JVHD_TAG, jName)
285 lvmCache.remove(lvName)
288def getAllVHDJournals(lvmCache):
289 """Get a list of all VHD journals in VG vgName as (jName,jFile) pairs"""
290 journals = []
291 lvList = lvmCache.getTagged(JVHD_TAG)
292 for lvName in lvList: 292 ↛ 293line 292 didn't jump to line 293, because the loop on line 292 never started
293 jName = lvName[len(JVHD_TAG) + 1:]
294 journals.append((jName, lvName))
295 return journals
298def lvRefreshOnSlaves(session, srUuid, vgName, lvName, vdiUuid, slaves):
299 args = {"vgName": vgName,
300 "action1": "activate",
301 "uuid1": vdiUuid,
302 "ns1": NS_PREFIX_LVM + srUuid,
303 "lvName1": lvName,
304 "action2": "refresh",
305 "lvName2": lvName,
306 "action3": "deactivate",
307 "uuid3": vdiUuid,
308 "ns3": NS_PREFIX_LVM + srUuid,
309 "lvName3": lvName}
310 for slave in slaves:
311 util.SMlog("Refreshing %s on slave %s" % (lvName, slave))
312 text = session.xenapi.host.call_plugin(slave, "on-slave", "multi", args)
313 util.SMlog("call-plugin returned: '%s'" % text)
316def lvRefreshOnAllSlaves(session, srUuid, vgName, lvName, vdiUuid):
317 slaves = util.get_all_slaves(session)
318 lvRefreshOnSlaves(session, srUuid, vgName, lvName, vdiUuid, slaves)
321def setInnerNodeRefcounts(lvmCache, srUuid):
322 """[Re]calculate and set the refcounts for inner VHD nodes based on
323 refcounts of the leaf nodes. We can infer inner node refcounts on slaves
324 directly because they are in use only when VDIs are attached - as opposed
325 to the Master case where the coalesce process can also operate on inner
326 nodes.
327 Return all LVs (paths) that are active but not in use (i.e. that should
328 be deactivated)"""
329 vdiInfo = getVDIInfo(lvmCache)
330 for uuid, vdi in vdiInfo.items():
331 vdi.refcount = 0
333 ns = NS_PREFIX_LVM + srUuid
334 for uuid, vdi in vdiInfo.items():
335 if vdi.hidden:
336 continue # only read leaf refcounts
337 refcount = RefCounter.check(uuid, ns)
338 assert(refcount == (0, 0) or refcount == (0, 1))
339 if refcount[1]:
340 vdi.refcount = 1
341 while vdi.parentUuid:
342 vdi = vdiInfo[vdi.parentUuid]
343 vdi.refcount += 1
345 pathsNotInUse = []
346 for uuid, vdi in vdiInfo.items():
347 if vdi.hidden:
348 util.SMlog("Setting refcount for %s to %d" % (uuid, vdi.refcount))
349 RefCounter.set(uuid, vdi.refcount, 0, ns)
350 if vdi.refcount == 0 and vdi.lvActive:
351 path = os.path.join("/dev", lvmCache.vgName, vdi.lvName)
352 pathsNotInUse.append(path)
354 return pathsNotInUse