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

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 

16 

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 

22 

23from sm.core import util 

24from sm import vhdutil 

25from sm.core.lock import Lock 

26from sm.refcounter import RefCounter 

27 

28MSIZE_MB = 2 * 1024 * 1024 # max virt size for fast resize 

29MSIZE = int(MSIZE_MB * 1024 * 1024) 

30 

31VG_LOCATION = "/dev" 

32VG_PREFIX = "VG_XenStorage-" 

33LVM_SIZE_INCREMENT = 4 * 1024 * 1024 

34 

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] 

40 

41JRN_INFLATE = "inflate" 

42 

43JVHD_TAG = "jvhd" 

44 

45LOCK_RETRY_ATTEMPTS = 20 

46 

47# ref counting for VDI's: we need a ref count for LV activation/deactivation 

48# on the master 

49NS_PREFIX_LVM = "lvm-" 

50 

51 

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 = "" 

64 

65 def __init__(self, uuid): 

66 self.uuid = uuid 

67 

68 

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) 

77 

78 

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("--", "-") 

84 

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 

92 

93 

94def calcSizeLV(sizeVHD): 

95 return util.roundup(LVM_SIZE_INCREMENT, sizeVHD) 

96 

97 

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) 

104 

105 

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) 

111 

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 

120 

121 

122def getVDIInfo(lvmCache): 

123 """Load VDI info (both LV and if the VDI is not raw, VHD info)""" 

124 vdis = {} 

125 lvs = getLVInfo(lvmCache) 

126 

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 

141 

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 

165 

166 

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 

174 

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) 

190 

191 

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) 

203 

204 

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) 

213 

214 

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() 

225 

226 

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() 

246 

247 

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) 

254 

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 

262 

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() 

273 

274 

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) 

280 

281 

282def deleteVHDJournalLV(lvmCache, jName): 

283 """Delete a VHD journal LV""" 

284 lvName = "%s_%s" % (JVHD_TAG, jName) 

285 lvmCache.remove(lvName) 

286 

287 

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 

296 

297 

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) 

314 

315 

316def lvRefreshOnAllSlaves(session, srUuid, vgName, lvName, vdiUuid): 

317 slaves = util.get_all_slaves(session) 

318 lvRefreshOnSlaves(session, srUuid, vgName, lvName, vdiUuid, slaves) 

319 

320 

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 

332 

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 

344 

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) 

353 

354 return pathsNotInUse