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 = self._getResizeJournalVolumeName(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 lvmCache.remove(self._getResizeJournalVolumeName(jName)) 

106 

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 

118 

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) 

131 

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 

141 

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) 

157 

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) 

165 

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) 

172 

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

196 

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) 

205 

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 

213 

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

224 

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

231 

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

239 

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) 

250 

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) 

259 

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 

268 

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) 

276 

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 

291 

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 

294 

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

299 

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 

320 

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) 

342 

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

348 

349 @classmethod 

350 def _getResizeJournalVolumeName(cls, jName: str) -> str: 

351 return f"{cls.JOURNAL_RESIZE_TAG}_{jName}" 

352 

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

366 

367# ------------------------------------------------------------------------------ 

368 

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 

382 

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 

394 

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) 

403 

404 return pathsNotInUse 

405 

406# ------------------------------------------------------------------------------ 

407 

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