Hide keyboard shortcuts

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 

17 

18""" 

19Helper functions for LVMSR. This module knows about RAW, VHD and QCOW2 VDI's that live in LV's. 

20""" 

21 

22from sm_typing import Dict, Final, List, Optional, Tuple, cast 

23 

24import os 

25import sys 

26import time 

27 

28import lock 

29import util 

30import XenAPI # pylint: disable=import-error 

31 

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 

39 

40# ------------------------------------------------------------------------------ 

41 

42LOCK_RETRY_ATTEMPTS: Final = 20 

43 

44LV_PREFIX: Final = { 

45 VdiType.RAW: "LV-", 

46 VdiType.VHD: "VHD-", 

47 VdiType.QCOW2: "QCOW2-" 

48} 

49 

50LV_PREFIX_TO_VDI_TYPE: Final = {v: k for k, v in LV_PREFIX.items()} 

51 

52# ------------------------------------------------------------------------------ 

53 

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 

67 

68 def __init__(self, uuid: str): 

69 self.uuid = uuid 

70 

71# ------------------------------------------------------------------------------ 

72 

73class LvmCowUtil(object): 

74 JOURNAL_INFLATE: Final = "inflate" 

75 JOURNAL_RESIZE_TAG: Final = "jvhd" 

76 

77 def __init__(self, cowutil: CowUtil): 

78 self.cowutil = cowutil 

79 

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) 

88 

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) 

99 

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) 

107 

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 

119 

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) 

132 

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 

142 

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) 

158 

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) 

166 

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) 

173 

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

197 

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) 

206 

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 

214 

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

225 

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

232 

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 '' 

240 

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) 

251 

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) 

260 

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 

269 

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) 

277 

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 

292 

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 

295 

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

300 

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 

321 

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) 

343 

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

349 

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

363 

364# ------------------------------------------------------------------------------ 

365 

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 

379 

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 

391 

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) 

400 

401 return pathsNotInUse 

402 

403# ------------------------------------------------------------------------------ 

404 

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