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# Script to coalesce and garbage collect COW-based SR's in the background 

19# 

20 

21from sm_typing import Any, Dict, Optional, List, override 

22 

23import os 

24import os.path 

25import sys 

26import time 

27import signal 

28import subprocess 

29import getopt 

30import datetime 

31import traceback 

32import base64 

33import zlib 

34import errno 

35import stat 

36from pathlib import Path 

37 

38import XenAPI # pylint: disable=import-error 

39import util 

40import lvutil 

41import lvmcache 

42import journaler 

43import fjournaler 

44import lock 

45import blktap2 

46import xs_errors 

47from refcounter import RefCounter 

48from ipc import IPCFlag 

49from lvmanager import LVActivator 

50from srmetadata import LVMMetadataHandler, VDI_TYPE_TAG 

51from functools import reduce 

52from time import monotonic as _time 

53 

54from constants import NS_PREFIX_LVM, VG_LOCATION, VG_PREFIX 

55from cowutil import CowImageInfo, CowUtil, getCowUtil 

56from lvmcowutil import LV_PREFIX, LvmCowUtil 

57from vditype import VdiType, VdiTypeExtension, VDI_COW_TYPES, VDI_TYPE_TO_EXTENSION 

58 

59try: 

60 from linstorcowutil import LinstorCowUtil, MultiLinstorCowUtil 

61 from linstorjournaler import LinstorJournaler 

62 from linstorvolumemanager import get_controller_uri 

63 from linstorvolumemanager import LinstorVolumeManager, LinstorVolumeManagerError, LinstorVolumeOpeners 

64 from linstorvolumemanager import PERSISTENT_PREFIX as LINSTOR_PERSISTENT_PREFIX 

65 

66 LINSTOR_AVAILABLE = True 

67except ImportError: 

68 LINSTOR_AVAILABLE = False 

69 

70# Disable automatic leaf-coalescing. Online leaf-coalesce is currently not 

71# possible due to lvhd_stop_using_() not working correctly. However, we leave 

72# this option available through the explicit LEAFCLSC_FORCE flag in the VDI 

73# record for use by the offline tool (which makes the operation safe by pausing 

74# the VM first) 

75AUTO_ONLINE_LEAF_COALESCE_ENABLED = True 

76 

77FLAG_TYPE_ABORT = "abort" # flag to request aborting of GC/coalesce 

78 

79# process "lock", used simply as an indicator that a process already exists 

80# that is doing GC/coalesce on this SR (such a process holds the lock, and we 

81# check for the fact by trying the lock). 

82lockGCRunning = None 

83 

84# process "lock" to indicate that the GC process has been activated but may not 

85# yet be running, stops a second process from being started. 

86LOCK_TYPE_GC_ACTIVE = "gc_active" 

87lockGCActive = None 

88 

89# Default coalesce error rate limit, in messages per minute. A zero value 

90# disables throttling, and a negative value disables error reporting. 

91DEFAULT_COALESCE_ERR_RATE = 1.0 / 60 

92 

93COALESCE_LAST_ERR_TAG = 'last-coalesce-error' 

94COALESCE_ERR_RATE_TAG = 'coalesce-error-rate' 

95VAR_RUN = "/var/run/" 

96SPEED_LOG_ROOT = VAR_RUN + "{uuid}.speed_log" 

97 

98N_RUNNING_AVERAGE = 10 

99 

100NON_PERSISTENT_DIR = '/run/nonpersistent/sm' 

101 

102# Signal Handler 

103SIGTERM = False 

104 

105 

106class AbortException(util.SMException): 

107 pass 

108 

109class CancelException(util.SMException): 

110 pass 

111 

112def receiveSignal(signalNumber, frame): 

113 global SIGTERM 

114 

115 util.SMlog("GC: recieved SIGTERM") 

116 SIGTERM = True 

117 return 

118 

119 

120################################################################################ 

121# 

122# Util 

123# 

124class Util: 

125 RET_RC = 1 

126 RET_STDOUT = 2 

127 RET_STDERR = 4 

128 

129 UUID_LEN = 36 

130 

131 PREFIX = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024} 

132 

133 @staticmethod 

134 def log(text) -> None: 

135 util.SMlog(text, ident="SMGC") 

136 

137 @staticmethod 

138 def logException(tag): 

139 info = sys.exc_info() 

140 if info[0] == SystemExit: 140 ↛ 142line 140 didn't jump to line 142, because the condition on line 140 was never true

141 # this should not be happening when catching "Exception", but it is 

142 sys.exit(0) 

143 tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2])) 

144 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*") 

145 Util.log(" ***********************") 

146 Util.log(" * E X C E P T I O N *") 

147 Util.log(" ***********************") 

148 Util.log("%s: EXCEPTION %s, %s" % (tag, info[0], info[1])) 

149 Util.log(tb) 

150 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*") 

151 

152 @staticmethod 

153 def doexec(args, expectedRC, inputtext=None, ret=None, log=True): 

154 "Execute a subprocess, then return its return code, stdout, stderr" 

155 proc = subprocess.Popen(args, 

156 stdin=subprocess.PIPE, \ 

157 stdout=subprocess.PIPE, \ 

158 stderr=subprocess.PIPE, \ 

159 shell=True, \ 

160 close_fds=True) 

161 (stdout, stderr) = proc.communicate(inputtext) 

162 stdout = str(stdout) 

163 stderr = str(stderr) 

164 rc = proc.returncode 

165 if log: 

166 Util.log("`%s`: %s" % (args, rc)) 

167 if type(expectedRC) != type([]): 

168 expectedRC = [expectedRC] 

169 if not rc in expectedRC: 

170 reason = stderr.strip() 

171 if stdout.strip(): 

172 reason = "%s (stdout: %s)" % (reason, stdout.strip()) 

173 Util.log("Failed: %s" % reason) 

174 raise util.CommandException(rc, args, reason) 

175 

176 if ret == Util.RET_RC: 

177 return rc 

178 if ret == Util.RET_STDERR: 

179 return stderr 

180 return stdout 

181 

182 @staticmethod 

183 def runAbortable(func, ret, ns, abortTest, pollInterval, timeOut, prefSig=signal.SIGKILL): 

184 """execute func in a separate thread and kill it if abortTest signals 

185 so""" 

186 abortSignaled = abortTest() # check now before we clear resultFlag 

187 resultFlag = IPCFlag(ns) 

188 resultFlag.clearAll() 

189 pid = os.fork() 

190 if pid: 

191 startTime = _time() 

192 try: 

193 while True: 

194 if resultFlag.test("success"): 

195 Util.log(" Child process completed successfully") 

196 resultFlag.clear("success") 

197 return 

198 if resultFlag.test("failure"): 

199 resultFlag.clear("failure") 

200 raise util.SMException("Child process exited with error") 

201 if abortTest() or abortSignaled or SIGTERM: 

202 os.killpg(pid, prefSig) 

203 raise AbortException("Aborting due to signal") 

204 if timeOut and _time() - startTime > timeOut: 

205 os.killpg(pid, prefSig) 

206 resultFlag.clearAll() 

207 raise util.SMException("Timed out") 

208 time.sleep(pollInterval) 

209 finally: 

210 wait_pid = 0 

211 rc = -1 

212 count = 0 

213 while wait_pid == 0 and count < 10: 

214 wait_pid, rc = os.waitpid(pid, os.WNOHANG) 

215 if wait_pid == 0: 

216 time.sleep(2) 

217 count += 1 

218 

219 if wait_pid == 0: 

220 Util.log("runAbortable: wait for process completion timed out") 

221 else: 

222 os.setpgrp() 

223 try: 

224 if func() == ret: 

225 resultFlag.set("success") 

226 else: 

227 resultFlag.set("failure") 

228 except Exception as e: 

229 Util.log("Child process failed with : (%s)" % e) 

230 resultFlag.set("failure") 

231 Util.logException("This exception has occured") 

232 os._exit(0) 

233 

234 @staticmethod 

235 def num2str(number): 

236 for prefix in ("G", "M", "K"): 236 ↛ 239line 236 didn't jump to line 239, because the loop on line 236 didn't complete

237 if number >= Util.PREFIX[prefix]: 

238 return "%.3f%s" % (float(number) / Util.PREFIX[prefix], prefix) 

239 return "%s" % number 

240 

241 @staticmethod 

242 def numBits(val): 

243 count = 0 

244 while val: 

245 count += val & 1 

246 val = val >> 1 

247 return count 

248 

249 @staticmethod 

250 def countBits(bitmap1, bitmap2): 

251 """return bit count in the bitmap produced by ORing the two bitmaps""" 

252 len1 = len(bitmap1) 

253 len2 = len(bitmap2) 

254 lenLong = len1 

255 lenShort = len2 

256 bitmapLong = bitmap1 

257 if len2 > len1: 

258 lenLong = len2 

259 lenShort = len1 

260 bitmapLong = bitmap2 

261 

262 count = 0 

263 for i in range(lenShort): 

264 val = bitmap1[i] | bitmap2[i] 

265 count += Util.numBits(val) 

266 

267 for i in range(i + 1, lenLong): 

268 val = bitmapLong[i] 

269 count += Util.numBits(val) 

270 return count 

271 

272 @staticmethod 

273 def getThisScript(): 

274 thisScript = util.get_real_path(__file__) 

275 if thisScript.endswith(".pyc"): 

276 thisScript = thisScript[:-1] 

277 return thisScript 

278 

279 

280################################################################################ 

281# 

282# XAPI 

283# 

284class XAPI: 

285 USER = "root" 

286 PLUGIN_ON_SLAVE = "on-slave" 

287 

288 CONFIG_SM = 0 

289 CONFIG_OTHER = 1 

290 CONFIG_ON_BOOT = 2 

291 CONFIG_ALLOW_CACHING = 3 

292 

293 CONFIG_NAME = { 

294 CONFIG_SM: "sm-config", 

295 CONFIG_OTHER: "other-config", 

296 CONFIG_ON_BOOT: "on-boot", 

297 CONFIG_ALLOW_CACHING: "allow_caching" 

298 } 

299 

300 class LookupError(util.SMException): 

301 pass 

302 

303 def __init__(self, session, srUuid): 

304 self.session = session 

305 self._api_session = None 

306 if self.session is None: 

307 self._api_session = util.ApiSession("SMGC") 

308 self.session = self._api_session.session 

309 self._srRef = self.session.xenapi.SR.get_by_uuid(srUuid) 

310 self.srRecord = self.session.xenapi.SR.get_record(self._srRef) 

311 self.hostUuid = util.get_this_host() 

312 self._hostRef = self.session.xenapi.host.get_by_uuid(self.hostUuid) 

313 self.task = None 

314 self.task_progress = {"coalescable": 0, "done": 0} 

315 

316 def __del__(self): 

317 if self._api_session: 

318 self._api_session.logout() 

319 

320 @property 

321 def srRef(self): 

322 return self._srRef 

323 

324 def isPluggedHere(self): 

325 pbds = self.getAttachedPBDs() 

326 for pbdRec in pbds: 

327 if pbdRec["host"] == self._hostRef: 

328 return True 

329 return False 

330 

331 def poolOK(self): 

332 host_recs = self.session.xenapi.host.get_all_records() 

333 for host_ref, host_rec in host_recs.items(): 

334 if not host_rec["enabled"]: 

335 Util.log("Host %s not enabled" % host_rec["uuid"]) 

336 return False 

337 return True 

338 

339 def isMaster(self): 

340 if self.srRecord["shared"]: 

341 pool = list(self.session.xenapi.pool.get_all_records().values())[0] 

342 return pool["master"] == self._hostRef 

343 else: 

344 pbds = self.getAttachedPBDs() 

345 if len(pbds) < 1: 

346 raise util.SMException("Local SR not attached") 

347 elif len(pbds) > 1: 

348 raise util.SMException("Local SR multiply attached") 

349 return pbds[0]["host"] == self._hostRef 

350 

351 def getAttachedPBDs(self): 

352 """Return PBD records for all PBDs of this SR that are currently 

353 attached""" 

354 attachedPBDs = [] 

355 pbds = self.session.xenapi.PBD.get_all_records() 

356 for pbdRec in pbds.values(): 

357 if pbdRec["SR"] == self._srRef and pbdRec["currently_attached"]: 

358 attachedPBDs.append(pbdRec) 

359 return attachedPBDs 

360 

361 def getOnlineHosts(self): 

362 return util.get_online_hosts(self.session) 

363 

364 def ensureInactive(self, hostRef, args): 

365 text = self.session.xenapi.host.call_plugin( \ 

366 hostRef, self.PLUGIN_ON_SLAVE, "multi", args) 

367 Util.log("call-plugin returned: '%s'" % text) 

368 

369 def getRecordHost(self, hostRef): 

370 return self.session.xenapi.host.get_record(hostRef) 

371 

372 def _getRefVDI(self, uuid): 

373 return self.session.xenapi.VDI.get_by_uuid(uuid) 

374 

375 def getRefVDI(self, vdi): 

376 return self._getRefVDI(vdi.uuid) 

377 

378 def getRecordVDI(self, uuid): 

379 try: 

380 ref = self._getRefVDI(uuid) 

381 return self.session.xenapi.VDI.get_record(ref) 

382 except XenAPI.Failure: 

383 return None 

384 

385 def singleSnapshotVDI(self, vdi): 

386 return self.session.xenapi.VDI.snapshot(vdi.getRef(), 

387 {"type": "internal"}) 

388 

389 def forgetVDI(self, srUuid, vdiUuid): 

390 """Forget the VDI, but handle the case where the VDI has already been 

391 forgotten (i.e. ignore errors)""" 

392 try: 

393 vdiRef = self.session.xenapi.VDI.get_by_uuid(vdiUuid) 

394 self.session.xenapi.VDI.forget(vdiRef) 

395 except XenAPI.Failure: 

396 pass 

397 

398 def getConfigVDI(self, vdi, key): 

399 kind = vdi.CONFIG_TYPE[key] 

400 if kind == self.CONFIG_SM: 

401 cfg = self.session.xenapi.VDI.get_sm_config(vdi.getRef()) 

402 elif kind == self.CONFIG_OTHER: 

403 cfg = self.session.xenapi.VDI.get_other_config(vdi.getRef()) 

404 elif kind == self.CONFIG_ON_BOOT: 

405 cfg = self.session.xenapi.VDI.get_on_boot(vdi.getRef()) 

406 elif kind == self.CONFIG_ALLOW_CACHING: 

407 cfg = self.session.xenapi.VDI.get_allow_caching(vdi.getRef()) 

408 else: 

409 assert(False) 

410 Util.log("Got %s for %s: %s" % (self.CONFIG_NAME[kind], vdi, repr(cfg))) 

411 return cfg 

412 

413 def removeFromConfigVDI(self, vdi, key): 

414 kind = vdi.CONFIG_TYPE[key] 

415 if kind == self.CONFIG_SM: 

416 self.session.xenapi.VDI.remove_from_sm_config(vdi.getRef(), key) 

417 elif kind == self.CONFIG_OTHER: 

418 self.session.xenapi.VDI.remove_from_other_config(vdi.getRef(), key) 

419 else: 

420 assert(False) 

421 

422 def addToConfigVDI(self, vdi, key, val): 

423 kind = vdi.CONFIG_TYPE[key] 

424 if kind == self.CONFIG_SM: 

425 self.session.xenapi.VDI.add_to_sm_config(vdi.getRef(), key, val) 

426 elif kind == self.CONFIG_OTHER: 

427 self.session.xenapi.VDI.add_to_other_config(vdi.getRef(), key, val) 

428 else: 

429 assert(False) 

430 

431 def isSnapshot(self, vdi): 

432 return self.session.xenapi.VDI.get_is_a_snapshot(vdi.getRef()) 

433 

434 def markCacheSRsDirty(self): 

435 sr_refs = self.session.xenapi.SR.get_all_records_where( \ 

436 'field "local_cache_enabled" = "true"') 

437 for sr_ref in sr_refs: 

438 Util.log("Marking SR %s dirty" % sr_ref) 

439 util.set_dirty(self.session, sr_ref) 

440 

441 def srUpdate(self): 

442 Util.log("Starting asynch srUpdate for SR %s" % self.srRecord["uuid"]) 

443 abortFlag = IPCFlag(self.srRecord["uuid"]) 

444 task = self.session.xenapi.Async.SR.update(self._srRef) 

445 cancelTask = True 

446 try: 

447 for i in range(60): 

448 status = self.session.xenapi.task.get_status(task) 

449 if not status == "pending": 

450 Util.log("SR.update_asynch status changed to [%s]" % status) 

451 cancelTask = False 

452 return 

453 if abortFlag.test(FLAG_TYPE_ABORT): 

454 Util.log("Abort signalled during srUpdate, cancelling task...") 

455 try: 

456 self.session.xenapi.task.cancel(task) 

457 cancelTask = False 

458 Util.log("Task cancelled") 

459 except: 

460 pass 

461 return 

462 time.sleep(1) 

463 finally: 

464 if cancelTask: 

465 self.session.xenapi.task.cancel(task) 

466 self.session.xenapi.task.destroy(task) 

467 Util.log("Asynch srUpdate still running, but timeout exceeded.") 

468 

469 def update_task(self): 

470 self.session.xenapi.task.set_other_config( 

471 self.task, 

472 { 

473 "applies_to": self._srRef 

474 }) 

475 total = self.task_progress['coalescable'] + self.task_progress['done'] 

476 if (total > 0): 

477 self.session.xenapi.task.set_progress( 

478 self.task, float(self.task_progress['done']) / total) 

479 

480 def create_task(self, label, description): 

481 self.task = self.session.xenapi.task.create(label, description) 

482 self.update_task() 

483 

484 def update_task_progress(self, key, value): 

485 self.task_progress[key] = value 

486 if self.task: 

487 self.update_task() 

488 

489 def set_task_status(self, status): 

490 if self.task: 

491 self.session.xenapi.task.set_status(self.task, status) 

492 

493 

494################################################################################ 

495# 

496# VDI 

497# 

498class VDI(object): 

499 """Object representing a VDI of a COW-based SR""" 

500 

501 POLL_INTERVAL = 1 

502 POLL_TIMEOUT = 30 

503 DEVICE_MAJOR = 202 

504 

505 # config keys & values 

506 DB_VDI_PARENT = "vhd-parent" 

507 DB_VDI_TYPE = "vdi_type" 

508 DB_VDI_BLOCKS = "vhd-blocks" 

509 DB_VDI_PAUSED = "paused" 

510 DB_VDI_RELINKING = "relinking" 

511 DB_VDI_ACTIVATING = "activating" 

512 DB_GC = "gc" 

513 DB_COALESCE = "coalesce" 

514 DB_LEAFCLSC = "leaf-coalesce" # config key 

515 DB_GC_NO_SPACE = "gc_no_space" 

516 LEAFCLSC_DISABLED = "false" # set by user; means do not leaf-coalesce 

517 LEAFCLSC_FORCE = "force" # set by user; means skip snap-coalesce 

518 LEAFCLSC_OFFLINE = "offline" # set here for informational purposes: means 

519 # no space to snap-coalesce or unable to keep 

520 # up with VDI. This is not used by the SM, it 

521 # might be used by external components. 

522 DB_ONBOOT = "on-boot" 

523 ONBOOT_RESET = "reset" 

524 DB_ALLOW_CACHING = "allow_caching" 

525 

526 CONFIG_TYPE = { 

527 DB_VDI_PARENT: XAPI.CONFIG_SM, 

528 DB_VDI_TYPE: XAPI.CONFIG_SM, 

529 DB_VDI_BLOCKS: XAPI.CONFIG_SM, 

530 DB_VDI_PAUSED: XAPI.CONFIG_SM, 

531 DB_VDI_RELINKING: XAPI.CONFIG_SM, 

532 DB_VDI_ACTIVATING: XAPI.CONFIG_SM, 

533 DB_GC: XAPI.CONFIG_OTHER, 

534 DB_COALESCE: XAPI.CONFIG_OTHER, 

535 DB_LEAFCLSC: XAPI.CONFIG_OTHER, 

536 DB_ONBOOT: XAPI.CONFIG_ON_BOOT, 

537 DB_ALLOW_CACHING: XAPI.CONFIG_ALLOW_CACHING, 

538 DB_GC_NO_SPACE: XAPI.CONFIG_SM 

539 } 

540 

541 LIVE_LEAF_COALESCE_MAX_SIZE = 20 * 1024 * 1024 # bytes 

542 LIVE_LEAF_COALESCE_TIMEOUT = 10 # seconds 

543 TIMEOUT_SAFETY_MARGIN = 0.5 # extra margin when calculating 

544 # feasibility of leaf coalesce 

545 

546 JRN_RELINK = "relink" # journal entry type for relinking children 

547 JRN_COALESCE = "coalesce" # to communicate which VDI is being coalesced 

548 JRN_LEAF = "leaf" # used in coalesce-leaf 

549 

550 STR_TREE_INDENT = 4 

551 

552 def __init__(self, sr, uuid, vdi_type): 

553 self.sr = sr 

554 self.scanError = True 

555 self.uuid = uuid 

556 self.vdi_type = vdi_type 

557 self.fileName = "" 

558 self.parentUuid = "" 

559 self.sizeVirt = -1 

560 self._sizePhys = -1 

561 self._sizeAllocated = -1 

562 self._hidden = False 

563 self.parent = None 

564 self.children = [] 

565 self._vdiRef = None 

566 self.cowutil = getCowUtil(vdi_type) 

567 self._clearRef() 

568 

569 @staticmethod 

570 def extractUuid(path): 

571 raise NotImplementedError("Implement in sub class") 

572 

573 def load(self, info=None) -> None: 

574 """Load VDI info""" 

575 pass 

576 

577 def getDriverName(self) -> str: 

578 return self.vdi_type 

579 

580 def getRef(self): 

581 if self._vdiRef is None: 

582 self._vdiRef = self.sr.xapi.getRefVDI(self) 

583 return self._vdiRef 

584 

585 def getConfig(self, key, default=None): 

586 config = self.sr.xapi.getConfigVDI(self, key) 

587 if key == self.DB_ONBOOT or key == self.DB_ALLOW_CACHING: 587 ↛ 588line 587 didn't jump to line 588, because the condition on line 587 was never true

588 val = config 

589 else: 

590 val = config.get(key) 

591 if val: 

592 return val 

593 return default 

594 

595 def setConfig(self, key, val): 

596 self.sr.xapi.removeFromConfigVDI(self, key) 

597 self.sr.xapi.addToConfigVDI(self, key, val) 

598 Util.log("Set %s = %s for %s" % (key, val, self)) 

599 

600 def delConfig(self, key): 

601 self.sr.xapi.removeFromConfigVDI(self, key) 

602 Util.log("Removed %s from %s" % (key, self)) 

603 

604 def ensurePaused(self): 

605 if not self.getConfig(self.DB_VDI_PAUSED) == "true": 

606 self.pause(failfast=True) 

607 

608 def ensureUnpaused(self): 

609 if self.getConfig(self.DB_VDI_PAUSED) == "true": 

610 Util.log("Unpausing VDI %s" % self) 

611 self.unpause() 

612 

613 def pause(self, failfast=False) -> None: 

614 if not blktap2.VDI.tap_pause(self.sr.xapi.session, self.sr.uuid, 

615 self.uuid, failfast): 

616 raise util.SMException("Failed to pause VDI %s" % self) 

617 

618 def _report_tapdisk_unpause_error(self): 

619 try: 

620 xapi = self.sr.xapi.session.xenapi 

621 sr_ref = xapi.SR.get_by_uuid(self.sr.uuid) 

622 msg_name = "failed to unpause tapdisk" 

623 msg_body = "Failed to unpause tapdisk for VDI %s, " \ 

624 "VMs using this tapdisk have lost access " \ 

625 "to the corresponding disk(s)" % self.uuid 

626 xapi.message.create(msg_name, "4", "SR", self.sr.uuid, msg_body) 

627 except Exception as e: 

628 util.SMlog("failed to generate message: %s" % e) 

629 

630 def unpause(self): 

631 if not blktap2.VDI.tap_unpause(self.sr.xapi.session, self.sr.uuid, 

632 self.uuid): 

633 self._report_tapdisk_unpause_error() 

634 raise util.SMException("Failed to unpause VDI %s" % self) 

635 

636 def refresh(self, ignoreNonexistent=True): 

637 """Pause-unpause in one step""" 

638 self.sr.lock() 

639 try: 

640 try: 

641 if not blktap2.VDI.tap_refresh(self.sr.xapi.session, 641 ↛ 643line 641 didn't jump to line 643, because the condition on line 641 was never true

642 self.sr.uuid, self.uuid): 

643 self._report_tapdisk_unpause_error() 

644 raise util.SMException("Failed to refresh %s" % self) 

645 except XenAPI.Failure as e: 

646 if util.isInvalidVDI(e) and ignoreNonexistent: 

647 Util.log("VDI %s not found, ignoring" % self) 

648 return 

649 raise 

650 finally: 

651 self.sr.unlock() 

652 

653 def isSnapshot(self): 

654 return self.sr.xapi.isSnapshot(self) 

655 

656 def isAttachedRW(self): 

657 return util.is_attached_rw( 

658 self.sr.xapi.session.xenapi.VDI.get_sm_config(self.getRef())) 

659 

660 def getVDIBlocks(self): 

661 val = self.updateBlockInfo() 

662 bitmap = zlib.decompress(base64.b64decode(val)) 

663 return bitmap 

664 

665 def isCoalesceable(self): 

666 """A VDI is coalesceable if it has no siblings and is not a leaf""" 

667 return ( 

668 not self.scanError and 

669 self.parent and 

670 len(self.parent.children) == 1 and 

671 self.isHidden() and 

672 len(self.children) > 0 and ( 

673 # Conditions below are Qcow2 specific: 

674 # A Qcow2 chain can't be coalesce with more than one leaf attached. 

675 # Put it another way, Qcow2 leaves activated on multiple hosts must 

676 # prevent the coalesce of the chain. 

677 self.vdi_type != VdiType.QCOW2 or 

678 (self.vdi_type == VdiType.QCOW2 and len(self.sr.hasLeavesAttachedOn(self)) <= 1) 

679 ) 

680 ) 

681 

682 def isLeafCoalesceable(self): 

683 """A VDI is leaf-coalesceable if it has no siblings and is a leaf""" 

684 return not self.scanError and \ 

685 self.parent and \ 

686 len(self.parent.children) == 1 and \ 

687 not self.isHidden() and \ 

688 len(self.children) == 0 

689 

690 def canLiveCoalesce(self, speed): 

691 """Can we stop-and-leaf-coalesce this VDI? The VDI must be 

692 isLeafCoalesceable() already""" 

693 feasibleSize = False 

694 allowedDownTime = \ 

695 self.TIMEOUT_SAFETY_MARGIN * self.LIVE_LEAF_COALESCE_TIMEOUT 

696 allocated_size = self.getAllocatedSize() 

697 if speed: 

698 feasibleSize = \ 

699 allocated_size // speed < allowedDownTime 

700 else: 

701 feasibleSize = \ 

702 allocated_size < self.LIVE_LEAF_COALESCE_MAX_SIZE 

703 

704 return (feasibleSize or 

705 self.getConfig(self.DB_LEAFCLSC) == self.LEAFCLSC_FORCE) 

706 

707 def getAllPrunable(self): 

708 if len(self.children) == 0: # base case 

709 # it is possible to have a hidden leaf that was recently coalesced 

710 # onto its parent, its children already relinked but not yet 

711 # reloaded - in which case it may not be garbage collected yet: 

712 # some tapdisks could still be using the file. 

713 if self.sr.journaler.get(self.JRN_RELINK, self.uuid): 

714 return [] 

715 if not self.scanError and self.isHidden(): 

716 return [self] 

717 return [] 

718 

719 thisPrunable = True 

720 vdiList = [] 

721 for child in self.children: 

722 childList = child.getAllPrunable() 

723 vdiList.extend(childList) 

724 if child not in childList: 

725 thisPrunable = False 

726 

727 # We can destroy the current VDI if all childs are hidden BUT the 

728 # current VDI must be hidden too to do that! 

729 # Example in this case (after a failed live leaf coalesce): 

730 # 

731 # SMGC: [32436] SR 07ed ('linstor-nvme-sr') (2 VDIs in 1 VHD trees): 

732 # SMGC: [32436] b5458d61(1.000G/4.127M) 

733 # SMGC: [32436] *OLD_b545(1.000G/4.129M) 

734 # 

735 # OLD_b545 is hidden and must be removed, but b5458d61 not. 

736 # Normally we are not in this function when the delete action is 

737 # executed but in `_liveLeafCoalesce`. 

738 

739 if not self.scanError and not self.isHidden() and thisPrunable: 

740 vdiList.append(self) 

741 return vdiList 

742 

743 def getSizePhys(self) -> int: 

744 return self._sizePhys 

745 

746 def getAllocatedSize(self) -> int: 

747 return self._sizeAllocated 

748 

749 def getTreeRoot(self): 

750 "Get the root of the tree that self belongs to" 

751 root = self 

752 while root.parent: 

753 root = root.parent 

754 return root 

755 

756 def getTreeHeight(self): 

757 "Get the height of the subtree rooted at self" 

758 if len(self.children) == 0: 

759 return 1 

760 

761 maxChildHeight = 0 

762 for child in self.children: 

763 childHeight = child.getTreeHeight() 

764 if childHeight > maxChildHeight: 

765 maxChildHeight = childHeight 

766 

767 return maxChildHeight + 1 

768 

769 def getAllLeaves(self) -> List["VDI"]: 

770 "Get all leaf nodes in the subtree rooted at self" 

771 if len(self.children) == 0: 

772 return [self] 

773 

774 leaves = [] 

775 for child in self.children: 

776 leaves.extend(child.getAllLeaves()) 

777 return leaves 

778 

779 def updateBlockInfo(self) -> Optional[str]: 

780 val = base64.b64encode(self._queryCowBlocks()).decode() 

781 try: 

782 self.setConfig(VDI.DB_VDI_BLOCKS, val) 

783 except Exception: 

784 if self.vdi_type != VdiType.QCOW2: 

785 raise 

786 # Sometime with QCOW2, our allocation table is too big to be stored in XAPI, in this case we do not store it 

787 # and we write `skipped` instead so that hasWork is happy (and the GC doesn't run in loop indefinitely). 

788 self.setConfig(VDI.DB_VDI_BLOCKS, "skipped") 

789 

790 return val 

791 

792 def rename(self, uuid) -> None: 

793 "Rename the VDI file" 

794 assert(not self.sr.vdis.get(uuid)) 

795 self._clearRef() 

796 oldUuid = self.uuid 

797 self.uuid = uuid 

798 self.children = [] 

799 # updating the children themselves is the responsibility of the caller 

800 del self.sr.vdis[oldUuid] 

801 self.sr.vdis[self.uuid] = self 

802 

803 def delete(self) -> None: 

804 "Physically delete the VDI" 

805 lock.Lock.cleanup(self.uuid, NS_PREFIX_LVM + self.sr.uuid) 

806 lock.Lock.cleanupAll(self.uuid) 

807 self._clear() 

808 

809 def getParent(self) -> str: 

810 return self.cowutil.getParent(self.path, lambda x: x.strip()) 810 ↛ exitline 810 didn't run the lambda on line 810

811 

812 def repair(self, parent) -> None: 

813 self.cowutil.repair(parent) 

814 

815 @override 

816 def __str__(self) -> str: 

817 strHidden = "" 

818 if self.isHidden(): 818 ↛ 819line 818 didn't jump to line 819, because the condition on line 818 was never true

819 strHidden = "*" 

820 strSizeVirt = "?" 

821 if self.sizeVirt > 0: 821 ↛ 822line 821 didn't jump to line 822, because the condition on line 821 was never true

822 strSizeVirt = Util.num2str(self.sizeVirt) 

823 strSizePhys = "?" 

824 if self._sizePhys > 0: 824 ↛ 825line 824 didn't jump to line 825, because the condition on line 824 was never true

825 strSizePhys = "/%s" % Util.num2str(self._sizePhys) 

826 strSizeAllocated = "?" 

827 if self._sizeAllocated >= 0: 

828 strSizeAllocated = "/%s" % Util.num2str(self._sizeAllocated) 

829 strType = "[{}]".format(self.vdi_type) 

830 

831 return "%s%s(%s%s%s)%s" % (strHidden, self.uuid[0:8], strSizeVirt, 

832 strSizePhys, strSizeAllocated, strType) 

833 

834 def validate(self, fast=False) -> None: 

835 if self.cowutil.check(self.path, fast=fast) != CowUtil.CheckResult.Success: 835 ↛ 836line 835 didn't jump to line 836, because the condition on line 835 was never true

836 raise util.SMException("COW image %s corrupted" % self) 

837 

838 def _clear(self): 

839 self.uuid = "" 

840 self.path = "" 

841 self.parentUuid = "" 

842 self.parent = None 

843 self._clearRef() 

844 

845 def _clearRef(self): 

846 self._vdiRef = None 

847 

848 @staticmethod 

849 def _cancel_exception(sig, frame): 

850 raise CancelException() 

851 

852 def _call_plugin_coalesce(self, hostRef, leaf): 

853 signal.signal(signal.SIGTERM, self._cancel_exception) 

854 args = {"path": self.path, "vdi_type": self.vdi_type, "leaf_path": leaf.path} 

855 Util.log("Calling remote coalesce plugin with: {}".format(args)) 

856 try: 

857 ret = self.sr.xapi.session.xenapi.host.call_plugin( \ 

858 hostRef, XAPI.PLUGIN_ON_SLAVE, "commit_tapdisk", args) 

859 Util.log("Remote coalesce returned {}".format(ret)) 

860 except CancelException: 

861 Util.log(f"Cancelling online coalesce following signal {args}") 

862 self.sr.xapi.session.xenapi.host.call_plugin( \ 

863 hostRef, XAPI.PLUGIN_ON_SLAVE, "commit_cancel", args) 

864 raise 

865 except Exception: 

866 raise 

867 

868 def _doCoalesceOnHost(self, hostRef, leaf): 

869 self.parent._increaseSizeVirt(self.sizeVirt) 

870 self.sr._updateSlavesOnResize(self.parent) 

871 

872 self._coalesceCowImageOnHost(hostRef, leaf) 

873 

874 #self._verifyContents(0) 

875 self.parent.updateBlockInfo() 

876 

877 def _isOpenOnHosts(self) -> Optional[str]: 

878 for pbdRecord in self.sr.xapi.getAttachedPBDs(): 

879 hostRef = pbdRecord["host"] 

880 args = {"path": self.path} 

881 is_openers = util.strtobool(self.sr.xapi.session.xenapi.host.call_plugin( \ 

882 hostRef, XAPI.PLUGIN_ON_SLAVE, "is_openers", args)) 

883 if is_openers: 

884 return hostRef 

885 return None 

886 

887 def _doCoalesce(self) -> None: 

888 """Coalesce self onto parent. Only perform the actual coalescing of 

889 an image, but not the subsequent relinking. We'll do that as the next step, 

890 after reloading the entire SR in case things have changed while we 

891 were coalescing""" 

892 self.validate() 

893 self.parent.validate(True) 

894 self.parent._increaseSizeVirt(self.sizeVirt) 

895 self.sr._updateSlavesOnResize(self.parent) 

896 self._coalesceCowImage(0) 

897 self.parent.validate(True) 

898 #self._verifyContents(0) 

899 self.parent.updateBlockInfo() 

900 

901 def _verifyContents(self, timeOut): 

902 Util.log(" Coalesce verification on %s" % self) 

903 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 

904 Util.runAbortable(lambda: self._runTapdiskDiff(), True, 

905 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut) 

906 Util.log(" Coalesce verification succeeded") 

907 

908 def _runTapdiskDiff(self): 

909 cmd = "tapdisk-diff -n %s:%s -m %s:%s" % \ 

910 (self.getDriverName(), self.path, \ 

911 self.parent.getDriverName(), self.parent.path) 

912 Util.doexec(cmd, 0) 

913 return True 

914 

915 @staticmethod 

916 def _reportCoalesceError(vdi, ce): 

917 """Reports a coalesce error to XenCenter. 

918 

919 vdi: the VDI object on which the coalesce error occured 

920 ce: the CommandException that was raised""" 

921 

922 msg_name = os.strerror(ce.code) 

923 if ce.code == errno.ENOSPC: 

924 # TODO We could add more information here, e.g. exactly how much 

925 # space is required for the particular coalesce, as well as actions 

926 # to be taken by the user and consequences of not taking these 

927 # actions. 

928 msg_body = 'Run out of space while coalescing.' 

929 elif ce.code == errno.EIO: 

930 msg_body = 'I/O error while coalescing.' 

931 else: 

932 msg_body = '' 

933 util.SMlog('Coalesce failed on SR %s: %s (%s)' 

934 % (vdi.sr.uuid, msg_name, msg_body)) 

935 

936 # Create a XenCenter message, but don't spam. 

937 xapi = vdi.sr.xapi.session.xenapi 

938 sr_ref = xapi.SR.get_by_uuid(vdi.sr.uuid) 

939 oth_cfg = xapi.SR.get_other_config(sr_ref) 

940 if COALESCE_ERR_RATE_TAG in oth_cfg: 

941 coalesce_err_rate = float(oth_cfg[COALESCE_ERR_RATE_TAG]) 

942 else: 

943 coalesce_err_rate = DEFAULT_COALESCE_ERR_RATE 

944 

945 xcmsg = False 

946 if coalesce_err_rate == 0: 

947 xcmsg = True 

948 elif coalesce_err_rate > 0: 

949 now = datetime.datetime.now() 

950 sm_cfg = xapi.SR.get_sm_config(sr_ref) 

951 if COALESCE_LAST_ERR_TAG in sm_cfg: 

952 # seconds per message (minimum distance in time between two 

953 # messages in seconds) 

954 spm = datetime.timedelta(seconds=(1.0 / coalesce_err_rate) * 60) 

955 last = datetime.datetime.fromtimestamp( 

956 float(sm_cfg[COALESCE_LAST_ERR_TAG])) 

957 if now - last >= spm: 

958 xapi.SR.remove_from_sm_config(sr_ref, 

959 COALESCE_LAST_ERR_TAG) 

960 xcmsg = True 

961 else: 

962 xcmsg = True 

963 if xcmsg: 

964 xapi.SR.add_to_sm_config(sr_ref, COALESCE_LAST_ERR_TAG, 

965 str(now.strftime('%s'))) 

966 if xcmsg: 

967 xapi.message.create(msg_name, "3", "SR", vdi.sr.uuid, msg_body) 

968 

969 def coalesce(self) -> int: 

970 return self.cowutil.coalesce(self.path) 

971 

972 @staticmethod 

973 def _doCoalesceCowImage(vdi: "VDI"): 

974 try: 

975 startTime = time.time() 

976 allocated_size = vdi.getAllocatedSize() 

977 coalesced_size = vdi.coalesce() 

978 endTime = time.time() 

979 vdi.sr.recordStorageSpeed(startTime, endTime, coalesced_size) 

980 except util.CommandException as ce: 

981 # We use try/except for the following piece of code because it runs 

982 # in a separate process context and errors will not be caught and 

983 # reported by anyone. 

984 try: 

985 # Report coalesce errors back to user via XC 

986 VDI._reportCoalesceError(vdi, ce) 

987 except Exception as e: 

988 util.SMlog('failed to create XenCenter message: %s' % e) 

989 raise ce 

990 except: 

991 raise 

992 

993 def _vdi_is_raw(self, vdi_path): 

994 """ 

995 Given path to vdi determine if it is raw 

996 """ 

997 uuid = self.extractUuid(vdi_path) 

998 return self.sr.vdis[uuid].vdi_type == VdiType.RAW 

999 

1000 def _coalesceCowImage(self, timeOut): 

1001 Util.log(" Running COW coalesce on %s" % self) 

1002 def abortTest(): 

1003 if self.cowutil.isCoalesceableOnRemote(): 

1004 file = self.sr._gc_running_file(self) 

1005 try: 

1006 with open(file, "r") as f: 

1007 if not f.read(): 

1008 return True 

1009 except OSError as e: 

1010 if e.errno == errno.ENOENT: 

1011 util.SMlog("File {} does not exist".format(file)) 

1012 else: 

1013 util.SMlog("IOError: {}".format(e)) 

1014 return True 

1015 return IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 

1016 

1017 try: 

1018 util.fistpoint.activate_custom_fn( 

1019 "cleanup_coalesceVHD_inject_failure", 

1020 util.inject_failure) 

1021 Util.runAbortable(lambda: VDI._doCoalesceCowImage(self), None, 

1022 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut) 

1023 except: 

1024 # Exception at this phase could indicate a failure in COW coalesce 

1025 # or a kill of COW coalesce by runAbortable due to timeOut 

1026 # Try a repair and reraise the exception 

1027 parent = "" 

1028 try: 

1029 parent = self.getParent() 

1030 if not self._vdi_is_raw(parent): 

1031 # Repair error is logged and ignored. Error reraised later 

1032 util.SMlog('Coalesce failed on %s, attempting repair on ' \ 

1033 'parent %s' % (self.uuid, parent)) 

1034 self.repair(parent) 

1035 except Exception as e: 

1036 util.SMlog('(error ignored) Failed to repair parent %s ' \ 

1037 'after failed coalesce on %s, err: %s' % 

1038 (parent, self.path, e)) 

1039 raise 

1040 

1041 util.fistpoint.activate("LVHDRT_coalescing_VHD_data", self.sr.uuid) 

1042 

1043 def _coalesceCowImageOnHost(self, hostRef, leaf): 

1044 Util.log(" Running COW coalesce on {} via remote host {}".format(self, hostRef)) 

1045 def abortTest(): 

1046 file = self.sr._gc_running_file(self) 

1047 try: 

1048 with open(file, "r") as f: 

1049 if not f.read(): 

1050 Util.log("abortTest: Cancelling coalesce") 

1051 return True 

1052 except OSError as e: 

1053 if e.errno == errno.ENOENT: 

1054 Util.log("File {} does not exist".format(file)) 

1055 else: 

1056 Util.log("IOError: {}".format(e)) 

1057 return True 

1058 return False 

1059 

1060 Util.runAbortable(lambda: self._call_plugin_coalesce(hostRef, leaf), 

1061 None, self.sr.uuid, abortTest, VDI.POLL_INTERVAL, 0, prefSig=signal.SIGTERM) 

1062 

1063 def _relinkSkip(self) -> None: 

1064 """Relink children of this VDI to point to the parent of this VDI""" 

1065 abortFlag = IPCFlag(self.sr.uuid) 

1066 for child in self.children: 

1067 if abortFlag.test(FLAG_TYPE_ABORT): 1067 ↛ 1068line 1067 didn't jump to line 1068, because the condition on line 1067 was never true

1068 raise AbortException("Aborting due to signal") 

1069 Util.log(" Relinking %s from %s to %s" % \ 

1070 (child, self, self.parent)) 

1071 util.fistpoint.activate("LVHDRT_relinking_grandchildren", self.sr.uuid) 

1072 child._setParent(self.parent) 

1073 self.children = [] 

1074 

1075 def _reloadChildren(self, vdiSkip): 

1076 """Pause & unpause all VDIs in the subtree to cause blktap to reload 

1077 the COW image metadata for this file in any online VDI""" 

1078 abortFlag = IPCFlag(self.sr.uuid) 

1079 for child in self.children: 

1080 if child == vdiSkip: 

1081 continue 

1082 if abortFlag.test(FLAG_TYPE_ABORT): 1082 ↛ 1083line 1082 didn't jump to line 1083, because the condition on line 1082 was never true

1083 raise AbortException("Aborting due to signal") 

1084 Util.log(" Reloading VDI %s" % child) 

1085 child._reload() 

1086 

1087 def _reload(self): 

1088 """Pause & unpause to cause blktap to reload the image metadata""" 

1089 for child in self.children: 1089 ↛ 1090line 1089 didn't jump to line 1090, because the loop on line 1089 never started

1090 child._reload() 

1091 

1092 # only leaves can be attached 

1093 if len(self.children) == 0: 1093 ↛ exitline 1093 didn't return from function '_reload', because the condition on line 1093 was never false

1094 try: 

1095 self.delConfig(VDI.DB_VDI_RELINKING) 

1096 except XenAPI.Failure as e: 

1097 if not util.isInvalidVDI(e): 

1098 raise 

1099 self.refresh() 

1100 

1101 def _needRelink(self, list_not_to_relink): 

1102 """ 

1103 If we coalesce up the chain, we shouldn't need to do the relink at all, we only need to do the relink on the children if their direct parent was the one we were coalescing 

1104 """ 

1105 if not list_not_to_relink: 1105 ↛ 1107line 1105 didn't jump to line 1107, because the condition on line 1105 was never false

1106 return True 

1107 if self.uuid in list_not_to_relink: 

1108 return False 

1109 else: 

1110 return True 

1111 

1112 def _tagChildrenForRelink(self, list_not_to_relink=None): 

1113 if len(self.children) == 0: 

1114 retries = 0 

1115 try: 

1116 while retries < 15: 

1117 retries += 1 

1118 if self.getConfig(VDI.DB_VDI_ACTIVATING) is not None: 

1119 Util.log("VDI %s is activating, wait to relink" % 

1120 self.uuid) 

1121 else: 

1122 if self._needRelink(list_not_to_relink): 1122 ↛ 1132line 1122 didn't jump to line 1132, because the condition on line 1122 was never false

1123 self.setConfig(VDI.DB_VDI_RELINKING, "True") 

1124 

1125 if self.getConfig(VDI.DB_VDI_ACTIVATING): 

1126 self.delConfig(VDI.DB_VDI_RELINKING) 

1127 Util.log("VDI %s started activating while tagging" % 

1128 self.uuid) 

1129 else: 

1130 return 

1131 else: 

1132 Util.log(f"Not adding relinking tag to VDI {self.uuid}") 

1133 return 

1134 time.sleep(2) 

1135 

1136 raise util.SMException("Failed to tag vdi %s for relink" % self) 

1137 except XenAPI.Failure as e: 

1138 if not util.isInvalidVDI(e): 

1139 raise 

1140 

1141 for child in self.children: 

1142 child._tagChildrenForRelink(list_not_to_relink) 

1143 

1144 def _loadInfoParent(self): 

1145 ret = self.cowutil.getParent(self.path, LvmCowUtil.extractUuid) 

1146 if ret: 

1147 self.parentUuid = ret 

1148 

1149 def _setParent(self, parent) -> None: 

1150 self.cowutil.setParent(self.path, parent.path, False) 

1151 self.parent = parent 

1152 self.parentUuid = parent.uuid 

1153 parent.children.append(self) 

1154 try: 

1155 self.setConfig(self.DB_VDI_PARENT, self.parentUuid) 

1156 Util.log("Updated the vhd-parent field for child %s with %s" % \ 

1157 (self.uuid, self.parentUuid)) 

1158 except: 

1159 Util.log("Failed to update %s with vhd-parent field %s" % \ 

1160 (self.uuid, self.parentUuid)) 

1161 

1162 def _ensureParentActiveForRelink(self) -> None: 

1163 pass 

1164 

1165 def _update_vhd_parent(self, real_parent_uuid): 

1166 try: 

1167 self.setConfig(self.DB_VDI_PARENT, real_parent_uuid) 

1168 Util.log("Updated the vhd-parent field for child %s with real parent %s following a online coalesce" % \ 

1169 (self.uuid, real_parent_uuid)) 

1170 except: 

1171 Util.log("Failed to update %s with vhd-parent field %s" % \ 

1172 (self.uuid, real_parent_uuid)) 

1173 

1174 def isHidden(self) -> bool: 

1175 if self._hidden is None: 1175 ↛ 1176line 1175 didn't jump to line 1176, because the condition on line 1175 was never true

1176 self._loadInfoHidden() 

1177 return self._hidden 

1178 

1179 def _loadInfoHidden(self) -> None: 

1180 hidden = self.cowutil.getHidden(self.path) 

1181 self._hidden = (hidden != 0) 

1182 

1183 def _setHidden(self, hidden=True) -> None: 

1184 self._hidden = None 

1185 self.cowutil.setHidden(self.path, hidden) 

1186 self._hidden = hidden 

1187 

1188 def _increaseSizeVirt(self, size, atomic=True) -> None: 

1189 """ensure the virtual size of 'self' is at least 'size'. Note that 

1190 resizing a COW image must always be offline and atomically: the file must 

1191 not be open by anyone and no concurrent operations may take place. 

1192 Thus we use the Agent API call for performing paused atomic 

1193 operations. If the caller is already in the atomic context, it must 

1194 call with atomic = False""" 

1195 if self.sizeVirt >= size: 1195 ↛ 1197line 1195 didn't jump to line 1197, because the condition on line 1195 was never false

1196 return 

1197 Util.log(" Expanding COW image virt size for VDI %s: %s -> %s" % \ 

1198 (self, Util.num2str(self.sizeVirt), Util.num2str(size))) 

1199 

1200 msize = self.cowutil.getMaxResizeSize(self.path) 

1201 if (size <= msize): 

1202 self.cowutil.setSizeVirtFast(self.path, size) 

1203 else: 

1204 if atomic: 

1205 vdiList = self._getAllSubtree() 

1206 self.sr.lock() 

1207 try: 

1208 self.sr.pauseVDIs(vdiList) 

1209 try: 

1210 self._setSizeVirt(size) 

1211 finally: 

1212 self.sr.unpauseVDIs(vdiList) 

1213 finally: 

1214 self.sr.unlock() 

1215 else: 

1216 self._setSizeVirt(size) 

1217 

1218 self.sizeVirt = self.cowutil.getSizeVirt(self.path) 

1219 

1220 def _setSizeVirt(self, size) -> None: 

1221 """WARNING: do not call this method directly unless all VDIs in the 

1222 subtree are guaranteed to be unplugged (and remain so for the duration 

1223 of the operation): this operation is only safe for offline COW images""" 

1224 jFile = os.path.join(self.sr.path, self.uuid) 

1225 self.cowutil.setSizeVirt(self.path, size, jFile) 

1226 

1227 def _queryCowBlocks(self) -> bytes: 

1228 return self.cowutil.getBlockBitmap(self.path) 

1229 

1230 def _getCoalescedSizeData(self): 

1231 """Get the data size of the resulting image if we coalesce self onto 

1232 parent. We calculate the actual size by using the image block allocation 

1233 information (as opposed to just adding up the two image sizes to get an 

1234 upper bound)""" 

1235 # make sure we don't use stale BAT info from vdi_rec since the child 

1236 # was writable all this time 

1237 self.delConfig(VDI.DB_VDI_BLOCKS) 

1238 blocksChild = self.getVDIBlocks() 

1239 blocksParent = self.parent.getVDIBlocks() 

1240 numBlocks = Util.countBits(blocksChild, blocksParent) 

1241 Util.log("Num combined blocks = %d" % numBlocks) 

1242 sizeData = numBlocks * self.cowutil.getBlockSize(self.path) 

1243 assert(sizeData <= self.sizeVirt) 

1244 return sizeData 

1245 

1246 def _calcExtraSpaceForCoalescing(self) -> int: 

1247 sizeData = self._getCoalescedSizeData() 

1248 sizeCoalesced = sizeData + self.cowutil.calcOverheadBitmap(sizeData) + \ 

1249 self.cowutil.calcOverheadEmpty(self.sizeVirt) 

1250 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced)) 

1251 return sizeCoalesced - self.parent.getSizePhys() 

1252 

1253 def _calcExtraSpaceForLeafCoalescing(self) -> int: 

1254 """How much extra space in the SR will be required to 

1255 [live-]leaf-coalesce this VDI""" 

1256 # the space requirements are the same as for inline coalesce 

1257 return self._calcExtraSpaceForCoalescing() 

1258 

1259 def _calcExtraSpaceForSnapshotCoalescing(self) -> int: 

1260 """How much extra space in the SR will be required to 

1261 snapshot-coalesce this VDI""" 

1262 return self._calcExtraSpaceForCoalescing() + \ 

1263 self.cowutil.calcOverheadEmpty(self.sizeVirt) # extra snap leaf 

1264 

1265 def _getAllSubtree(self): 

1266 """Get self and all VDIs in the subtree of self as a flat list""" 

1267 vdiList = [self] 

1268 for child in self.children: 

1269 vdiList.extend(child._getAllSubtree()) 

1270 return vdiList 

1271 

1272 

1273class FileVDI(VDI): 

1274 """Object representing a VDI in a file-based SR (EXT or NFS)""" 

1275 

1276 @override 

1277 @staticmethod 

1278 def extractUuid(path): 

1279 fileName = os.path.basename(path) 

1280 return os.path.splitext(fileName)[0] 

1281 

1282 def __init__(self, sr, uuid, vdi_type): 

1283 VDI.__init__(self, sr, uuid, vdi_type) 

1284 self.fileName = "%s%s" % (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type]) 

1285 

1286 @override 

1287 def load(self, info=None) -> None: 

1288 if not info: 

1289 if not util.pathexists(self.path): 

1290 raise util.SMException("%s not found" % self.path) 

1291 try: 

1292 info = self.cowutil.getInfo(self.path, self.extractUuid) 

1293 except util.SMException: 

1294 Util.log(" [VDI %s: failed to read COW image metadata]" % self.uuid) 

1295 return 

1296 self.parent = None 

1297 self.children = [] 

1298 self.parentUuid = info.parentUuid 

1299 self.sizeVirt = info.sizeVirt 

1300 self._sizePhys = info.sizePhys 

1301 self._sizeAllocated = info.sizeAllocated 

1302 self._hidden = info.hidden 

1303 self.scanError = False 

1304 self.path = os.path.join(self.sr.path, "%s%s" % \ 

1305 (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type])) 

1306 

1307 @override 

1308 def rename(self, uuid) -> None: 

1309 oldPath = self.path 

1310 VDI.rename(self, uuid) 

1311 self.fileName = "%s%s" % (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type]) 

1312 self.path = os.path.join(self.sr.path, self.fileName) 

1313 assert(not util.pathexists(self.path)) 

1314 Util.log("Renaming %s -> %s" % (oldPath, self.path)) 

1315 os.rename(oldPath, self.path) 

1316 

1317 @override 

1318 def delete(self) -> None: 

1319 if len(self.children) > 0: 1319 ↛ 1320line 1319 didn't jump to line 1320, because the condition on line 1319 was never true

1320 raise util.SMException("VDI %s has children, can't delete" % \ 

1321 self.uuid) 

1322 try: 

1323 self.sr.lock() 

1324 try: 

1325 os.unlink(self.path) 

1326 self.sr.forgetVDI(self.uuid) 

1327 finally: 

1328 self.sr.unlock() 

1329 except OSError: 

1330 raise util.SMException("os.unlink(%s) failed" % self.path) 

1331 VDI.delete(self) 

1332 

1333 @override 

1334 def getAllocatedSize(self) -> int: 

1335 if self._sizeAllocated == -1: 1335 ↛ 1336line 1335 didn't jump to line 1336, because the condition on line 1335 was never true

1336 self._sizeAllocated = self.cowutil.getAllocatedSize(self.path) 

1337 return self._sizeAllocated 

1338 

1339 

1340class LVMVDI(VDI): 

1341 """Object representing a VDI in an LVM SR""" 

1342 

1343 JRN_ZERO = "zero" # journal entry type for zeroing out end of parent 

1344 

1345 @override 

1346 def load(self, info=None) -> None: 

1347 # `info` is always set. `None` default value is only here to match parent method. 

1348 assert info, "No info given to LVMVDI.load" 

1349 self.parent = None 

1350 self.children = [] 

1351 self._sizePhys = -1 

1352 self._sizeAllocated = -1 

1353 self.scanError = info.scanError 

1354 self.sizeLV = info.sizeLV 

1355 self.sizeVirt = info.sizeVirt 

1356 self.fileName = info.lvName 

1357 self.lvActive = info.lvActive 

1358 self.lvOpen = info.lvOpen 

1359 self.lvReadonly = info.lvReadonly 

1360 self._hidden = info.hidden 

1361 self.parentUuid = info.parentUuid 

1362 self.path = os.path.join(self.sr.path, self.fileName) 

1363 self.lvmcowutil = LvmCowUtil(self.cowutil) 

1364 

1365 @override 

1366 @staticmethod 

1367 def extractUuid(path): 

1368 return LvmCowUtil.extractUuid(path) 

1369 

1370 def inflate(self, size): 

1371 """inflate the LV containing the COW image to 'size'""" 

1372 if not VdiType.isCowImage(self.vdi_type): 

1373 return 

1374 self._activate() 

1375 self.sr.lock() 

1376 try: 

1377 self.lvmcowutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, self.vdi_type, size) 

1378 util.fistpoint.activate("LVHDRT_inflating_the_parent", self.sr.uuid) 

1379 finally: 

1380 self.sr.unlock() 

1381 self.sizeLV = self.sr.lvmCache.getSize(self.fileName) 

1382 self._sizePhys = -1 

1383 self._sizeAllocated = -1 

1384 

1385 def deflate(self): 

1386 """deflate the LV containing the image to minimum""" 

1387 if not VdiType.isCowImage(self.vdi_type): 

1388 return 

1389 self._activate() 

1390 self.sr.lock() 

1391 try: 

1392 self.lvmcowutil.deflate(self.sr.lvmCache, self.fileName, self.getSizePhys()) 

1393 finally: 

1394 self.sr.unlock() 

1395 self.sizeLV = self.sr.lvmCache.getSize(self.fileName) 

1396 self._sizePhys = -1 

1397 self._sizeAllocated = -1 

1398 

1399 def inflateFully(self): 

1400 self.inflate(self.lvmcowutil.calcVolumeSize(self.sizeVirt)) 

1401 

1402 def inflateParentForCoalesce(self): 

1403 """Inflate the parent only as much as needed for the purposes of 

1404 coalescing""" 

1405 if not VdiType.isCowImage(self.parent.vdi_type): 

1406 return 

1407 inc = self._calcExtraSpaceForCoalescing() 

1408 if inc > 0: 

1409 util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent", self.sr.uuid) 

1410 self.parent.inflate(self.parent.sizeLV + inc) 

1411 

1412 @override 

1413 def updateBlockInfo(self) -> Optional[str]: 

1414 if VdiType.isCowImage(self.vdi_type): 

1415 return VDI.updateBlockInfo(self) 

1416 return None 

1417 

1418 @override 

1419 def rename(self, uuid) -> None: 

1420 oldUuid = self.uuid 

1421 oldLVName = self.fileName 

1422 VDI.rename(self, uuid) 

1423 self.fileName = LV_PREFIX[self.vdi_type] + self.uuid 

1424 self.path = os.path.join(self.sr.path, self.fileName) 

1425 assert(not self.sr.lvmCache.checkLV(self.fileName)) 

1426 

1427 self.sr.lvmCache.rename(oldLVName, self.fileName) 

1428 if self.sr.lvActivator.get(oldUuid, False): 

1429 self.sr.lvActivator.replace(oldUuid, self.uuid, self.fileName, False) 

1430 

1431 ns = NS_PREFIX_LVM + self.sr.uuid 

1432 (cnt, bcnt) = RefCounter.check(oldUuid, ns) 

1433 RefCounter.set(self.uuid, cnt, bcnt, ns) 

1434 RefCounter.reset(oldUuid, ns) 

1435 

1436 @override 

1437 def delete(self) -> None: 

1438 if len(self.children) > 0: 

1439 raise util.SMException("VDI %s has children, can't delete" % \ 

1440 self.uuid) 

1441 self.sr.lock() 

1442 try: 

1443 self.sr.lvmCache.remove(self.fileName) 

1444 self.sr.forgetVDI(self.uuid) 

1445 finally: 

1446 self.sr.unlock() 

1447 RefCounter.reset(self.uuid, NS_PREFIX_LVM + self.sr.uuid) 

1448 VDI.delete(self) 

1449 

1450 @override 

1451 def getSizePhys(self) -> int: 

1452 if self._sizePhys == -1: 

1453 self._loadInfoSizePhys() 

1454 return self._sizePhys 

1455 

1456 def _loadInfoSizePhys(self): 

1457 """Get the physical utilization of the COW image file. We do it individually 

1458 (and not using the COW batch scanner) as an optimization: this info is 

1459 relatively expensive and we need it only for VDI's involved in 

1460 coalescing.""" 

1461 if not VdiType.isCowImage(self.vdi_type): 

1462 return 

1463 self._activate() 

1464 self._sizePhys = self.cowutil.getSizePhys(self.path) 

1465 if self._sizePhys <= 0: 

1466 raise util.SMException("phys size of %s = %d" % \ 

1467 (self, self._sizePhys)) 

1468 

1469 @override 

1470 def getAllocatedSize(self) -> int: 

1471 if self._sizeAllocated == -1: 

1472 self._loadInfoSizeAllocated() 

1473 return self._sizeAllocated 

1474 

1475 def _loadInfoSizeAllocated(self): 

1476 """ 

1477 Get the allocated size of the COW volume. 

1478 """ 

1479 if not VdiType.isCowImage(self.vdi_type): 

1480 return 

1481 self._activate() 

1482 self._sizeAllocated = self.cowutil.getAllocatedSize(self.path) 

1483 

1484 @override 

1485 def _loadInfoHidden(self) -> None: 

1486 if not VdiType.isCowImage(self.vdi_type): 

1487 self._hidden = self.sr.lvmCache.getHidden(self.fileName) 

1488 else: 

1489 VDI._loadInfoHidden(self) 

1490 

1491 @override 

1492 def _setHidden(self, hidden=True) -> None: 

1493 if not VdiType.isCowImage(self.vdi_type): 

1494 self._hidden = None 

1495 self.sr.lvmCache.setHidden(self.fileName, hidden) 

1496 self._hidden = hidden 

1497 else: 

1498 VDI._setHidden(self, hidden) 

1499 

1500 @override 

1501 def __str__(self) -> str: 

1502 strType = self.vdi_type 

1503 if self.vdi_type == VdiType.RAW: 

1504 strType = "RAW" 

1505 strHidden = "" 

1506 if self.isHidden(): 

1507 strHidden = "*" 

1508 strSizePhys = "" 

1509 if self._sizePhys > 0: 

1510 strSizePhys = Util.num2str(self._sizePhys) 

1511 strSizeAllocated = "" 

1512 if self._sizeAllocated >= 0: 

1513 strSizeAllocated = Util.num2str(self._sizeAllocated) 

1514 strActive = "n" 

1515 if self.lvActive: 

1516 strActive = "a" 

1517 if self.lvOpen: 

1518 strActive += "o" 

1519 return "%s%s[%s](%s/%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType, 

1520 Util.num2str(self.sizeVirt), strSizePhys, strSizeAllocated, 

1521 Util.num2str(self.sizeLV), strActive) 

1522 

1523 @override 

1524 def validate(self, fast=False) -> None: 

1525 if VdiType.isCowImage(self.vdi_type): 

1526 VDI.validate(self, fast) 

1527 

1528 def _setChainRw(self) -> List[str]: 

1529 """ 

1530 Set the readonly LV and children writable. 

1531 It's needed because the coalesce can be done by tapdisk directly 

1532 and it will need to write parent information for children. 

1533 The VDI we want to coalesce into it's parent need to be writable for libqcow coalesce part. 

1534 Return a list of the LV that were previously readonly to be made RO again after the coalesce. 

1535 """ 

1536 was_ro = [] 

1537 if self.lvReadonly: 

1538 self.sr.lvmCache.setReadonly(self.fileName, False) 

1539 was_ro.append(self.fileName) 

1540 

1541 for child in self.children: 

1542 if child.lvReadonly: 

1543 self.sr.lvmCache.setReadonly(child.fileName, False) 

1544 was_ro.append(child.fileName) 

1545 

1546 return was_ro 

1547 

1548 def _setChainRo(self, was_ro: List[str]) -> None: 

1549 """Set the list of LV in parameters to readonly""" 

1550 for lvName in was_ro: 

1551 self.sr.lvmCache.setReadonly(lvName, True) 

1552 

1553 @override 

1554 def _doCoalesce(self) -> None: 

1555 """LVMVDI parents must first be activated, inflated, and made writable""" 

1556 was_ro = [] 

1557 try: 

1558 self._activateChain() 

1559 self.sr.lvmCache.setReadonly(self.parent.fileName, False) 

1560 self.parent.validate() 

1561 self.inflateParentForCoalesce() 

1562 was_ro = self._setChainRw() 

1563 VDI._doCoalesce(self) 

1564 finally: 

1565 self.parent._loadInfoSizePhys() 

1566 self.parent.deflate() 

1567 self.sr.lvmCache.setReadonly(self.parent.fileName, True) 

1568 self._setChainRo(was_ro) 

1569 

1570 @override 

1571 def _setParent(self, parent) -> None: 

1572 self._activate() 

1573 if self.lvReadonly: 

1574 self.sr.lvmCache.setReadonly(self.fileName, False) 

1575 

1576 try: 

1577 self.cowutil.setParent(self.path, parent.path, parent.vdi_type == VdiType.RAW) 

1578 finally: 

1579 if self.lvReadonly: 

1580 self.sr.lvmCache.setReadonly(self.fileName, True) 

1581 self._deactivate() 

1582 self.parent = parent 

1583 self.parentUuid = parent.uuid 

1584 parent.children.append(self) 

1585 try: 

1586 self.setConfig(self.DB_VDI_PARENT, self.parentUuid) 

1587 Util.log("Updated the VDI-parent field for child %s with %s" % \ 

1588 (self.uuid, self.parentUuid)) 

1589 except: 

1590 Util.log("Failed to update the VDI-parent with %s for child %s" % \ 

1591 (self.parentUuid, self.uuid)) 

1592 

1593 def _activate(self): 

1594 self.sr.lvActivator.activate(self.uuid, self.fileName, False) 

1595 

1596 def _activateChain(self): 

1597 vdi = self 

1598 while vdi: 

1599 vdi._activate() 

1600 vdi = vdi.parent 

1601 

1602 def _deactivate(self): 

1603 self.sr.lvActivator.deactivate(self.uuid, False) 

1604 

1605 @override 

1606 def _ensureParentActiveForRelink(self) -> None: 

1607 self.parent._activate() 

1608 

1609 @override 

1610 def _increaseSizeVirt(self, size, atomic=True) -> None: 

1611 "ensure the virtual size of 'self' is at least 'size'" 

1612 self._activate() 

1613 if VdiType.isCowImage(self.vdi_type): 

1614 VDI._increaseSizeVirt(self, size, atomic) 

1615 return 

1616 

1617 # raw VDI case 

1618 offset = self.sizeLV 

1619 if self.sizeVirt < size: 

1620 oldSize = self.sizeLV 

1621 self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size) 

1622 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV)) 

1623 self.sr.lvmCache.setSize(self.fileName, self.sizeLV) 

1624 offset = oldSize 

1625 unfinishedZero = False 

1626 jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid) 

1627 if jval: 

1628 unfinishedZero = True 

1629 offset = int(jval) 

1630 length = self.sizeLV - offset 

1631 if not length: 

1632 return 

1633 

1634 if unfinishedZero: 

1635 Util.log(" ==> Redoing unfinished zeroing out") 

1636 else: 

1637 self.sr.journaler.create(self.JRN_ZERO, self.uuid, \ 

1638 str(offset)) 

1639 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length)) 

1640 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 

1641 func = lambda: util.zeroOut(self.path, offset, length) 

1642 Util.runAbortable(func, True, self.sr.uuid, abortTest, 

1643 VDI.POLL_INTERVAL, 0) 

1644 self.sr.journaler.remove(self.JRN_ZERO, self.uuid) 

1645 

1646 @override 

1647 def _setSizeVirt(self, size) -> None: 

1648 """WARNING: do not call this method directly unless all VDIs in the 

1649 subtree are guaranteed to be unplugged (and remain so for the duration 

1650 of the operation): this operation is only safe for offline COW images.""" 

1651 self._activate() 

1652 jFile = self.lvmcowutil.createResizeJournal(self.sr.lvmCache, self.uuid) 

1653 try: 

1654 self.lvmcowutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid, self.vdi_type, size, jFile) 

1655 finally: 

1656 self.lvmcowutil.destroyResizeJournal(self.sr.lvmCache, self.uuid) 

1657 

1658 @override 

1659 def _queryCowBlocks(self) -> bytes: 

1660 self._activate() 

1661 return VDI._queryCowBlocks(self) 

1662 

1663 @override 

1664 def getParent(self) -> str: 

1665 self._activate() 

1666 parent = VDI.getParent(self) 

1667 self._deactivate() 

1668 return parent 

1669 

1670 @override 

1671 def _calcExtraSpaceForCoalescing(self) -> int: 

1672 if not VdiType.isCowImage(self.parent.vdi_type): 

1673 return 0 # raw parents are never deflated in the first place 

1674 sizeCoalesced = self.lvmcowutil.calcVolumeSize(self._getCoalescedSizeData()) 

1675 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced)) 

1676 return sizeCoalesced - self.parent.sizeLV 

1677 

1678 @override 

1679 def _calcExtraSpaceForLeafCoalescing(self) -> int: 

1680 """How much extra space in the SR will be required to 

1681 [live-]leaf-coalesce this VDI""" 

1682 # we can deflate the leaf to minimize the space requirements 

1683 deflateDiff = self.sizeLV - lvutil.calcSizeLV(self.getSizePhys()) 

1684 return self._calcExtraSpaceForCoalescing() - deflateDiff 

1685 

1686 @override 

1687 def _calcExtraSpaceForSnapshotCoalescing(self) -> int: 

1688 return self._calcExtraSpaceForCoalescing() + \ 

1689 lvutil.calcSizeLV(self.getSizePhys()) 

1690 

1691 

1692class LinstorVDI(VDI): 

1693 """Object representing a VDI in a LINSTOR SR""" 

1694 

1695 VOLUME_LOCK_TIMEOUT = 30 

1696 

1697 @override 

1698 def load(self, info=None) -> None: 

1699 self.parentUuid = info.parentUuid 

1700 self.scanError = True 

1701 self.parent = None 

1702 self.children = [] 

1703 

1704 self.fileName = self.sr._linstor.get_volume_name(self.uuid) 

1705 self.path = self.sr._linstor.build_device_path(self.fileName) 

1706 self.linstorcowutil = LinstorCowUtil(self.sr.xapi.session, self.sr._linstor, info.vdiType) 

1707 

1708 if not info: 

1709 try: 

1710 info = self.linstorcowutil.get_info(self.uuid) 

1711 except util.SMException: 

1712 Util.log( 

1713 ' [VDI {}: failed to read COW image metadata]'.format(self.uuid) 

1714 ) 

1715 return 

1716 

1717 self.parentUuid = info.parentUuid 

1718 self.sizeVirt = info.sizeVirt 

1719 self._sizePhys = -1 

1720 self._sizeAllocated = -1 

1721 self.drbd_size = -1 

1722 self._hidden = info.hidden 

1723 self.scanError = False 

1724 

1725 @override 

1726 def getSizePhys(self, fetch=False) -> int: 

1727 if self._sizePhys < 0 or fetch: 

1728 self._sizePhys = self.linstorcowutil.get_size_phys(self.uuid) 

1729 return self._sizePhys 

1730 

1731 def getDrbdSize(self, fetch=False): 

1732 if self.drbd_size < 0 or fetch: 

1733 self.drbd_size = self.linstorcowutil.get_drbd_size(self.uuid) 

1734 return self.drbd_size 

1735 

1736 @override 

1737 def getAllocatedSize(self) -> int: 

1738 if self._sizeAllocated == -1: 

1739 if VdiType.isCowImage(self.vdi_type): 

1740 self._sizeAllocated = self.linstorcowutil.get_allocated_size(self.uuid) 

1741 return self._sizeAllocated 

1742 

1743 def inflate(self, size): 

1744 if not VdiType.isCowImage(self.vdi_type): 

1745 return 

1746 self.sr.lock() 

1747 try: 

1748 # Ensure we use the real DRBD size and not the cached one. 

1749 # Why? Because this attribute can be changed if volume is resized by user. 

1750 self.drbd_size = self.getDrbdSize(fetch=True) 

1751 self.linstorcowutil.inflate(self.sr.journaler, self.uuid, self.path, size, self.drbd_size) 

1752 finally: 

1753 self.sr.unlock() 

1754 self.drbd_size = -1 

1755 self._sizePhys = -1 

1756 self._sizeAllocated = -1 

1757 

1758 def deflate(self): 

1759 if not VdiType.isCowImage(self.vdi_type): 

1760 return 

1761 self.sr.lock() 

1762 try: 

1763 # Ensure we use the real sizes and not the cached info. 

1764 self.drbd_size = self.getDrbdSize(fetch=True) 

1765 self._sizePhys = self.getSizePhys(fetch=True) 

1766 self.linstorcowutil.force_deflate(self.path, self._sizePhys, self.drbd_size, zeroize=False) 

1767 finally: 

1768 self.sr.unlock() 

1769 self.drbd_size = -1 

1770 self._sizePhys = -1 

1771 self._sizeAllocated = -1 

1772 

1773 def inflateFully(self): 

1774 if VdiType.isCowImage(self.vdi_type): 

1775 self.inflate(self.linstorcowutil.compute_volume_size(self.sizeVirt)) 

1776 

1777 @override 

1778 def rename(self, uuid) -> None: 

1779 Util.log('Renaming {} -> {} (path={})'.format( 

1780 self.uuid, uuid, self.path 

1781 )) 

1782 self.sr._linstor.update_volume_uuid(self.uuid, uuid) 

1783 VDI.rename(self, uuid) 

1784 

1785 @override 

1786 def delete(self) -> None: 

1787 if len(self.children) > 0: 

1788 raise util.SMException( 

1789 'VDI {} has children, can\'t delete'.format(self.uuid) 

1790 ) 

1791 self.sr.lock() 

1792 try: 

1793 self.sr._linstor.destroy_volume(self.uuid) 

1794 self.sr.forgetVDI(self.uuid) 

1795 finally: 

1796 self.sr.unlock() 

1797 VDI.delete(self) 

1798 

1799 @override 

1800 def validate(self, fast=False) -> None: 

1801 if VdiType.isCowImage(self.vdi_type) and self.linstorcowutil.check(self.uuid, fast=fast) != CowUtil.CheckResult.Success: 

1802 raise util.SMException('COW image {} corrupted'.format(self)) 

1803 

1804 @override 

1805 def pause(self, failfast=False) -> None: 

1806 self.sr._linstor.ensure_volume_is_not_locked( 

1807 self.uuid, timeout=self.VOLUME_LOCK_TIMEOUT 

1808 ) 

1809 return super(LinstorVDI, self).pause(failfast) 

1810 

1811 @override 

1812 def coalesce(self) -> int: 

1813 # Note: We raise `SMException` here to skip the current coalesce in case of failure. 

1814 # Using another exception we can't execute the next coalesce calls. 

1815 return self.linstorcowutil.force_coalesce(self.path) 

1816 

1817 @override 

1818 def getParent(self) -> str: 

1819 return self.linstorcowutil.get_parent( 

1820 self.sr._linstor.get_volume_uuid_from_device_path(self.path) 

1821 ) 

1822 

1823 @override 

1824 def repair(self, parent_uuid) -> None: 

1825 self.linstorcowutil.force_repair( 

1826 self.sr._linstor.get_device_path(parent_uuid) 

1827 ) 

1828 

1829 @override 

1830 def _relinkSkip(self) -> None: 

1831 abortFlag = IPCFlag(self.sr.uuid) 

1832 for child in self.children: 

1833 if abortFlag.test(FLAG_TYPE_ABORT): 

1834 raise AbortException('Aborting due to signal') 

1835 Util.log( 

1836 ' Relinking {} from {} to {}'.format( 

1837 child, self, self.parent 

1838 ) 

1839 ) 

1840 

1841 session = child.sr.xapi.session 

1842 sr_uuid = child.sr.uuid 

1843 vdi_uuid = child.uuid 

1844 try: 

1845 self.sr._linstor.ensure_volume_is_not_locked( 

1846 vdi_uuid, timeout=self.VOLUME_LOCK_TIMEOUT 

1847 ) 

1848 blktap2.VDI.tap_pause(session, sr_uuid, vdi_uuid) 

1849 child._setParent(self.parent) 

1850 finally: 

1851 blktap2.VDI.tap_unpause(session, sr_uuid, vdi_uuid) 

1852 self.children = [] 

1853 

1854 @override 

1855 def _setParent(self, parent) -> None: 

1856 self.sr._linstor.get_device_path(self.uuid) 

1857 self.linstorcowutil.force_parent(self.path, parent.path) 

1858 self.parent = parent 

1859 self.parentUuid = parent.uuid 

1860 parent.children.append(self) 

1861 try: 

1862 self.setConfig(self.DB_VDI_PARENT, self.parentUuid) 

1863 Util.log("Updated the vhd-parent field for child %s with %s" % \ 

1864 (self.uuid, self.parentUuid)) 

1865 except: 

1866 Util.log("Failed to update %s with vhd-parent field %s" % \ 

1867 (self.uuid, self.parentUuid)) 

1868 

1869 @override 

1870 def _doCoalesce(self) -> None: 

1871 try: 

1872 self._activateChain() 

1873 self.parent.validate() 

1874 self._inflateParentForCoalesce() 

1875 VDI._doCoalesce(self) 

1876 finally: 

1877 self.parent.deflate() 

1878 

1879 def _activateChain(self): 

1880 vdi = self 

1881 while vdi: 

1882 try: 

1883 p = self.sr._linstor.get_device_path(vdi.uuid) 

1884 except Exception as e: 

1885 # Use SMException to skip coalesce. 

1886 # Otherwise the GC is stopped... 

1887 raise util.SMException(str(e)) 

1888 vdi = vdi.parent 

1889 

1890 @override 

1891 def _setHidden(self, hidden=True) -> None: 

1892 HIDDEN_TAG = 'hidden' 

1893 

1894 if not VdiType.isCowImage(self.vdi_type): 

1895 self._hidden = None 

1896 self.sr._linstor.update_volume_metadata(self.uuid, { 

1897 HIDDEN_TAG: hidden 

1898 }) 

1899 self._hidden = hidden 

1900 else: 

1901 VDI._setHidden(self, hidden) 

1902 

1903 @override 

1904 def _increaseSizeVirt(self, size, atomic=True): 

1905 if self.vdi_type == VdiType.RAW: 

1906 offset = self.drbd_size 

1907 if self.sizeVirt < size: 

1908 oldSize = self.drbd_size 

1909 self.drbd_size = LinstorVolumeManager.round_up_volume_size(size) 

1910 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.drbd_size)) 

1911 self.sr._linstor.resize_volume(self.uuid, self.drbd_size) 

1912 offset = oldSize 

1913 unfinishedZero = False 

1914 jval = self.sr.journaler.get(LinstorJournaler.ZERO, self.uuid) 

1915 if jval: 

1916 unfinishedZero = True 

1917 offset = int(jval) 

1918 length = self.drbd_size - offset 

1919 if not length: 

1920 return 

1921 

1922 if unfinishedZero: 

1923 Util.log(" ==> Redoing unfinished zeroing out") 

1924 else: 

1925 self.sr.journaler.create(LinstorJournaler.ZERO, self.uuid, str(offset)) 

1926 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length)) 

1927 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 

1928 func = lambda: util.zeroOut(self.path, offset, length) 

1929 Util.runAbortable(func, True, self.sr.uuid, abortTest, VDI.POLL_INTERVAL, 0) 

1930 self.sr.journaler.remove(LinstorJournaler.ZERO, self.uuid) 

1931 return 

1932 

1933 if self.sizeVirt >= size: 

1934 return 

1935 Util.log(" Expanding COW image virt size for VDI %s: %s -> %s" % \ 

1936 (self, Util.num2str(self.sizeVirt), Util.num2str(size))) 

1937 

1938 msize = self.linstorcowutil.get_max_resize_size(self.uuid) * 1024 * 1024 

1939 if (size <= msize): 

1940 self.linstorcowutil.set_size_virt_fast(self.path, size) 

1941 else: 

1942 if atomic: 

1943 vdiList = self._getAllSubtree() 

1944 self.sr.lock() 

1945 try: 

1946 self.sr.pauseVDIs(vdiList) 

1947 try: 

1948 self._setSizeVirt(size) 

1949 finally: 

1950 self.sr.unpauseVDIs(vdiList) 

1951 finally: 

1952 self.sr.unlock() 

1953 else: 

1954 self._setSizeVirt(size) 

1955 

1956 self.sizeVirt = self.linstorcowutil.get_size_virt(self.uuid) 

1957 

1958 @override 

1959 def _setSizeVirt(self, size) -> None: 

1960 jfile = self.uuid + '-jvhd' 

1961 self.sr._linstor.create_volume( 

1962 jfile, self.cowutil.getResizeJournalSize(), persistent=False, volume_name=jfile 

1963 ) 

1964 try: 

1965 self.inflate(self.linstorcowutil.compute_volume_size(size)) 

1966 self.linstorcowutil.set_size_virt(self.path, size, jfile) 

1967 finally: 

1968 try: 

1969 self.sr._linstor.destroy_volume(jfile) 

1970 except Exception: 

1971 # We can ignore it, in any case this volume is not persistent. 

1972 pass 

1973 

1974 @override 

1975 def _queryCowBlocks(self) -> bytes: 

1976 return self.linstorcowutil.get_block_bitmap(self.uuid) 

1977 

1978 def _inflateParentForCoalesce(self): 

1979 if not VdiType.isCowImage(self.parent.vdi_type): 

1980 return 

1981 inc = self._calcExtraSpaceForCoalescing() 

1982 if inc > 0: 

1983 self.parent.inflate(self.parent.getDrbdSize() + inc) 

1984 

1985 @override 

1986 def _calcExtraSpaceForCoalescing(self) -> int: 

1987 if not VdiType.isCowImage(self.parent.vdi_type): 

1988 return 0 

1989 size_coalesced = self.linstorcowutil.compute_volume_size(self._getCoalescedSizeData()) 

1990 Util.log("Coalesced size = %s" % Util.num2str(size_coalesced)) 

1991 return size_coalesced - self.parent.getDrbdSize() 

1992 

1993 @override 

1994 def _calcExtraSpaceForLeafCoalescing(self) -> int: 

1995 assert self.getDrbdSize() > 0 

1996 assert self.getSizePhys() > 0 

1997 deflate_diff = self.getDrbdSize() - LinstorVolumeManager.round_up_volume_size(self.getSizePhys()) 

1998 assert deflate_diff >= 0 

1999 return self._calcExtraSpaceForCoalescing() - deflate_diff 

2000 

2001 @override 

2002 def _calcExtraSpaceForSnapshotCoalescing(self) -> int: 

2003 assert self.getSizePhys() > 0 

2004 return self._calcExtraSpaceForCoalescing() + \ 

2005 LinstorVolumeManager.round_up_volume_size(self.getSizePhys()) 

2006 

2007################################################################################ 

2008# 

2009# SR 

2010# 

2011class SR(object): 

2012 class LogFilter: 

2013 def __init__(self, sr): 

2014 self.sr = sr 

2015 self.stateLogged = False 

2016 self.prevState = {} 

2017 self.currState = {} 

2018 

2019 def logState(self): 

2020 changes = "" 

2021 self.currState.clear() 

2022 for vdi in self.sr.vdiTrees: 

2023 self.currState[vdi.uuid] = self._getTreeStr(vdi) 

2024 if not self.prevState.get(vdi.uuid) or \ 

2025 self.prevState[vdi.uuid] != self.currState[vdi.uuid]: 

2026 changes += self.currState[vdi.uuid] 

2027 

2028 for uuid in self.prevState: 

2029 if not self.currState.get(uuid): 

2030 changes += "Tree %s gone\n" % uuid 

2031 

2032 result = "SR %s (%d VDIs in %d COW trees): " % \ 

2033 (self.sr, len(self.sr.vdis), len(self.sr.vdiTrees)) 

2034 

2035 if len(changes) > 0: 

2036 if self.stateLogged: 

2037 result += "showing only COW trees that changed:" 

2038 result += "\n%s" % changes 

2039 else: 

2040 result += "no changes" 

2041 

2042 for line in result.split("\n"): 

2043 Util.log("%s" % line) 

2044 self.prevState.clear() 

2045 for key, val in self.currState.items(): 

2046 self.prevState[key] = val 

2047 self.stateLogged = True 

2048 

2049 def logNewVDI(self, uuid): 

2050 if self.stateLogged: 

2051 Util.log("Found new VDI when scanning: %s" % uuid) 

2052 

2053 def _getTreeStr(self, vdi, indent=8): 

2054 treeStr = "%s%s\n" % (" " * indent, vdi) 

2055 for child in vdi.children: 

2056 treeStr += self._getTreeStr(child, indent + VDI.STR_TREE_INDENT) 

2057 return treeStr 

2058 

2059 TYPE_FILE = "file" 

2060 TYPE_LVHD = "lvhd" 

2061 TYPE_LINSTOR = "linstor" 

2062 TYPES = [TYPE_LVHD, TYPE_FILE, TYPE_LINSTOR] 

2063 

2064 LOCK_RETRY_INTERVAL = 3 

2065 LOCK_RETRY_ATTEMPTS = 20 

2066 LOCK_RETRY_ATTEMPTS_LOCK = 100 

2067 

2068 SCAN_RETRY_ATTEMPTS = 3 

2069 

2070 JRN_CLONE = "clone" # journal entry type for the clone operation (from SM) 

2071 TMP_RENAME_PREFIX = "OLD_" 

2072 

2073 KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline" 

2074 KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override" 

2075 

2076 @staticmethod 

2077 def getInstance(uuid, xapiSession, createLock=True, force=False): 

2078 xapi = XAPI(xapiSession, uuid) 

2079 type = normalizeType(xapi.srRecord["type"]) 

2080 if type == SR.TYPE_FILE: 

2081 return FileSR(uuid, xapi, createLock, force) 

2082 elif type == SR.TYPE_LVHD: 

2083 return LVMSR(uuid, xapi, createLock, force) 

2084 elif type == SR.TYPE_LINSTOR: 

2085 return LinstorSR(uuid, xapi, createLock, force) 

2086 raise util.SMException("SR type %s not recognized" % type) 

2087 

2088 def __init__(self, uuid, xapi, createLock, force): 

2089 self.logFilter = self.LogFilter(self) 

2090 self.uuid = uuid 

2091 self.path = "" 

2092 self.name = "" 

2093 self.vdis = {} 

2094 self.vdiTrees = [] 

2095 self.journaler = None 

2096 self.xapi = xapi 

2097 self._locked = 0 

2098 self._srLock = None 

2099 if createLock: 2099 ↛ 2100line 2099 didn't jump to line 2100, because the condition on line 2099 was never true

2100 self._srLock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid) 

2101 else: 

2102 Util.log("Requested no SR locking") 

2103 self.name = self.xapi.srRecord["name_label"] 

2104 self._failedCoalesceTargets = [] 

2105 

2106 if not self.xapi.isPluggedHere(): 

2107 if force: 2107 ↛ 2108line 2107 didn't jump to line 2108, because the condition on line 2107 was never true

2108 Util.log("SR %s not attached on this host, ignoring" % uuid) 

2109 else: 

2110 if not self.wait_for_plug(): 

2111 raise util.SMException("SR %s not attached on this host" % uuid) 

2112 

2113 if force: 2113 ↛ 2114line 2113 didn't jump to line 2114, because the condition on line 2113 was never true

2114 Util.log("Not checking if we are Master (SR %s)" % uuid) 

2115 elif not self.xapi.isMaster(): 2115 ↛ 2116line 2115 didn't jump to line 2116, because the condition on line 2115 was never true

2116 raise util.SMException("This host is NOT master, will not run") 

2117 

2118 self.no_space_candidates = {} 

2119 

2120 def msg_cleared(self, xapi_session, msg_ref): 

2121 try: 

2122 msg = xapi_session.xenapi.message.get_record(msg_ref) 

2123 except XenAPI.Failure: 

2124 return True 

2125 

2126 return msg is None 

2127 

2128 def check_no_space_candidates(self): 

2129 with util.ApiSession("SMGC-check-no-space") as xapi_session: 

2130 msg_id = self.xapi.srRecord["sm_config"].get(VDI.DB_GC_NO_SPACE) 

2131 if self.no_space_candidates: 

2132 if msg_id is None or self.msg_cleared(xapi_session, msg_id): 

2133 util.SMlog("Could not coalesce due to a lack of space " 

2134 f"in SR {self.uuid}") 

2135 msg_body = ("Unable to perform data coalesce due to a lack " 

2136 f"of space in SR {self.uuid}") 

2137 msg_id = xapi_session.xenapi.message.create( 

2138 'SM_GC_NO_SPACE', 

2139 3, 

2140 "SR", 

2141 self.uuid, 

2142 msg_body) 

2143 xapi_session.xenapi.SR.remove_from_sm_config( 

2144 self.xapi.srRef, VDI.DB_GC_NO_SPACE) 

2145 xapi_session.xenapi.SR.add_to_sm_config( 

2146 self.xapi.srRef, VDI.DB_GC_NO_SPACE, msg_id) 

2147 

2148 for candidate in self.no_space_candidates.values(): 

2149 candidate.setConfig(VDI.DB_GC_NO_SPACE, msg_id) 

2150 elif msg_id is not None: 

2151 # Everything was coalescable, remove the message 

2152 xapi_session.xenapi.SR.remove_from_sm_config(self.xapi.srRef, VDI.DB_GC_NO_SPACE) 

2153 xapi_session.xenapi.message.destroy(msg_id) 

2154 

2155 def clear_no_space_msg(self, vdi): 

2156 msg_id = None 

2157 try: 

2158 msg_id = vdi.getConfig(VDI.DB_GC_NO_SPACE) 

2159 except XenAPI.Failure: 

2160 pass 

2161 

2162 self.no_space_candidates.pop(vdi.uuid, None) 

2163 if msg_id is not None: 2163 ↛ exitline 2163 didn't return from function 'clear_no_space_msg', because the condition on line 2163 was never false

2164 vdi.delConfig(VDI.DB_GC_NO_SPACE) 

2165 

2166 

2167 def wait_for_plug(self): 

2168 for _ in range(1, 10): 

2169 time.sleep(2) 

2170 if self.xapi.isPluggedHere(): 

2171 return True 

2172 return False 

2173 

2174 def gcEnabled(self, refresh=True): 

2175 if refresh: 

2176 self.xapi.srRecord = \ 

2177 self.xapi.session.xenapi.SR.get_record(self.xapi._srRef) 

2178 if self.xapi.srRecord["other_config"].get(VDI.DB_GC) == "false": 

2179 Util.log("GC is disabled for this SR, abort") 

2180 return False 

2181 return True 

2182 

2183 def scan(self, force=False) -> None: 

2184 """Scan the SR and load VDI info for each VDI. If called repeatedly, 

2185 update VDI objects if they already exist""" 

2186 pass 

2187 

2188 def scanLocked(self, force=False): 

2189 self.lock() 

2190 try: 

2191 self.scan(force) 

2192 finally: 

2193 self.unlock() 

2194 

2195 def getVDI(self, uuid): 

2196 return self.vdis.get(uuid) 

2197 

2198 def hasWork(self): 

2199 if len(self.findGarbage()) > 0: 

2200 return True 

2201 if self.findCoalesceable(): 

2202 return True 

2203 if self.findLeafCoalesceable(): 

2204 return True 

2205 if self.needUpdateBlockInfo(): 

2206 return True 

2207 return False 

2208 

2209 def findCoalesceable(self): 

2210 """Find a coalesceable VDI. Return a vdi that should be coalesced 

2211 (choosing one among all coalesceable candidates according to some 

2212 criteria) or None if there is no VDI that could be coalesced""" 

2213 

2214 candidates = [] 

2215 

2216 srSwitch = self.xapi.srRecord["other_config"].get(VDI.DB_COALESCE) 

2217 if srSwitch == "false": 

2218 Util.log("Coalesce disabled for this SR") 

2219 return candidates 

2220 

2221 # finish any VDI for which a relink journal entry exists first 

2222 journals = self.journaler.getAll(VDI.JRN_RELINK) 

2223 for uuid in journals: 

2224 vdi = self.getVDI(uuid) 

2225 if vdi and vdi not in self._failedCoalesceTargets: 

2226 return vdi 

2227 

2228 for vdi in self.vdis.values(): 

2229 if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets: 

2230 candidates.append(vdi) 

2231 Util.log("%s is coalescable" % vdi.uuid) 

2232 

2233 self.xapi.update_task_progress("coalescable", len(candidates)) 

2234 

2235 # pick one in the tallest tree 

2236 treeHeight = dict() 

2237 for c in candidates: 

2238 height = c.getTreeRoot().getTreeHeight() 

2239 if treeHeight.get(height): 

2240 treeHeight[height].append(c) 

2241 else: 

2242 treeHeight[height] = [c] 

2243 

2244 freeSpace = self.getFreeSpace() 

2245 heights = list(treeHeight.keys()) 

2246 heights.sort(reverse=True) 

2247 for h in heights: 

2248 for c in treeHeight[h]: 

2249 spaceNeeded = c._calcExtraSpaceForCoalescing() 

2250 if spaceNeeded <= freeSpace: 

2251 Util.log("Coalesce candidate: %s (tree height %d)" % (c, h)) 

2252 self.clear_no_space_msg(c) 

2253 return c 

2254 else: 

2255 self.no_space_candidates[c.uuid] = c 

2256 Util.log("No space to coalesce %s (free space: %d)" % \ 

2257 (c, freeSpace)) 

2258 return None 

2259 

2260 def getSwitch(self, key): 

2261 return self.xapi.srRecord["other_config"].get(key) 

2262 

2263 def forbiddenBySwitch(self, switch, condition, fail_msg): 

2264 srSwitch = self.getSwitch(switch) 

2265 ret = False 

2266 if srSwitch: 

2267 ret = srSwitch == condition 

2268 

2269 if ret: 

2270 Util.log(fail_msg) 

2271 

2272 return ret 

2273 

2274 def leafCoalesceForbidden(self): 

2275 return (self.forbiddenBySwitch(VDI.DB_COALESCE, 

2276 "false", 

2277 "Coalesce disabled for this SR") or 

2278 self.forbiddenBySwitch(VDI.DB_LEAFCLSC, 

2279 VDI.LEAFCLSC_DISABLED, 

2280 "Leaf-coalesce disabled for this SR")) 

2281 

2282 def findLeafCoalesceable(self): 

2283 """Find leaf-coalesceable VDIs in each COW tree""" 

2284 

2285 candidates = [] 

2286 if self.leafCoalesceForbidden(): 

2287 return candidates 

2288 

2289 self.gatherLeafCoalesceable(candidates) 

2290 

2291 self.xapi.update_task_progress("coalescable", len(candidates)) 

2292 

2293 freeSpace = self.getFreeSpace() 

2294 for candidate in candidates: 

2295 # check the space constraints to see if leaf-coalesce is actually 

2296 # feasible for this candidate 

2297 spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing() 

2298 spaceNeededLive = spaceNeeded 

2299 if spaceNeeded > freeSpace: 

2300 spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing() 

2301 if candidate.canLiveCoalesce(self.getStorageSpeed()): 

2302 spaceNeeded = spaceNeededLive 

2303 

2304 if spaceNeeded <= freeSpace: 

2305 Util.log("Leaf-coalesce candidate: %s" % candidate) 

2306 self.clear_no_space_msg(candidate) 

2307 return candidate 

2308 else: 

2309 Util.log("No space to leaf-coalesce %s (free space: %d)" % \ 

2310 (candidate, freeSpace)) 

2311 if spaceNeededLive <= freeSpace: 

2312 Util.log("...but enough space if skip snap-coalesce") 

2313 candidate.setConfig(VDI.DB_LEAFCLSC, 

2314 VDI.LEAFCLSC_OFFLINE) 

2315 self.no_space_candidates[candidate.uuid] = candidate 

2316 

2317 return None 

2318 

2319 def gatherLeafCoalesceable(self, candidates): 

2320 for vdi in self.vdis.values(): 

2321 if not vdi.isLeafCoalesceable(): 

2322 continue 

2323 if vdi in self._failedCoalesceTargets: 

2324 continue 

2325 if vdi.getConfig(vdi.DB_ONBOOT) == vdi.ONBOOT_RESET: 

2326 Util.log("Skipping reset-on-boot %s" % vdi) 

2327 continue 

2328 if vdi.getConfig(vdi.DB_ALLOW_CACHING): 

2329 Util.log("Skipping allow_caching=true %s" % vdi) 

2330 continue 

2331 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED: 

2332 Util.log("Leaf-coalesce disabled for %s" % vdi) 

2333 continue 

2334 if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or 

2335 vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE): 

2336 continue 

2337 candidates.append(vdi) 

2338 

2339 def coalesce(self, vdi, dryRun=False): 

2340 """Coalesce vdi onto parent""" 

2341 Util.log("Coalescing %s -> %s" % (vdi, vdi.parent)) 

2342 if dryRun: 2342 ↛ 2343line 2342 didn't jump to line 2343, because the condition on line 2342 was never true

2343 return 

2344 

2345 try: 

2346 self._coalesce(vdi) 

2347 except util.SMException as e: 

2348 if isinstance(e, AbortException): 2348 ↛ 2349line 2348 didn't jump to line 2349, because the condition on line 2348 was never true

2349 self.cleanup() 

2350 raise 

2351 else: 

2352 self._failedCoalesceTargets.append(vdi) 

2353 Util.logException("coalesce") 

2354 Util.log("Coalesce failed, skipping") 

2355 self.cleanup() 

2356 

2357 def coalesceLeaf(self, vdi, dryRun=False): 

2358 """Leaf-coalesce vdi onto parent""" 

2359 Util.log("Leaf-coalescing %s -> %s" % (vdi, vdi.parent)) 

2360 if dryRun: 

2361 return 

2362 

2363 try: 

2364 uuid = vdi.uuid 

2365 try: 

2366 # "vdi" object will no longer be valid after this call 

2367 if vdi.cowutil.isCoalesceableOnRemote(): 

2368 Util.log("We will live coalesce leaf: {uuid}".format(uuid=vdi.uuid)) 

2369 self._liveLeafCoalesce(vdi, coalesce_on_remote=True) 

2370 else: 

2371 Util.log("We can't live coalesce leaf: {uuid}".format(uuid=vdi.uuid)) 

2372 self._coalesceLeaf(vdi) 

2373 finally: 

2374 vdi = self.getVDI(uuid) 

2375 if vdi: 

2376 vdi.delConfig(vdi.DB_LEAFCLSC) 

2377 except AbortException: 

2378 self.cleanup() 

2379 raise 

2380 except (util.SMException, XenAPI.Failure) as e: 

2381 self._failedCoalesceTargets.append(vdi) 

2382 Util.logException("leaf-coalesce") 

2383 Util.log("Leaf-coalesce failed on %s, skipping" % vdi) 

2384 self.cleanup() 

2385 

2386 def garbageCollect(self, dryRun=False): 

2387 vdiList = self.findGarbage() 

2388 Util.log("Found %d VDIs for deletion:" % len(vdiList)) 

2389 for vdi in vdiList: 

2390 Util.log(" %s" % vdi) 

2391 if not dryRun: 

2392 self.deleteVDIs(vdiList) 

2393 self.cleanupJournals(dryRun) 

2394 

2395 def findGarbage(self): 

2396 vdiList = [] 

2397 for vdi in self.vdiTrees: 

2398 vdiList.extend(vdi.getAllPrunable()) 

2399 return vdiList 

2400 

2401 def deleteVDIs(self, vdiList) -> None: 

2402 for vdi in vdiList: 

2403 if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT): 

2404 raise AbortException("Aborting due to signal") 

2405 Util.log("Deleting unlinked VDI %s" % vdi) 

2406 self.deleteVDI(vdi) 

2407 

2408 def deleteVDI(self, vdi) -> None: 

2409 assert(len(vdi.children) == 0) 

2410 del self.vdis[vdi.uuid] 

2411 if vdi.parent: 2411 ↛ 2413line 2411 didn't jump to line 2413, because the condition on line 2411 was never false

2412 vdi.parent.children.remove(vdi) 

2413 if vdi in self.vdiTrees: 2413 ↛ 2414line 2413 didn't jump to line 2414, because the condition on line 2413 was never true

2414 self.vdiTrees.remove(vdi) 

2415 vdi.delete() 

2416 

2417 def forgetVDI(self, vdiUuid) -> None: 

2418 self.xapi.forgetVDI(self.uuid, vdiUuid) 

2419 

2420 def pauseVDIs(self, vdiList) -> None: 

2421 paused = [] 

2422 failed = False 

2423 for vdi in vdiList: 

2424 try: 

2425 vdi.pause() 

2426 paused.append(vdi) 

2427 except: 

2428 Util.logException("pauseVDIs") 

2429 failed = True 

2430 break 

2431 

2432 if failed: 

2433 self.unpauseVDIs(paused) 

2434 raise util.SMException("Failed to pause VDIs") 

2435 

2436 def unpauseVDIs(self, vdiList): 

2437 failed = False 

2438 for vdi in vdiList: 

2439 try: 

2440 vdi.unpause() 

2441 except: 

2442 Util.log("ERROR: Failed to unpause VDI %s" % vdi) 

2443 failed = True 

2444 if failed: 

2445 raise util.SMException("Failed to unpause VDIs") 

2446 

2447 def getFreeSpace(self) -> int: 

2448 return 0 

2449 

2450 def cleanup(self): 

2451 Util.log("In cleanup") 

2452 return 

2453 

2454 @override 

2455 def __str__(self) -> str: 

2456 if self.name: 

2457 ret = "%s ('%s')" % (self.uuid[0:4], self.name) 

2458 else: 

2459 ret = "%s" % self.uuid 

2460 return ret 

2461 

2462 def lock(self): 

2463 """Acquire the SR lock. Nested acquire()'s are ok. Check for Abort 

2464 signal to avoid deadlocking (trying to acquire the SR lock while the 

2465 lock is held by a process that is trying to abort us)""" 

2466 if not self._srLock: 

2467 return 

2468 

2469 if self._locked == 0: 

2470 abortFlag = IPCFlag(self.uuid) 

2471 for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK): 

2472 if self._srLock.acquireNoblock(): 

2473 self._locked += 1 

2474 return 

2475 if abortFlag.test(FLAG_TYPE_ABORT): 

2476 raise AbortException("Abort requested") 

2477 time.sleep(SR.LOCK_RETRY_INTERVAL) 

2478 raise util.SMException("Unable to acquire the SR lock") 

2479 

2480 self._locked += 1 

2481 

2482 def unlock(self): 

2483 if not self._srLock: 2483 ↛ 2485line 2483 didn't jump to line 2485, because the condition on line 2483 was never false

2484 return 

2485 assert(self._locked > 0) 

2486 self._locked -= 1 

2487 if self._locked == 0: 

2488 self._srLock.release() 

2489 

2490 def needUpdateBlockInfo(self) -> bool: 

2491 for vdi in self.vdis.values(): 

2492 if vdi.scanError or len(vdi.children) == 0: 

2493 continue 

2494 if not vdi.getConfig(vdi.DB_VDI_BLOCKS): 

2495 return True 

2496 return False 

2497 

2498 def updateBlockInfo(self) -> None: 

2499 for vdi in self.vdis.values(): 

2500 if vdi.scanError or len(vdi.children) == 0: 

2501 continue 

2502 if not vdi.getConfig(vdi.DB_VDI_BLOCKS): 

2503 vdi.updateBlockInfo() 

2504 

2505 def cleanupCoalesceJournals(self): 

2506 """Remove stale coalesce VDI indicators""" 

2507 entries = self.journaler.getAll(VDI.JRN_COALESCE) 

2508 for uuid, jval in entries.items(): 

2509 self.journaler.remove(VDI.JRN_COALESCE, uuid) 

2510 

2511 def cleanupJournals(self, dryRun=False): 

2512 """delete journal entries for non-existing VDIs""" 

2513 for t in [LVMVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]: 

2514 entries = self.journaler.getAll(t) 

2515 for uuid, jval in entries.items(): 

2516 if self.getVDI(uuid): 

2517 continue 

2518 if t == SR.JRN_CLONE: 

2519 baseUuid, clonUuid = jval.split("_") 

2520 if self.getVDI(baseUuid): 

2521 continue 

2522 Util.log(" Deleting stale '%s' journal entry for %s " 

2523 "(%s)" % (t, uuid, jval)) 

2524 if not dryRun: 

2525 self.journaler.remove(t, uuid) 

2526 

2527 def cleanupCache(self, maxAge=-1) -> int: 

2528 return 0 

2529 

2530 def hasLeavesAttachedOn(self, vdi: VDI): 

2531 leaves = vdi.getAllLeaves() 

2532 leaves_vdi = [leaf.uuid for leaf in leaves] 

2533 return util.get_hosts_attached_on_with_vdi_uuid(self.xapi.session, leaves_vdi) 

2534 

2535 def _gc_running_file(self, vdi: VDI): 

2536 run_file = "gc_running_{}".format(vdi.uuid) 

2537 return os.path.join(NON_PERSISTENT_DIR, str(self.uuid), run_file) 

2538 

2539 def _create_running_file(self, vdi: VDI): 

2540 with open(self._gc_running_file(vdi), "w") as f: 

2541 f.write("1") 

2542 

2543 def _delete_running_file(self, vdi: VDI): 

2544 os.unlink(self._gc_running_file(vdi)) 

2545 

2546 def _coalesce(self, vdi: VDI): 

2547 list_not_to_relink = None 

2548 if self.journaler.get(vdi.JRN_RELINK, vdi.uuid): 2548 ↛ 2551line 2548 didn't jump to line 2551, because the condition on line 2548 was never true

2549 # this means we had done the actual coalescing already and just 

2550 # need to finish relinking and/or refreshing the children 

2551 Util.log("==> Coalesce apparently already done: skipping") 

2552 

2553 # The parent volume must be active for the parent change to occur. 

2554 # The parent volume may become inactive if the host is rebooted. 

2555 vdi._ensureParentActiveForRelink() 

2556 else: 

2557 # JRN_COALESCE is used to check which VDI is being coalesced in 

2558 # order to decide whether to abort the coalesce. We remove the 

2559 # journal as soon as the COW coalesce step is done, because we 

2560 # don't expect the rest of the process to take long 

2561 

2562 if os.path.exists(self._gc_running_file(vdi)): 2562 ↛ 2563line 2562 didn't jump to line 2563, because the condition on line 2562 was never true

2563 util.SMlog("gc_running already exist for {}. Ignoring...".format(self.uuid)) 

2564 

2565 self._create_running_file(vdi) 

2566 

2567 self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1") 

2568 host_refs = self.hasLeavesAttachedOn(vdi) 

2569 # This check of multiple host_refs was done earlier in `isCoalesceable` but 

2570 # we recheck here to be sure another leaf wasn't activated in the meantime. 

2571 if vdi.cowutil.isCoalesceableOnRemote() and len(host_refs) > 1: 2571 ↛ 2572line 2571 didn't jump to line 2572, because the condition on line 2571 was never true

2572 Util.log("Not coalesceable, chain activated more than once") 

2573 Util.log(f"VDI '{vdi.uuid}' has leaves attached on {host_refs}") 

2574 raise Exception("Not coalesceable, chain activated more than once") 

2575 

2576 try: 

2577 if host_refs and vdi.cowutil.isCoalesceableOnRemote(): 2577 ↛ 2579line 2577 didn't jump to line 2579, because the condition on line 2577 was never true

2578 #Leaf opened on another host, we need to call online coalesce 

2579 Util.log("Remote coalesce for {}".format(vdi.path)) 

2580 

2581 leaf_for_coalesce_uuid, host_ref = next(iter(host_refs.items())) # First host_ref since we should only have one 

2582 leaf_for_coalesce = [leaf for leaf in vdi.getAllLeaves() if leaf.uuid == leaf_for_coalesce_uuid][0] 

2583 

2584 vdi._doCoalesceOnHost(host_ref, leaf_for_coalesce) 

2585 # If we use a host OpaqueRef to do a online coalesce, this vdi will not need to be relinked since it was done by tapdisk 

2586 # If we coalesce up the chain, we shouldn't need to do the relink at all, we only need to do the relink on the children if their direct parent was the one we were coalescing 

2587 for child in vdi.children: 

2588 real_parent_uuid = child.extractUuid(child.getParent()) 

2589 if real_parent_uuid == vdi.parent.uuid: 

2590 child._update_vhd_parent(real_parent_uuid) # We update the sm-config:vhd-parent value for this VDI since it has already been relinked 

2591 list_not_to_relink = [leaf.uuid for leaf in child.getAllLeaves()] 

2592 else: 

2593 Util.log("Offline coalesce for {}".format(vdi.path)) 

2594 vdi._doCoalesce() 

2595 except Exception as e: 

2596 Util.log("EXCEPTION while coalescing: {}".format(e)) 

2597 self._delete_running_file(vdi) 

2598 raise 

2599 

2600 self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid) 

2601 self._delete_running_file(vdi) 

2602 

2603 util.fistpoint.activate("LVHDRT_before_create_relink_journal", self.uuid) 

2604 

2605 # we now need to relink the children: lock the SR to prevent ops 

2606 # like SM.clone from manipulating the VDIs we'll be relinking and 

2607 # rescan the SR first in case the children changed since the last 

2608 # scan 

2609 self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1") 

2610 

2611 self.lock() 

2612 try: 

2613 vdi.parent._tagChildrenForRelink(list_not_to_relink) 

2614 self.scan() 

2615 vdi._relinkSkip() 

2616 finally: 

2617 self.unlock() 

2618 # Reload the children to leave things consistent 

2619 vdi.parent._reloadChildren(vdi) 

2620 self.journaler.remove(vdi.JRN_RELINK, vdi.uuid) 

2621 

2622 self.deleteVDI(vdi) 

2623 

2624 class CoalesceTracker: 

2625 GRACE_ITERATIONS = 2 

2626 MAX_ITERATIONS_NO_PROGRESS = 3 

2627 MAX_ITERATIONS = 20 

2628 MAX_INCREASE_FROM_MINIMUM = 1.2 

2629 HISTORY_STRING = "Iteration: {its} -- Initial size {initSize}" \ 

2630 " --> Final size {finSize}" 

2631 

2632 def __init__(self, sr): 

2633 self.itsNoProgress = 0 

2634 self.its = 0 

2635 self.minSize = float("inf") 

2636 self._history = [] 

2637 self.reason = "" 

2638 self.startSize = None 

2639 self.finishSize = None 

2640 self.sr = sr 

2641 self.grace_remaining = self.GRACE_ITERATIONS 

2642 

2643 @property 

2644 def history(self): 

2645 return [x['msg'] for x in self._history] 

2646 

2647 def moving_average(self): 

2648 """ 

2649 Calculate a three point moving average 

2650 """ 

2651 assert len(self._history) >= 3 

2652 

2653 mv_average = sum([x['finalsize'] for x in self._history]) / len(self._history) 

2654 util.SMlog(f'Calculated moving average as {mv_average}') 

2655 return mv_average 

2656 

2657 def abortCoalesce(self, prevSize, curSize): 

2658 self.its += 1 

2659 self._history.append( 

2660 { 

2661 'finalsize': curSize, 

2662 'msg': self.HISTORY_STRING.format(its=self.its, 

2663 initSize=prevSize, 

2664 finSize=curSize) 

2665 } 

2666 ) 

2667 

2668 self.finishSize = curSize 

2669 

2670 if self.startSize is None: 

2671 self.startSize = prevSize 

2672 

2673 if curSize < self.minSize: 

2674 self.minSize = curSize 

2675 

2676 if prevSize < self.minSize: 

2677 self.minSize = prevSize 

2678 

2679 if self.its < 4: 

2680 # Perform at least three iterations 

2681 return False 

2682 

2683 if prevSize >= curSize or curSize < self.moving_average(): 

2684 # We made progress 

2685 return False 

2686 else: 

2687 self.itsNoProgress += 1 

2688 Util.log("No progress, attempt:" 

2689 " {attempt}".format(attempt=self.itsNoProgress)) 

2690 util.fistpoint.activate("cleanup_tracker_no_progress", self.sr.uuid) 

2691 

2692 if self.its > self.MAX_ITERATIONS: 

2693 max = self.MAX_ITERATIONS 

2694 self.reason = \ 

2695 "Max iterations ({max}) exceeded".format(max=max) 

2696 return True 

2697 

2698 if self.itsNoProgress > self.MAX_ITERATIONS_NO_PROGRESS: 

2699 max = self.MAX_ITERATIONS_NO_PROGRESS 

2700 self.reason = \ 

2701 "No progress made for {max} iterations".format(max=max) 

2702 return True 

2703 

2704 maxSizeFromMin = self.MAX_INCREASE_FROM_MINIMUM * self.minSize 

2705 if curSize > maxSizeFromMin: 

2706 self.grace_remaining -= 1 

2707 if self.grace_remaining == 0: 

2708 self.reason = "Unexpected bump in size," \ 

2709 " compared to minimum achieved" 

2710 

2711 return True 

2712 

2713 return False 

2714 

2715 def printSizes(self): 

2716 Util.log("Starting size was {size}" 

2717 .format(size=self.startSize)) 

2718 Util.log("Final size was {size}" 

2719 .format(size=self.finishSize)) 

2720 Util.log("Minimum size achieved was {size}" 

2721 .format(size=self.minSize)) 

2722 

2723 def printReasoning(self): 

2724 Util.log("Aborted coalesce") 

2725 for hist in self.history: 

2726 Util.log(hist) 

2727 Util.log(self.reason) 

2728 self.printSizes() 

2729 

2730 def printSummary(self): 

2731 if self.its == 0: 

2732 return 

2733 

2734 if self.reason: 2734 ↛ 2735line 2734 didn't jump to line 2735, because the condition on line 2734 was never true

2735 Util.log("Aborted coalesce") 

2736 Util.log(self.reason) 

2737 else: 

2738 Util.log("Coalesce summary") 

2739 

2740 Util.log(f"Performed {self.its} iterations") 

2741 self.printSizes() 

2742 

2743 

2744 def _coalesceLeaf(self, vdi): 

2745 """Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot 

2746 complete due to external changes, namely vdi_delete and vdi_snapshot 

2747 that alter leaf-coalescibility of vdi""" 

2748 tracker = self.CoalesceTracker(self) 

2749 while not vdi.canLiveCoalesce(self.getStorageSpeed()): 

2750 prevSizePhys = vdi.getSizePhys() 

2751 if not self._snapshotCoalesce(vdi): 2751 ↛ 2752line 2751 didn't jump to line 2752, because the condition on line 2751 was never true

2752 return False 

2753 if tracker.abortCoalesce(prevSizePhys, vdi.getSizePhys()): 

2754 tracker.printReasoning() 

2755 raise util.SMException("VDI {uuid} could not be coalesced" 

2756 .format(uuid=vdi.uuid)) 

2757 tracker.printSummary() 

2758 return self._liveLeafCoalesce(vdi) 

2759 

2760 def calcStorageSpeed(self, startTime, endTime, coalescedSize): 

2761 speed = None 

2762 total_time = endTime - startTime 

2763 if total_time > 0: 

2764 speed = float(coalescedSize) / float(total_time) 

2765 return speed 

2766 

2767 def writeSpeedToFile(self, speed): 

2768 content = [] 

2769 speedFile = None 

2770 path = SPEED_LOG_ROOT.format(uuid=self.uuid) 

2771 self.lock() 

2772 try: 

2773 Util.log("Writing to file: {myfile}".format(myfile=path)) 

2774 lines = "" 

2775 if not os.path.isfile(path): 

2776 lines = str(speed) + "\n" 

2777 else: 

2778 speedFile = open(path, "r+") 

2779 content = speedFile.readlines() 

2780 content.append(str(speed) + "\n") 

2781 if len(content) > N_RUNNING_AVERAGE: 

2782 del content[0] 

2783 lines = "".join(content) 

2784 

2785 util.atomicFileWrite(path, VAR_RUN, lines) 

2786 finally: 

2787 if speedFile is not None: 

2788 speedFile.close() 

2789 Util.log("Closing file: {myfile}".format(myfile=path)) 

2790 self.unlock() 

2791 

2792 def recordStorageSpeed(self, startTime, endTime, coalescedSize): 

2793 speed = self.calcStorageSpeed(startTime, endTime, coalescedSize) 

2794 if speed is None: 

2795 return 

2796 

2797 self.writeSpeedToFile(speed) 

2798 

2799 def getStorageSpeed(self): 

2800 speedFile = None 

2801 path = SPEED_LOG_ROOT.format(uuid=self.uuid) 

2802 self.lock() 

2803 try: 

2804 speed = None 

2805 if os.path.isfile(path): 

2806 speedFile = open(path) 

2807 content = speedFile.readlines() 

2808 try: 

2809 content = [float(i) for i in content] 

2810 except ValueError: 

2811 Util.log("Something bad in the speed log:{log}". 

2812 format(log=speedFile.readlines())) 

2813 return speed 

2814 

2815 if len(content): 

2816 speed = sum(content) / float(len(content)) 

2817 if speed <= 0: 2817 ↛ 2819line 2817 didn't jump to line 2819, because the condition on line 2817 was never true

2818 # Defensive, should be impossible. 

2819 Util.log("Bad speed: {speed} calculated for SR: {uuid}". 

2820 format(speed=speed, uuid=self.uuid)) 

2821 speed = None 

2822 else: 

2823 Util.log("Speed file empty for SR: {uuid}". 

2824 format(uuid=self.uuid)) 

2825 else: 

2826 Util.log("Speed log missing for SR: {uuid}". 

2827 format(uuid=self.uuid)) 

2828 return speed 

2829 finally: 

2830 if not (speedFile is None): 

2831 speedFile.close() 

2832 self.unlock() 

2833 

2834 def _snapshotCoalesce(self, vdi): 

2835 # Note that because we are not holding any locks here, concurrent SM 

2836 # operations may change this tree under our feet. In particular, vdi 

2837 # can be deleted, or it can be snapshotted. 

2838 assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED) 

2839 Util.log("Single-snapshotting %s" % vdi) 

2840 util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid) 

2841 try: 

2842 ret = self.xapi.singleSnapshotVDI(vdi) 

2843 Util.log("Single-snapshot returned: %s" % ret) 

2844 except XenAPI.Failure as e: 

2845 if util.isInvalidVDI(e): 

2846 Util.log("The VDI appears to have been concurrently deleted") 

2847 return False 

2848 raise 

2849 self.scanLocked() 

2850 tempSnap = vdi.parent 

2851 if not tempSnap.isCoalesceable(): 

2852 Util.log("The VDI appears to have been concurrently snapshotted") 

2853 return False 

2854 Util.log("Coalescing parent %s" % tempSnap) 

2855 util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid) 

2856 sizePhys = vdi.getSizePhys() 

2857 self._coalesce(tempSnap) 

2858 if not vdi.isLeafCoalesceable(): 

2859 Util.log("The VDI tree appears to have been altered since") 

2860 return False 

2861 return True 

2862 

2863 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool: 

2864 util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid) 

2865 self.lock() 

2866 try: 

2867 self.scan() 

2868 if not self.getVDI(vdi.uuid): 

2869 Util.log("The VDI appears to have been deleted meanwhile") 

2870 return False 

2871 if not vdi.isLeafCoalesceable(): 

2872 Util.log("The VDI is no longer leaf-coalesceable") 

2873 return False 

2874 

2875 uuid = vdi.uuid 

2876 if not coalesce_on_remote: 

2877 vdi.pause(failfast=True) 

2878 try: 

2879 try: 

2880 self._create_running_file(vdi) 

2881 # "vdi" object will no longer be valid after this call 

2882 self._doCoalesceLeaf(vdi, coalesce_on_remote) 

2883 except: 

2884 Util.logException("_doCoalesceLeaf") 

2885 self._handleInterruptedCoalesceLeaf() 

2886 raise 

2887 finally: 

2888 vdi = self.getVDI(uuid) 

2889 if vdi: 

2890 vdi.ensureUnpaused() 

2891 self._delete_running_file(vdi) 

2892 vdiOld = self.getVDI(self.TMP_RENAME_PREFIX + uuid) 

2893 if vdiOld: 

2894 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid) 

2895 self.deleteVDI(vdiOld) 

2896 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid) 

2897 finally: 

2898 self.cleanup() 

2899 self.unlock() 

2900 self.logFilter.logState() 

2901 return True 

2902 

2903 def _doCoalesceLeaf(self, vdi: VDI, coalesce_on_remote: bool): 

2904 """Actual coalescing of a leaf VDI onto parent. Must be called in an 

2905 offline/atomic context""" 

2906 self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid) 

2907 self._prepareCoalesceLeaf(vdi) 

2908 vdi.parent._setHidden(False) 

2909 vdi.parent._increaseSizeVirt(vdi.sizeVirt, False) 

2910 host_refs = self.hasLeavesAttachedOn(vdi) if coalesce_on_remote else None 

2911 if host_refs: 2911 ↛ 2912line 2911 didn't jump to line 2912, because the condition on line 2911 was never true

2912 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid) 

2913 _, host_ref = next(iter(host_refs.items())) 

2914 vdi._coalesceCowImageOnHost(host_ref, vdi) # vdi is the leaf for the online coalesce 

2915 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid) 

2916 vdi.pause(failfast=True) 

2917 # We make a pause here after the online coalesce but before the rename so we can refresh the chain for tapdisk.  

2918 # It's also needed to be paused for the rename on slaves with LVMSR. 

2919 # We let the caller `_liveLeafCoalesce` do the unpause with the call to `vdi.ensureUnpaused()` 

2920 else: 

2921 vdi.validate(True) 

2922 vdi.parent.validate(True) 

2923 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid) 

2924 timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT 

2925 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE: 2925 ↛ 2926line 2925 didn't jump to line 2926, because the condition on line 2925 was never true

2926 Util.log("Leaf-coalesce forced, will not use timeout") 

2927 timeout = 0 

2928 vdi._coalesceCowImage(timeout) 

2929 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid) 

2930 vdi.parent.validate(True) 

2931 #vdi._verifyContents(timeout / 2) 

2932 

2933 # rename 

2934 vdiUuid = vdi.uuid 

2935 oldName = vdi.fileName 

2936 origParentUuid = vdi.parent.uuid 

2937 vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid) 

2938 util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid) 

2939 vdi.parent.rename(vdiUuid) 

2940 util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid) 

2941 self._updateSlavesOnRename(vdi.parent, oldName, origParentUuid) 

2942 

2943 # Note that "vdi.parent" is now the single remaining leaf and "vdi" is 

2944 # garbage 

2945 

2946 # update the VDI record 

2947 if vdi.parent.vdi_type == VdiType.RAW: 2947 ↛ 2948line 2947 didn't jump to line 2948, because the condition on line 2947 was never true

2948 vdi.parent.setConfig(VDI.DB_VDI_TYPE, VdiType.RAW) 

2949 vdi.parent.delConfig(VDI.DB_VDI_BLOCKS) 

2950 util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid) 

2951 

2952 self._updateNode(vdi) 

2953 

2954 # delete the obsolete leaf & inflate the parent (in that order, to 

2955 # minimize free space requirements) 

2956 parent = vdi.parent 

2957 vdi._setHidden(True) 

2958 vdi.parent.children = [] 

2959 vdi.parent = None 

2960 

2961 if parent.parent is None: 

2962 parent.delConfig(VDI.DB_VDI_PARENT) 

2963 

2964 extraSpace = self._calcExtraSpaceNeeded(vdi, parent) 

2965 freeSpace = self.getFreeSpace() 

2966 if freeSpace < extraSpace: 2966 ↛ 2969line 2966 didn't jump to line 2969, because the condition on line 2966 was never true

2967 # don't delete unless we need the space: deletion is time-consuming 

2968 # because it requires contacting the slaves, and we're paused here 

2969 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid) 

2970 self.deleteVDI(vdi) 

2971 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid) 

2972 

2973 util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid) 

2974 self.journaler.remove(VDI.JRN_LEAF, vdiUuid) 

2975 

2976 self.forgetVDI(origParentUuid) 

2977 self._finishCoalesceLeaf(parent) 

2978 self._updateSlavesOnResize(parent) 

2979 

2980 def _calcExtraSpaceNeeded(self, child, parent) -> int: 

2981 assert(VdiType.isCowImage(parent.vdi_type)) 

2982 extra = child.getSizePhys() - parent.getSizePhys() 

2983 if extra < 0: 2983 ↛ 2984line 2983 didn't jump to line 2984, because the condition on line 2983 was never true

2984 extra = 0 

2985 return extra 

2986 

2987 def _prepareCoalesceLeaf(self, vdi) -> None: 

2988 pass 

2989 

2990 def _updateNode(self, vdi) -> None: 

2991 pass 

2992 

2993 def _finishCoalesceLeaf(self, parent) -> None: 

2994 pass 

2995 

2996 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None: 

2997 pass 

2998 

2999 def _updateSlavesOnRename(self, vdi, oldName, origParentUuid) -> None: 

3000 pass 

3001 

3002 def _updateSlavesOnResize(self, vdi) -> None: 

3003 pass 

3004 

3005 def _removeStaleVDIs(self, uuidsPresent) -> None: 

3006 for uuid in list(self.vdis.keys()): 

3007 if not uuid in uuidsPresent: 

3008 Util.log("VDI %s disappeared since last scan" % \ 

3009 self.vdis[uuid]) 

3010 del self.vdis[uuid] 

3011 

3012 def _handleInterruptedCoalesceLeaf(self) -> None: 

3013 """An interrupted leaf-coalesce operation may leave the COW tree in an 

3014 inconsistent state. If the old-leaf VDI is still present, we revert the 

3015 operation (in case the original error is persistent); otherwise we must 

3016 finish the operation""" 

3017 pass 

3018 

3019 def _buildTree(self, force): 

3020 self.vdiTrees = [] 

3021 for vdi in self.vdis.values(): 

3022 if vdi.parentUuid: 

3023 parent = self.getVDI(vdi.parentUuid) 

3024 if not parent: 

3025 if vdi.uuid.startswith(self.TMP_RENAME_PREFIX): 

3026 self.vdiTrees.append(vdi) 

3027 continue 

3028 if force: 

3029 Util.log("ERROR: Parent VDI %s not found! (for %s)" % \ 

3030 (vdi.parentUuid, vdi.uuid)) 

3031 self.vdiTrees.append(vdi) 

3032 continue 

3033 else: 

3034 raise util.SMException("Parent VDI %s of %s not " \ 

3035 "found" % (vdi.parentUuid, vdi.uuid)) 

3036 vdi.parent = parent 

3037 parent.children.append(vdi) 

3038 else: 

3039 self.vdiTrees.append(vdi) 

3040 

3041 

3042class FileSR(SR): 

3043 TYPE = SR.TYPE_FILE 

3044 CACHE_FILE_EXT = ".vhdcache" 

3045 # cache cleanup actions 

3046 CACHE_ACTION_KEEP = 0 

3047 CACHE_ACTION_REMOVE = 1 

3048 CACHE_ACTION_REMOVE_IF_INACTIVE = 2 

3049 

3050 def __init__(self, uuid, xapi, createLock, force): 

3051 SR.__init__(self, uuid, xapi, createLock, force) 

3052 self.path = "/var/run/sr-mount/%s" % self.uuid 

3053 self.journaler = fjournaler.Journaler(self.path) 

3054 

3055 @override 

3056 def scan(self, force=False) -> None: 

3057 if not util.pathexists(self.path): 

3058 raise util.SMException("directory %s not found!" % self.uuid) 

3059 

3060 uuidsPresent: List[str] = [] 

3061 

3062 for vdi_type in VDI_COW_TYPES: 

3063 scan_result = self._scan(vdi_type, force) 

3064 for uuid, image_info in scan_result.items(): 

3065 vdi = self.getVDI(uuid) 

3066 if not vdi: 

3067 self.logFilter.logNewVDI(uuid) 

3068 vdi = FileVDI(self, uuid, vdi_type) 

3069 self.vdis[uuid] = vdi 

3070 vdi.load(image_info) 

3071 uuidsPresent.extend(scan_result.keys()) 

3072 

3073 rawList = [x for x in os.listdir(self.path) if x.endswith(VdiTypeExtension.RAW)] 

3074 for rawName in rawList: 

3075 uuid = FileVDI.extractUuid(rawName) 

3076 uuidsPresent.append(uuid) 

3077 vdi = self.getVDI(uuid) 

3078 if not vdi: 

3079 self.logFilter.logNewVDI(uuid) 

3080 vdi = FileVDI(self, uuid, VdiType.RAW) 

3081 self.vdis[uuid] = vdi 

3082 self._removeStaleVDIs(uuidsPresent) 

3083 self._buildTree(force) 

3084 self.logFilter.logState() 

3085 self._handleInterruptedCoalesceLeaf() 

3086 

3087 @override 

3088 def getFreeSpace(self) -> int: 

3089 return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path) 

3090 

3091 @override 

3092 def deleteVDIs(self, vdiList) -> None: 

3093 rootDeleted = False 

3094 for vdi in vdiList: 

3095 if not vdi.parent: 

3096 rootDeleted = True 

3097 break 

3098 SR.deleteVDIs(self, vdiList) 

3099 if self.xapi.srRecord["type"] == "nfs" and rootDeleted: 

3100 self.xapi.markCacheSRsDirty() 

3101 

3102 @override 

3103 def cleanupCache(self, maxAge=-1) -> int: 

3104 """Clean up IntelliCache cache files. Caches for leaf nodes are 

3105 removed when the leaf node no longer exists or its allow-caching 

3106 attribute is not set. Caches for parent nodes are removed when the 

3107 parent node no longer exists or it hasn't been used in more than 

3108 <maxAge> hours. 

3109 Return number of caches removed. 

3110 """ 

3111 numRemoved = 0 

3112 cacheFiles = [x for x in os.listdir(self.path) if self._isCacheFileName(x)] 

3113 Util.log("Found %d cache files" % len(cacheFiles)) 

3114 cutoff = datetime.datetime.now() - datetime.timedelta(hours=maxAge) 

3115 for cacheFile in cacheFiles: 

3116 uuid = cacheFile[:-len(self.CACHE_FILE_EXT)] 

3117 action = self.CACHE_ACTION_KEEP 

3118 rec = self.xapi.getRecordVDI(uuid) 

3119 if not rec: 

3120 Util.log("Cache %s: VDI doesn't exist" % uuid) 

3121 action = self.CACHE_ACTION_REMOVE 

3122 elif rec["managed"] and not rec["allow_caching"]: 

3123 Util.log("Cache %s: caching disabled" % uuid) 

3124 action = self.CACHE_ACTION_REMOVE 

3125 elif not rec["managed"] and maxAge >= 0: 

3126 lastAccess = datetime.datetime.fromtimestamp( \ 

3127 os.path.getatime(os.path.join(self.path, cacheFile))) 

3128 if lastAccess < cutoff: 

3129 Util.log("Cache %s: older than %d hrs" % (uuid, maxAge)) 

3130 action = self.CACHE_ACTION_REMOVE_IF_INACTIVE 

3131 

3132 if action == self.CACHE_ACTION_KEEP: 

3133 Util.log("Keeping cache %s" % uuid) 

3134 continue 

3135 

3136 lockId = uuid 

3137 parentUuid = None 

3138 if rec and rec["managed"]: 

3139 parentUuid = rec["sm_config"].get("vhd-parent") 

3140 if parentUuid: 

3141 lockId = parentUuid 

3142 

3143 cacheLock = lock.Lock(blktap2.VDI.LOCK_CACHE_SETUP, lockId) 

3144 cacheLock.acquire() 

3145 try: 

3146 if self._cleanupCache(uuid, action): 

3147 numRemoved += 1 

3148 finally: 

3149 cacheLock.release() 

3150 return numRemoved 

3151 

3152 def _cleanupCache(self, uuid, action): 

3153 assert(action != self.CACHE_ACTION_KEEP) 

3154 rec = self.xapi.getRecordVDI(uuid) 

3155 if rec and rec["allow_caching"]: 

3156 Util.log("Cache %s appears to have become valid" % uuid) 

3157 return False 

3158 

3159 fullPath = os.path.join(self.path, uuid + self.CACHE_FILE_EXT) 

3160 tapdisk = blktap2.Tapdisk.find_by_path(fullPath) 

3161 if tapdisk: 

3162 if action == self.CACHE_ACTION_REMOVE_IF_INACTIVE: 

3163 Util.log("Cache %s still in use" % uuid) 

3164 return False 

3165 Util.log("Shutting down tapdisk for %s" % fullPath) 

3166 tapdisk.shutdown() 

3167 

3168 Util.log("Deleting file %s" % fullPath) 

3169 os.unlink(fullPath) 

3170 return True 

3171 

3172 def _isCacheFileName(self, name): 

3173 return (len(name) == Util.UUID_LEN + len(self.CACHE_FILE_EXT)) and \ 

3174 name.endswith(self.CACHE_FILE_EXT) 

3175 

3176 def _scan(self, vdi_type, force): 

3177 for i in range(SR.SCAN_RETRY_ATTEMPTS): 

3178 error = False 

3179 pattern = os.path.join(self.path, "*%s" % VDI_TYPE_TO_EXTENSION[vdi_type]) 

3180 scan_result = getCowUtil(vdi_type).getAllInfoFromVG(pattern, FileVDI.extractUuid) 

3181 for uuid, vdiInfo in scan_result.items(): 

3182 if vdiInfo.error: 

3183 error = True 

3184 break 

3185 if not error: 

3186 return scan_result 

3187 Util.log("Scan error on attempt %d" % i) 

3188 if force: 

3189 return scan_result 

3190 raise util.SMException("Scan error") 

3191 

3192 @override 

3193 def deleteVDI(self, vdi) -> None: 

3194 self._checkSlaves(vdi) 

3195 SR.deleteVDI(self, vdi) 

3196 

3197 def _checkSlaves(self, vdi): 

3198 onlineHosts = self.xapi.getOnlineHosts() 

3199 abortFlag = IPCFlag(self.uuid) 

3200 for pbdRecord in self.xapi.getAttachedPBDs(): 

3201 hostRef = pbdRecord["host"] 

3202 if hostRef == self.xapi._hostRef: 

3203 continue 

3204 if abortFlag.test(FLAG_TYPE_ABORT): 

3205 raise AbortException("Aborting due to signal") 

3206 try: 

3207 self._checkSlave(hostRef, vdi) 

3208 except XenAPI.Failure: 

3209 if hostRef in onlineHosts: 

3210 raise 

3211 

3212 def _checkSlave(self, hostRef, vdi): 

3213 call = (hostRef, "nfs-on-slave", "check", {'path': vdi.path}) 

3214 Util.log("Checking with slave: %s" % repr(call)) 

3215 _host = self.xapi.session.xenapi.host 

3216 text = _host.call_plugin( * call) 

3217 

3218 @override 

3219 def _handleInterruptedCoalesceLeaf(self) -> None: 

3220 entries = self.journaler.getAll(VDI.JRN_LEAF) 

3221 extensions = tuple(VDI_TYPE_TO_EXTENSION[vdi_type] for vdi_type in VDI_COW_TYPES) 

3222 srRoot = Path(self.path) 

3223 

3224 for uuid, parentUuid in entries.items(): 

3225 hasCowPath = False 

3226 

3227 for extension in extensions: 

3228 hasCowPath = ( 

3229 (srRoot / (self.TMP_RENAME_PREFIX + uuid + extension)).exists() or 

3230 (srRoot / (parentUuid + extension)).exists() 

3231 ) 

3232 

3233 if hasCowPath: 

3234 break 

3235 

3236 if hasCowPath or (srRoot / (parentUuid + VdiTypeExtension.RAW)).exists(): 

3237 self._undoInterruptedCoalesceLeaf(uuid, parentUuid) 

3238 else: 

3239 self._finishInterruptedCoalesceLeaf(uuid, parentUuid) 

3240 

3241 self.journaler.remove(VDI.JRN_LEAF, uuid) 

3242 

3243 vdi = self.getVDI(uuid) 

3244 if vdi: 

3245 vdi.ensureUnpaused() 

3246 

3247 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3248 Util.log("*** UNDO LEAF-COALESCE") 

3249 parent = self.getVDI(parentUuid) 

3250 if not parent: 

3251 parent = self.getVDI(childUuid) 

3252 if not parent: 

3253 raise util.SMException("Neither %s nor %s found" % \ 

3254 (parentUuid, childUuid)) 

3255 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid)) 

3256 parent.rename(parentUuid) 

3257 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid) 

3258 

3259 child = self.getVDI(childUuid) 

3260 if not child: 

3261 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid) 

3262 if not child: 

3263 raise util.SMException("Neither %s nor %s found" % \ 

3264 (childUuid, self.TMP_RENAME_PREFIX + childUuid)) 

3265 Util.log("Renaming child back to %s" % childUuid) 

3266 child.rename(childUuid) 

3267 Util.log("Updating the VDI record") 

3268 child.setConfig(VDI.DB_VDI_PARENT, parentUuid) 

3269 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type) 

3270 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid) 

3271 

3272 if child.isHidden(): 

3273 child._setHidden(False) 

3274 if not parent.isHidden(): 

3275 parent._setHidden(True) 

3276 self._updateSlavesOnUndoLeafCoalesce(parent, child) 

3277 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid) 

3278 Util.log("*** leaf-coalesce undo successful") 

3279 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"): 

3280 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED) 

3281 

3282 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3283 Util.log("*** FINISH LEAF-COALESCE") 

3284 vdi = self.getVDI(childUuid) 

3285 if not vdi: 

3286 Util.log(f"_finishInterruptedCoalesceLeaf, vdi {childUuid} not found, aborting") 

3287 raise util.SMException("VDI %s not found" % childUuid) 

3288 try: 

3289 self.forgetVDI(parentUuid) 

3290 except XenAPI.Failure: 

3291 Util.logException('_finishInterruptedCoalesceLeaf') 

3292 pass 

3293 self._updateSlavesOnResize(vdi) 

3294 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid) 

3295 Util.log("*** finished leaf-coalesce successfully") 

3296 

3297 

3298class LVMSR(SR): 

3299 TYPE = SR.TYPE_LVHD 

3300 SUBTYPES = ["lvhdoiscsi", "lvhdohba"] 

3301 

3302 def __init__(self, uuid, xapi, createLock, force): 

3303 SR.__init__(self, uuid, xapi, createLock, force) 

3304 self.vgName = "%s%s" % (VG_PREFIX, self.uuid) 

3305 self.path = os.path.join(VG_LOCATION, self.vgName) 

3306 

3307 sr_ref = self.xapi.session.xenapi.SR.get_by_uuid(self.uuid) 

3308 other_conf = self.xapi.session.xenapi.SR.get_other_config(sr_ref) 

3309 lvm_conf = other_conf.get('lvm-conf') if other_conf else None 

3310 self.lvmCache = lvmcache.LVMCache(self.vgName, lvm_conf) 

3311 

3312 self.lvActivator = LVActivator(self.uuid, self.lvmCache) 

3313 self.journaler = journaler.Journaler(self.lvmCache) 

3314 

3315 @override 

3316 def deleteVDI(self, vdi) -> None: 

3317 if self.lvActivator.get(vdi.uuid, False): 

3318 self.lvActivator.deactivate(vdi.uuid, False) 

3319 self._checkSlaves(vdi) 

3320 SR.deleteVDI(self, vdi) 

3321 

3322 @override 

3323 def forgetVDI(self, vdiUuid) -> None: 

3324 SR.forgetVDI(self, vdiUuid) 

3325 mdpath = os.path.join(self.path, lvutil.MDVOLUME_NAME) 

3326 LVMMetadataHandler(mdpath).deleteVdiFromMetadata(vdiUuid) 

3327 

3328 @override 

3329 def getFreeSpace(self) -> int: 

3330 stats = lvutil._getVGstats(self.vgName) 

3331 return stats['physical_size'] - stats['physical_utilisation'] 

3332 

3333 @override 

3334 def cleanup(self): 

3335 if not self.lvActivator.deactivateAll(): 

3336 Util.log("ERROR deactivating LVs while cleaning up") 

3337 

3338 @override 

3339 def needUpdateBlockInfo(self) -> bool: 

3340 for vdi in self.vdis.values(): 

3341 if vdi.scanError or not VdiType.isCowImage(vdi.vdi_type) or len(vdi.children) == 0: 

3342 continue 

3343 if not vdi.getConfig(vdi.DB_VDI_BLOCKS): 

3344 return True 

3345 return False 

3346 

3347 @override 

3348 def updateBlockInfo(self) -> None: 

3349 numUpdated = 0 

3350 for vdi in self.vdis.values(): 

3351 if vdi.scanError or not VdiType.isCowImage(vdi.vdi_type) or len(vdi.children) == 0: 

3352 continue 

3353 if not vdi.getConfig(vdi.DB_VDI_BLOCKS): 

3354 vdi.updateBlockInfo() 

3355 numUpdated += 1 

3356 if numUpdated: 

3357 # deactivate the LVs back sooner rather than later. If we don't 

3358 # now, by the time this thread gets to deactivations, another one 

3359 # might have leaf-coalesced a node and deleted it, making the child 

3360 # inherit the refcount value and preventing the correct decrement 

3361 self.cleanup() 

3362 

3363 @override 

3364 def scan(self, force=False) -> None: 

3365 vdis = self._scan(force) 

3366 for uuid, vdiInfo in vdis.items(): 

3367 vdi = self.getVDI(uuid) 

3368 if not vdi: 

3369 self.logFilter.logNewVDI(uuid) 

3370 vdi = LVMVDI(self, uuid, vdiInfo.vdiType) 

3371 self.vdis[uuid] = vdi 

3372 vdi.load(vdiInfo) 

3373 self._removeStaleVDIs(vdis.keys()) 

3374 self._buildTree(force) 

3375 self.logFilter.logState() 

3376 self._handleInterruptedCoalesceLeaf() 

3377 

3378 def _scan(self, force): 

3379 for i in range(SR.SCAN_RETRY_ATTEMPTS): 

3380 error = False 

3381 self.lvmCache.refresh() 

3382 vdis = LvmCowUtil.getVDIInfo(self.lvmCache) 

3383 for uuid, vdiInfo in vdis.items(): 

3384 if vdiInfo.scanError: 

3385 error = True 

3386 break 

3387 if not error: 

3388 return vdis 

3389 Util.log("Scan error, retrying (%d)" % i) 

3390 if force: 

3391 return vdis 

3392 raise util.SMException("Scan error") 

3393 

3394 @override 

3395 def _removeStaleVDIs(self, uuidsPresent) -> None: 

3396 for uuid in list(self.vdis.keys()): 

3397 if not uuid in uuidsPresent: 

3398 Util.log("VDI %s disappeared since last scan" % \ 

3399 self.vdis[uuid]) 

3400 del self.vdis[uuid] 

3401 if self.lvActivator.get(uuid, False): 

3402 self.lvActivator.remove(uuid, False) 

3403 

3404 @override 

3405 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool: 

3406 """If the parent is raw and the child was resized (virt. size), then 

3407 we'll need to resize the parent, which can take a while due to zeroing 

3408 out of the extended portion of the LV. Do it before pausing the child 

3409 to avoid a protracted downtime""" 

3410 if not VdiType.isCowImage(vdi.parent.vdi_type) and vdi.sizeVirt > vdi.parent.sizeVirt: 

3411 self.lvmCache.setReadonly(vdi.parent.fileName, False) 

3412 vdi.parent._increaseSizeVirt(vdi.sizeVirt) 

3413 

3414 return SR._liveLeafCoalesce(self, vdi, coalesce_on_remote) 

3415 

3416 @override 

3417 def _prepareCoalesceLeaf(self, vdi) -> None: 

3418 vdi._activateChain() 

3419 self.lvmCache.setReadonly(vdi.parent.fileName, False) 

3420 vdi.deflate() 

3421 vdi.inflateParentForCoalesce() 

3422 

3423 @override 

3424 def _updateNode(self, vdi) -> None: 

3425 # fix the refcounts: the remaining node should inherit the binary 

3426 # refcount from the leaf (because if it was online, it should remain 

3427 # refcounted as such), but the normal refcount from the parent (because 

3428 # this node is really the parent node) - minus 1 if it is online (since 

3429 # non-leaf nodes increment their normal counts when they are online and 

3430 # we are now a leaf, storing that 1 in the binary refcount). 

3431 ns = NS_PREFIX_LVM + self.uuid 

3432 cCnt, cBcnt = RefCounter.check(vdi.uuid, ns) 

3433 pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns) 

3434 pCnt = pCnt - cBcnt 

3435 assert(pCnt >= 0) 

3436 RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns) 

3437 

3438 @override 

3439 def _finishCoalesceLeaf(self, parent) -> None: 

3440 if not parent.isSnapshot() or parent.isAttachedRW(): 

3441 parent.inflateFully() 

3442 else: 

3443 parent.deflate() 

3444 

3445 @override 

3446 def _calcExtraSpaceNeeded(self, child, parent) -> int: 

3447 return parent.lvmcowutil.calcVolumeSize(parent.sizeVirt) - parent.sizeLV 

3448 

3449 @override 

3450 def _handleInterruptedCoalesceLeaf(self) -> None: 

3451 entries = self.journaler.getAll(VDI.JRN_LEAF) 

3452 for uuid, parentUuid in entries.items(): 

3453 undo = False 

3454 for prefix in LV_PREFIX.values(): 

3455 parentLV = prefix + parentUuid 

3456 undo = self.lvmCache.checkLV(parentLV) 

3457 if undo: 

3458 break 

3459 

3460 if not undo: 

3461 for prefix in LV_PREFIX.values(): 

3462 tmpChildLV = prefix + uuid 

3463 undo = self.lvmCache.checkLV(tmpChildLV) 

3464 if undo: 

3465 break 

3466 

3467 if undo: 

3468 self._undoInterruptedCoalesceLeaf(uuid, parentUuid) 

3469 else: 

3470 self._finishInterruptedCoalesceLeaf(uuid, parentUuid) 

3471 self.journaler.remove(VDI.JRN_LEAF, uuid) 

3472 vdi = self.getVDI(uuid) 

3473 if vdi: 

3474 vdi.ensureUnpaused() 

3475 

3476 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3477 Util.log("*** UNDO LEAF-COALESCE") 

3478 parent = self.getVDI(parentUuid) 

3479 if not parent: 

3480 parent = self.getVDI(childUuid) 

3481 if not parent: 

3482 raise util.SMException("Neither %s nor %s found" % \ 

3483 (parentUuid, childUuid)) 

3484 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid)) 

3485 parent.rename(parentUuid) 

3486 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid) 

3487 

3488 child = self.getVDI(childUuid) 

3489 if not child: 

3490 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid) 

3491 if not child: 

3492 raise util.SMException("Neither %s nor %s found" % \ 

3493 (childUuid, self.TMP_RENAME_PREFIX + childUuid)) 

3494 Util.log("Renaming child back to %s" % childUuid) 

3495 child.rename(childUuid) 

3496 Util.log("Updating the VDI record") 

3497 child.setConfig(VDI.DB_VDI_PARENT, parentUuid) 

3498 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type) 

3499 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid) 

3500 

3501 # refcount (best effort - assume that it had succeeded if the 

3502 # second rename succeeded; if not, this adjustment will be wrong, 

3503 # leading to a non-deactivation of the LV) 

3504 ns = NS_PREFIX_LVM + self.uuid 

3505 cCnt, cBcnt = RefCounter.check(child.uuid, ns) 

3506 pCnt, pBcnt = RefCounter.check(parent.uuid, ns) 

3507 pCnt = pCnt + cBcnt 

3508 RefCounter.set(parent.uuid, pCnt, 0, ns) 

3509 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid) 

3510 

3511 parent.deflate() 

3512 child.inflateFully() 

3513 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid) 

3514 if child.isHidden(): 

3515 child._setHidden(False) 

3516 if not parent.isHidden(): 

3517 parent._setHidden(True) 

3518 if not parent.lvReadonly: 

3519 self.lvmCache.setReadonly(parent.fileName, True) 

3520 child.ensurePaused() 

3521 self._updateSlavesOnUndoLeafCoalesce(parent, child) 

3522 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid) 

3523 Util.log("*** leaf-coalesce undo successful") 

3524 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"): 

3525 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED) 

3526 

3527 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3528 Util.log("*** FINISH LEAF-COALESCE") 

3529 vdi = self.getVDI(childUuid) 

3530 if not vdi: 

3531 raise util.SMException("VDI %s not found" % childUuid) 

3532 vdi.inflateFully() 

3533 util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid) 

3534 try: 

3535 self.forgetVDI(parentUuid) 

3536 except XenAPI.Failure: 

3537 pass 

3538 self._updateSlavesOnResize(vdi) 

3539 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid) 

3540 Util.log("*** finished leaf-coalesce successfully") 

3541 

3542 def _checkSlaves(self, vdi): 

3543 """Confirm with all slaves in the pool that 'vdi' is not in use. We 

3544 try to check all slaves, including those that the Agent believes are 

3545 offline, but ignore failures for offline hosts. This is to avoid cases 

3546 where the Agent thinks a host is offline but the host is up.""" 

3547 args = {"vgName": self.vgName, 

3548 "action1": "deactivateNoRefcount", 

3549 "lvName1": vdi.fileName, 

3550 "action2": "cleanupLockAndRefcount", 

3551 "uuid2": vdi.uuid, 

3552 "ns2": NS_PREFIX_LVM + self.uuid} 

3553 onlineHosts = self.xapi.getOnlineHosts() 

3554 abortFlag = IPCFlag(self.uuid) 

3555 for pbdRecord in self.xapi.getAttachedPBDs(): 

3556 hostRef = pbdRecord["host"] 

3557 if hostRef == self.xapi._hostRef: 

3558 continue 

3559 if abortFlag.test(FLAG_TYPE_ABORT): 

3560 raise AbortException("Aborting due to signal") 

3561 Util.log("Checking with slave %s (path %s)" % ( 

3562 self.xapi.getRecordHost(hostRef)['hostname'], vdi.path)) 

3563 try: 

3564 self.xapi.ensureInactive(hostRef, args) 

3565 except XenAPI.Failure: 

3566 if hostRef in onlineHosts: 

3567 raise 

3568 

3569 @override 

3570 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None: 

3571 slaves = util.get_slaves_attached_on(self.xapi.session, [child.uuid]) 

3572 if not slaves: 

3573 Util.log("Update-on-leaf-undo: VDI %s not attached on any slave" % \ 

3574 child) 

3575 return 

3576 

3577 tmpName = LV_PREFIX[child.vdi_type] + self.TMP_RENAME_PREFIX + child.uuid 

3578 args = {"vgName": self.vgName, 

3579 "action1": "deactivateNoRefcount", 

3580 "lvName1": tmpName, 

3581 "action2": "deactivateNoRefcount", 

3582 "lvName2": child.fileName, 

3583 "action3": "refresh", 

3584 "lvName3": child.fileName, 

3585 "action4": "refresh", 

3586 "lvName4": parent.fileName} 

3587 for slave in slaves: 

3588 Util.log("Updating %s, %s, %s on slave %s" % \ 

3589 (tmpName, child.fileName, parent.fileName, 

3590 self.xapi.getRecordHost(slave)['hostname'])) 

3591 text = self.xapi.session.xenapi.host.call_plugin( \ 

3592 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args) 

3593 Util.log("call-plugin returned: '%s'" % text) 

3594 

3595 @override 

3596 def _updateSlavesOnRename(self, vdi, oldNameLV, origParentUuid) -> None: 

3597 slaves = util.get_slaves_attached_on(self.xapi.session, [vdi.uuid]) 

3598 if not slaves: 

3599 Util.log("Update-on-rename: VDI %s not attached on any slave" % vdi) 

3600 return 

3601 

3602 args = {"vgName": self.vgName, 

3603 "action1": "deactivateNoRefcount", 

3604 "lvName1": oldNameLV, 

3605 "action2": "refresh", 

3606 "lvName2": vdi.fileName, 

3607 "action3": "cleanupLockAndRefcount", 

3608 "uuid3": origParentUuid, 

3609 "ns3": NS_PREFIX_LVM + self.uuid} 

3610 for slave in slaves: 

3611 Util.log("Updating %s to %s on slave %s" % \ 

3612 (oldNameLV, vdi.fileName, 

3613 self.xapi.getRecordHost(slave)['hostname'])) 

3614 text = self.xapi.session.xenapi.host.call_plugin( \ 

3615 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args) 

3616 Util.log("call-plugin returned: '%s'" % text) 

3617 

3618 @override 

3619 def _updateSlavesOnResize(self, vdi) -> None: 

3620 uuids = [x.uuid for x in vdi.getAllLeaves()] 

3621 slaves = util.get_slaves_attached_on(self.xapi.session, uuids) 

3622 if not slaves: 

3623 util.SMlog("Update-on-resize: %s not attached on any slave" % vdi) 

3624 return 

3625 LvmCowUtil.refreshVolumeOnSlaves(self.xapi.session, self.uuid, self.vgName, 

3626 vdi.fileName, vdi.uuid, slaves) 

3627 

3628 

3629class LinstorSR(SR): 

3630 TYPE = SR.TYPE_LINSTOR 

3631 

3632 def __init__(self, uuid, xapi, createLock, force): 

3633 if not LINSTOR_AVAILABLE: 

3634 raise util.SMException( 

3635 'Can\'t load cleanup LinstorSR: LINSTOR libraries are missing' 

3636 ) 

3637 

3638 SR.__init__(self, uuid, xapi, createLock, force) 

3639 self.path = LinstorVolumeManager.DEV_ROOT_PATH 

3640 

3641 class LinstorProxy: 

3642 def __init__(self, sr: LinstorSR) -> None: 

3643 self.sr = sr 

3644 

3645 def __getattr__(self, attr: str) -> Any: 

3646 assert self.sr, "Cannot use `LinstorProxy` without valid `LinstorVolumeManager` instance" 

3647 return getattr(self.sr._linstor, attr) 

3648 

3649 self._linstor = None 

3650 self._linstor_proxy = LinstorProxy(self) 

3651 self._reloadLinstor(journaler_only=True) 

3652 

3653 @override 

3654 def deleteVDI(self, vdi) -> None: 

3655 self._checkSlaves(vdi) 

3656 SR.deleteVDI(self, vdi) 

3657 

3658 @override 

3659 def getFreeSpace(self) -> int: 

3660 return self._linstor.max_volume_size_allowed 

3661 

3662 @override 

3663 def scan(self, force=False) -> None: 

3664 all_vdi_info = self._scan(force) 

3665 for uuid, vdiInfo in all_vdi_info.items(): 

3666 # When vdiInfo is None, the VDI is RAW. 

3667 vdi = self.getVDI(uuid) 

3668 if not vdi: 

3669 self.logFilter.logNewVDI(uuid) 

3670 vdi = LinstorVDI(self, uuid, vdiInfo.vdiType if vdiInfo else VdiType.RAW) 

3671 self.vdis[uuid] = vdi 

3672 if vdiInfo: 

3673 vdi.load(vdiInfo) 

3674 self._removeStaleVDIs(all_vdi_info.keys()) 

3675 self._buildTree(force) 

3676 self.logFilter.logState() 

3677 self._handleInterruptedCoalesceLeaf() 

3678 

3679 @override 

3680 def pauseVDIs(self, vdiList) -> None: 

3681 self._linstor.ensure_volume_list_is_not_locked( 

3682 vdiList, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT 

3683 ) 

3684 return super(LinstorSR, self).pauseVDIs(vdiList) 

3685 

3686 def _reloadLinstor(self, journaler_only=False): 

3687 session = self.xapi.session 

3688 host_ref = util.get_this_host_ref(session) 

3689 sr_ref = session.xenapi.SR.get_by_uuid(self.uuid) 

3690 

3691 pbd = util.find_my_pbd(session, host_ref, sr_ref) 

3692 if pbd is None: 

3693 raise util.SMException('Failed to find PBD') 

3694 

3695 dconf = session.xenapi.PBD.get_device_config(pbd) 

3696 group_name = dconf['group-name'] 

3697 

3698 controller_uri = get_controller_uri() 

3699 

3700 if not journaler_only: 

3701 self._linstor = LinstorVolumeManager( 

3702 controller_uri, 

3703 group_name, 

3704 repair=True, 

3705 logger=util.SMlog 

3706 ) 

3707 

3708 self.journaler = LinstorJournaler( 

3709 group_name, 

3710 uri=None if self._linstor else controller_uri, 

3711 native_client=self._linstor.native_client if self._linstor else None, 

3712 logger=util.SMlog 

3713 ) 

3714 

3715 def _scan(self, force): 

3716 for i in range(SR.SCAN_RETRY_ATTEMPTS): 

3717 self._reloadLinstor() 

3718 error = False 

3719 try: 

3720 all_vdi_info = self._load_vdi_info() 

3721 for uuid, vdiInfo in all_vdi_info.items(): 

3722 if vdiInfo and vdiInfo.error: 

3723 error = True 

3724 break 

3725 if not error: 

3726 return all_vdi_info 

3727 Util.log('Scan error, retrying ({})'.format(i)) 

3728 except Exception as e: 

3729 Util.log('Scan exception, retrying ({}): {}'.format(i, e)) 

3730 Util.log(traceback.format_exc()) 

3731 

3732 if force: 

3733 return all_vdi_info 

3734 raise util.SMException('Scan error') 

3735 

3736 def _load_vdi_info(self): 

3737 all_volume_info = self._linstor.get_volumes_with_info() 

3738 volumes_metadata = self._linstor.get_volumes_with_metadata() 

3739 

3740 all_vdi_info = {} 

3741 pending_vdis = [] 

3742 

3743 def handle_fail(vdi_uuid, e): 

3744 Util.log(f" [VDI {vdi_uuid}: failed to load VDI info]: {e}") 

3745 info = CowImageInfo(vdi_uuid) 

3746 info.error = 1 

3747 return info 

3748 

3749 for vdi_uuid, volume_info in all_volume_info.items(): 

3750 vdi_type = VdiType.RAW 

3751 try: 

3752 volume_metadata = volumes_metadata[vdi_uuid] 

3753 if not volume_info.name and not list(volume_metadata.items()): 

3754 continue # Ignore it, probably deleted. 

3755 

3756 if vdi_uuid.startswith('DELETED_'): 

3757 # Assume it's really a RAW volume of a failed snap without COW header/footer. 

3758 # We must remove this VDI now without adding it in the VDI list. 

3759 # Otherwise `Relinking` calls and other actions can be launched on it. 

3760 # We don't want that... 

3761 Util.log('Deleting bad VDI {}'.format(vdi_uuid)) 

3762 

3763 self.lock() 

3764 try: 

3765 self._linstor.destroy_volume(vdi_uuid) 

3766 try: 

3767 self.forgetVDI(vdi_uuid) 

3768 except: 

3769 pass 

3770 except Exception as e: 

3771 Util.log('Cannot delete bad VDI: {}'.format(e)) 

3772 finally: 

3773 self.unlock() 

3774 continue 

3775 

3776 vdi_type = volume_metadata.get(VDI_TYPE_TAG) 

3777 if VdiType.isCowImage(vdi_type): 

3778 pending_vdis.append((vdi_uuid, vdi_type)) 

3779 else: 

3780 all_vdi_info[vdi_uuid] = None 

3781 except Exception as e: 

3782 all_vdi_info[vdi_uuid] = handle_fail(vdi_uuid, e) 

3783 

3784 multi_cowutil = MultiLinstorCowUtil(self._linstor.uri, self._linstor.group_name) 

3785 

3786 def load_info(vdi, multi_cowutil): 

3787 vdi_uuid, vdi_type = vdi 

3788 try: 

3789 vdiInfo = multi_cowutil.get_local_cowutil(vdi_type).get_info(vdi_uuid) 

3790 except Exception as e: 

3791 vdiInfo = handle_fail(vdi_uuid, e) 

3792 vdiInfo.vdiType = vdi_type 

3793 return vdiInfo 

3794 

3795 try: 

3796 for vdiInfo in multi_cowutil.run(load_info, pending_vdis): 

3797 all_vdi_info[vdiInfo.uuid] = vdiInfo 

3798 finally: 

3799 del multi_cowutil 

3800 

3801 return all_vdi_info 

3802 

3803 @override 

3804 def _prepareCoalesceLeaf(self, vdi) -> None: 

3805 vdi._activateChain() 

3806 vdi.deflate() 

3807 vdi._inflateParentForCoalesce() 

3808 

3809 @override 

3810 def _finishCoalesceLeaf(self, parent) -> None: 

3811 if not parent.isSnapshot() or parent.isAttachedRW(): 

3812 parent.inflateFully() 

3813 else: 

3814 parent.deflate() 

3815 

3816 @override 

3817 def _calcExtraSpaceNeeded(self, child, parent) -> int: 

3818 return LinstorCowUtil( 

3819 self.xapi.session, self._linstor, parent.vdi_type 

3820 ).compute_volume_size(parent.sizeVirt) - parent.getDrbdSize() 

3821 

3822 def _hasValidDevicePath(self, uuid): 

3823 try: 

3824 self._linstor.get_device_path(uuid) 

3825 except Exception: 

3826 # TODO: Maybe log exception. 

3827 return False 

3828 return True 

3829 

3830 @override 

3831 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool: 

3832 self.lock() 

3833 try: 

3834 self._linstor.ensure_volume_is_not_locked( 

3835 vdi.uuid, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT 

3836 ) 

3837 return super(LinstorSR, self)._liveLeafCoalesce(vdi) 

3838 finally: 

3839 self.unlock() 

3840 

3841 @override 

3842 def _handleInterruptedCoalesceLeaf(self) -> None: 

3843 entries = self.journaler.get_all(VDI.JRN_LEAF) 

3844 for uuid, parentUuid in entries.items(): 

3845 if self._hasValidDevicePath(parentUuid) or \ 

3846 self._hasValidDevicePath(self.TMP_RENAME_PREFIX + uuid): 

3847 self._undoInterruptedCoalesceLeaf(uuid, parentUuid) 

3848 else: 

3849 self._finishInterruptedCoalesceLeaf(uuid, parentUuid) 

3850 self.journaler.remove(VDI.JRN_LEAF, uuid) 

3851 vdi = self.getVDI(uuid) 

3852 if vdi: 

3853 vdi.ensureUnpaused() 

3854 

3855 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3856 Util.log('*** UNDO LEAF-COALESCE') 

3857 parent = self.getVDI(parentUuid) 

3858 if not parent: 

3859 parent = self.getVDI(childUuid) 

3860 if not parent: 

3861 raise util.SMException( 

3862 'Neither {} nor {} found'.format(parentUuid, childUuid) 

3863 ) 

3864 Util.log( 

3865 'Renaming parent back: {} -> {}'.format(childUuid, parentUuid) 

3866 ) 

3867 parent.rename(parentUuid) 

3868 

3869 child = self.getVDI(childUuid) 

3870 if not child: 

3871 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid) 

3872 if not child: 

3873 raise util.SMException( 

3874 'Neither {} nor {} found'.format( 

3875 childUuid, self.TMP_RENAME_PREFIX + childUuid 

3876 ) 

3877 ) 

3878 Util.log('Renaming child back to {}'.format(childUuid)) 

3879 child.rename(childUuid) 

3880 Util.log('Updating the VDI record') 

3881 child.setConfig(VDI.DB_VDI_PARENT, parentUuid) 

3882 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type) 

3883 

3884 # TODO: Maybe deflate here. 

3885 

3886 if child.isHidden(): 

3887 child._setHidden(False) 

3888 if not parent.isHidden(): 

3889 parent._setHidden(True) 

3890 self._updateSlavesOnUndoLeafCoalesce(parent, child) 

3891 Util.log('*** leaf-coalesce undo successful') 

3892 

3893 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid): 

3894 Util.log('*** FINISH LEAF-COALESCE') 

3895 vdi = self.getVDI(childUuid) 

3896 if not vdi: 

3897 raise util.SMException('VDI {} not found'.format(childUuid)) 

3898 # TODO: Maybe inflate. 

3899 try: 

3900 self.forgetVDI(parentUuid) 

3901 except XenAPI.Failure: 

3902 pass 

3903 self._updateSlavesOnResize(vdi) 

3904 Util.log('*** finished leaf-coalesce successfully') 

3905 

3906 def _checkSlaves(self, vdi): 

3907 try: 

3908 openers = self._linstor.get_volume_openers(vdi.uuid) 

3909 for host_openers in openers.values(): 

3910 for opener in host_openers.values(): 

3911 if opener['process-name'] != 'tapdisk': 

3912 raise util.SMException( 

3913 'VDI {} is in use: {}'.format(vdi.uuid, openers) 

3914 ) 

3915 except LinstorVolumeManagerError as e: 

3916 if e.code != LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS: 

3917 raise 

3918 

3919 @classmethod 

3920 def abort_gc_from_openers_vdi(cls, vdi_uuid: str, openers: "LinstorVolumeOpeners") -> bool: 

3921 return cls._abort_gc_from_openers(vdi_uuid, True, openers) 

3922 

3923 @classmethod 

3924 def abort_gc_from_openers_sr(cls, sr_uuid: str, openers: "LinstorVolumeOpeners") -> bool: 

3925 return cls._abort_gc_from_openers(sr_uuid, False, openers) 

3926 

3927 @staticmethod 

3928 def _abort_gc_from_openers(uuid: str, is_vdi_uuid: bool, openers: "LinstorVolumeOpeners") -> bool: 

3929 from linstorcowutil import MANAGER_PLUGIN 

3930 

3931 node_name = None 

3932 

3933 for host_openers in openers.values(): 

3934 for hostname, opener in host_openers.items(): 

3935 # Not the most accurate check but it works... 

3936 # `vhd-util` is probably prefixed with a "+" which is ignored here. 

3937 if not opener["process-name"].endswith("vhd-util") or "coalesce" not in opener["cmdline"]: 

3938 continue 

3939 

3940 if not node_name: 

3941 import socket 

3942 node_name = socket.gethostname() 

3943 

3944 if node_name == hostname: 

3945 continue 

3946 

3947 with util.timeout(5), util.ApiSession("SMGC-coalescing") as session: 

3948 sr_uuid = util.get_sr_uuid_from_vdi_uuid(session, uuid) if is_vdi_uuid else uuid 

3949 util.SMlog(f"LINSTOR volume is coalescing on `{sr_uuid}`. We're going to interrupt the GC...") 

3950 return util.strtobool(session.xenapi.host.call_plugin( 

3951 util.get_master_ref(session), MANAGER_PLUGIN, "abortGc", {"srUuid": sr_uuid} 

3952 )) 

3953 return False 

3954 

3955 

3956 

3957################################################################################ 

3958# 

3959# Helpers 

3960# 

3961def daemonize(): 

3962 pid = os.fork() 

3963 if pid: 

3964 os.waitpid(pid, 0) 

3965 Util.log("New PID [%d]" % pid) 

3966 return False 

3967 os.chdir("/") 

3968 os.setsid() 

3969 pid = os.fork() 

3970 if pid: 

3971 Util.log("Will finish as PID [%d]" % pid) 

3972 os._exit(0) 

3973 for fd in [0, 1, 2]: 

3974 try: 

3975 os.close(fd) 

3976 except OSError: 

3977 pass 

3978 # we need to fill those special fd numbers or pread won't work 

3979 sys.stdin = open("/dev/null", 'r') 

3980 sys.stderr = open("/dev/null", 'w') 

3981 sys.stdout = open("/dev/null", 'w') 

3982 # As we're a new process we need to clear the lock objects 

3983 lock.Lock.clearAll() 

3984 return True 

3985 

3986 

3987def normalizeType(type): 

3988 if type in LVMSR.SUBTYPES: 

3989 type = SR.TYPE_LVHD 

3990 if type in ["lvm", "lvmoiscsi", "lvmohba", "lvmofcoe"]: 

3991 # temporary while LVHD is symlinked as LVM 

3992 type = SR.TYPE_LVHD 

3993 if type in [ 

3994 "ext", "nfs", "ocfsoiscsi", "ocfsohba", "smb", "cephfs", "glusterfs", 

3995 "moosefs", "xfs", "zfs", "largeblock" 

3996 ]: 

3997 type = SR.TYPE_FILE 

3998 if type in ["linstor"]: 

3999 type = SR.TYPE_LINSTOR 

4000 if type not in SR.TYPES: 

4001 raise util.SMException("Unsupported SR type: %s" % type) 

4002 return type 

4003 

4004GCPAUSE_DEFAULT_SLEEP = 5 * 60 

4005 

4006 

4007def _gc_init_file(sr_uuid): 

4008 return os.path.join(NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init') 

4009 

4010 

4011def _create_init_file(sr_uuid): 

4012 util.makedirs(os.path.join(NON_PERSISTENT_DIR, str(sr_uuid))) 

4013 with open(os.path.join(_gc_init_file(sr_uuid)), 'w+') as f: 

4014 f.write('1') 

4015 

4016 

4017def _gcLoopPause(sr, dryRun=False, immediate=False): 

4018 if immediate: 

4019 return 

4020 

4021 # Check to see if the GCPAUSE_FISTPOINT is present. If so the fist 

4022 # point will just return. Otherwise, fall back on an abortable sleep. 

4023 

4024 if util.fistpoint.is_active(util.GCPAUSE_FISTPOINT): 

4025 

4026 util.fistpoint.activate_custom_fn(util.GCPAUSE_FISTPOINT, 4026 ↛ exitline 4026 didn't jump to the function exit

4027 lambda *args: None) 

4028 elif os.path.exists(_gc_init_file(sr.uuid)): 

4029 def abortTest(): 

4030 return IPCFlag(sr.uuid).test(FLAG_TYPE_ABORT) 

4031 

4032 # If time.sleep hangs we are in deep trouble, however for 

4033 # completeness we set the timeout of the abort thread to 

4034 # 110% of GCPAUSE_DEFAULT_SLEEP. 

4035 Util.log("GC active, about to go quiet") 

4036 Util.runAbortable(lambda: time.sleep(GCPAUSE_DEFAULT_SLEEP), 4036 ↛ exitline 4036 didn't run the lambda on line 4036

4037 None, sr.uuid, abortTest, VDI.POLL_INTERVAL, 

4038 GCPAUSE_DEFAULT_SLEEP * 1.1) 

4039 Util.log("GC active, quiet period ended") 

4040 

4041 

4042def _gcLoop(sr, dryRun=False, immediate=False): 

4043 if not lockGCActive.acquireNoblock(): 4043 ↛ 4044line 4043 didn't jump to line 4044, because the condition on line 4043 was never true

4044 Util.log("Another GC instance already active, exiting") 

4045 return 

4046 

4047 # Check we're still attached after acquiring locks 

4048 if not sr.xapi.isPluggedHere(): 

4049 Util.log("SR no longer attached, exiting") 

4050 return 

4051 

4052 # Clean up Intellicache files 

4053 sr.cleanupCache() 

4054 

4055 # Track how many we do 

4056 coalesced = 0 

4057 task_status = "success" 

4058 try: 

4059 # Check if any work needs to be done 

4060 if not sr.xapi.isPluggedHere(): 4060 ↛ 4061line 4060 didn't jump to line 4061, because the condition on line 4060 was never true

4061 Util.log("SR no longer attached, exiting") 

4062 return 

4063 sr.scanLocked() 

4064 if not sr.hasWork(): 

4065 Util.log("No work, exiting") 

4066 return 

4067 sr.xapi.create_task( 

4068 "Garbage Collection", 

4069 "Garbage collection for SR %s" % sr.uuid) 

4070 _gcLoopPause(sr, dryRun, immediate=immediate) 

4071 while True: 

4072 if SIGTERM: 

4073 Util.log("Term requested") 

4074 return 

4075 

4076 if not sr.xapi.isPluggedHere(): 4076 ↛ 4077line 4076 didn't jump to line 4077, because the condition on line 4076 was never true

4077 Util.log("SR no longer attached, exiting") 

4078 break 

4079 sr.scanLocked() 

4080 if not sr.hasWork(): 

4081 Util.log("No work, exiting") 

4082 break 

4083 

4084 if not lockGCRunning.acquireNoblock(): 4084 ↛ 4085line 4084 didn't jump to line 4085, because the condition on line 4084 was never true

4085 Util.log("Unable to acquire GC running lock.") 

4086 return 

4087 try: 

4088 if not sr.gcEnabled(): 4088 ↛ 4089line 4088 didn't jump to line 4089, because the condition on line 4088 was never true

4089 break 

4090 

4091 sr.xapi.update_task_progress("done", coalesced) 

4092 

4093 sr.cleanupCoalesceJournals() 

4094 # Create the init file here in case startup is waiting on it 

4095 _create_init_file(sr.uuid) 

4096 sr.scanLocked() 

4097 sr.updateBlockInfo() 

4098 

4099 howmany = len(sr.findGarbage()) 

4100 if howmany > 0: 

4101 Util.log("Found %d orphaned vdis" % howmany) 

4102 sr.lock() 

4103 try: 

4104 sr.garbageCollect(dryRun) 

4105 finally: 

4106 sr.unlock() 

4107 sr.xapi.srUpdate() 

4108 

4109 candidate = sr.findCoalesceable() 

4110 if candidate: 

4111 util.fistpoint.activate( 

4112 "LVHDRT_finding_a_suitable_pair", sr.uuid) 

4113 sr.coalesce(candidate, dryRun) 

4114 sr.xapi.srUpdate() 

4115 coalesced += 1 

4116 continue 

4117 

4118 candidate = sr.findLeafCoalesceable() 

4119 if candidate: 4119 ↛ 4126line 4119 didn't jump to line 4126, because the condition on line 4119 was never false

4120 sr.coalesceLeaf(candidate, dryRun) 

4121 sr.xapi.srUpdate() 

4122 coalesced += 1 

4123 continue 

4124 

4125 finally: 

4126 lockGCRunning.release() 4126 ↛ 4131line 4126 didn't jump to line 4131, because the break on line 4089 wasn't executed

4127 except: 

4128 task_status = "failure" 

4129 raise 

4130 finally: 

4131 sr.xapi.set_task_status(task_status) 

4132 Util.log("GC process exiting, no work left") 

4133 _create_init_file(sr.uuid) 

4134 lockGCActive.release() 

4135 

4136 

4137def _gc(session, srUuid, dryRun=False, immediate=False): 

4138 init(srUuid) 

4139 sr = SR.getInstance(srUuid, session) 

4140 if not sr.gcEnabled(False): 4140 ↛ 4141line 4140 didn't jump to line 4141, because the condition on line 4140 was never true

4141 return 

4142 

4143 try: 

4144 _gcLoop(sr, dryRun, immediate=immediate) 

4145 finally: 

4146 sr.check_no_space_candidates() 

4147 sr.cleanup() 

4148 sr.logFilter.logState() 

4149 del sr.xapi 

4150 

4151 

4152def _abort(srUuid, soft=False): 

4153 """Aborts an GC/coalesce. 

4154 

4155 srUuid: the UUID of the SR whose GC/coalesce must be aborted 

4156 soft: If set to True and there is a pending abort signal, the function 

4157 doesn't do anything. If set to False, a new abort signal is issued. 

4158 

4159 returns: If soft is set to False, we return True holding lockGCActive. If 

4160 soft is set to False and an abort signal is pending, we return False 

4161 without holding lockGCActive. An exception is raised in case of error.""" 

4162 Util.log("=== SR %s: abort ===" % (srUuid)) 

4163 init(srUuid) 

4164 if not lockGCActive.acquireNoblock(): 

4165 gotLock = False 

4166 Util.log("Aborting currently-running instance (SR %s)" % srUuid) 

4167 abortFlag = IPCFlag(srUuid) 

4168 if not abortFlag.set(FLAG_TYPE_ABORT, soft): 

4169 return False 

4170 for i in range(SR.LOCK_RETRY_ATTEMPTS): 

4171 gotLock = lockGCActive.acquireNoblock() 

4172 if gotLock: 

4173 break 

4174 time.sleep(SR.LOCK_RETRY_INTERVAL) 

4175 abortFlag.clear(FLAG_TYPE_ABORT) 

4176 if not gotLock: 

4177 raise util.CommandException(code=errno.ETIMEDOUT, 

4178 reason="SR %s: error aborting existing process" % srUuid) 

4179 return True 

4180 

4181 

4182def init(srUuid): 

4183 global lockGCRunning 

4184 if not lockGCRunning: 4184 ↛ 4185line 4184 didn't jump to line 4185, because the condition on line 4184 was never true

4185 lockGCRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, srUuid) 

4186 global lockGCActive 

4187 if not lockGCActive: 4187 ↛ 4188line 4187 didn't jump to line 4188, because the condition on line 4187 was never true

4188 lockGCActive = LockActive(srUuid) 

4189 

4190 

4191class LockActive: 

4192 """ 

4193 Wraps the use of LOCK_TYPE_GC_ACTIVE such that the lock cannot be acquired 

4194 if another process holds the SR lock. 

4195 """ 

4196 def __init__(self, srUuid): 

4197 self._lock = lock.Lock(LOCK_TYPE_GC_ACTIVE, srUuid) 

4198 self._srLock = lock.Lock(lock.LOCK_TYPE_SR, srUuid) 

4199 

4200 def acquireNoblock(self): 

4201 self._srLock.acquire() 

4202 

4203 try: 

4204 return self._lock.acquireNoblock() 

4205 finally: 

4206 self._srLock.release() 

4207 

4208 def release(self): 

4209 self._lock.release() 

4210 

4211 

4212def usage(): 

4213 output = """Garbage collect and/or coalesce COW images in a COW-based SR 

4214 

4215Parameters: 

4216 -u --uuid UUID SR UUID 

4217 and one of: 

4218 -g --gc garbage collect, coalesce, and repeat while there is work 

4219 -G --gc_force garbage collect once, aborting any current operations 

4220 -c --cache-clean <max_age> clean up IntelliCache cache files older than 

4221 max_age hours 

4222 -a --abort abort any currently running operation (GC or coalesce) 

4223 -q --query query the current state (GC'ing, coalescing or not running) 

4224 -x --disable disable GC/coalesce (will be in effect until you exit) 

4225 -t --debug see Debug below 

4226 

4227Options: 

4228 -b --background run in background (return immediately) (valid for -g only) 

4229 -f --force continue in the presence of COW images with errors (when doing 

4230 GC, this might cause removal of any such images) (only valid 

4231 for -G) (DANGEROUS) 

4232 

4233Debug: 

4234 The --debug parameter enables manipulation of LVHD VDIs for debugging 

4235 purposes. ** NEVER USE IT ON A LIVE VM ** 

4236 The following parameters are required: 

4237 -t --debug <cmd> <cmd> is one of "activate", "deactivate", "inflate", 

4238 "deflate". 

4239 -v --vdi_uuid VDI UUID 

4240 """ 

4241 #-d --dry-run don't actually perform any SR-modifying operations 

4242 print(output) 

4243 Util.log("(Invalid usage)") 

4244 sys.exit(1) 

4245 

4246 

4247############################################################################## 

4248# 

4249# API 

4250# 

4251def abort(srUuid, soft=False): 

4252 """Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair. 

4253 """ 

4254 if _abort(srUuid, soft): 

4255 stop_gc_service(srUuid) 

4256 Util.log("abort: releasing the process lock") 

4257 lockGCActive.release() 

4258 return True 

4259 else: 

4260 return False 

4261 

4262 

4263def run_gc(session, srUuid, dryRun, immediate=False): 

4264 try: 

4265 _gc(session, srUuid, dryRun, immediate=immediate) 

4266 return 0 

4267 except AbortException: 

4268 Util.log("Aborted") 

4269 return 2 

4270 except Exception: 

4271 Util.logException("gc") 

4272 Util.log("* * * * * SR %s: ERROR\n" % srUuid) 

4273 return 1 

4274 

4275 

4276def gc(session, srUuid, inBackground, dryRun=False): 

4277 """Garbage collect all deleted VDIs in SR "srUuid". Fork & return 

4278 immediately if inBackground=True. 

4279 

4280 The following algorithm is used: 

4281 1. If we are already GC'ing in this SR, return 

4282 2. If we are already coalescing a VDI pair: 

4283 a. Scan the SR and determine if the VDI pair is GC'able 

4284 b. If the pair is not GC'able, return 

4285 c. If the pair is GC'able, abort coalesce 

4286 3. Scan the SR 

4287 4. If there is nothing to collect, nor to coalesce, return 

4288 5. If there is something to collect, GC all, then goto 3 

4289 6. If there is something to coalesce, coalesce one pair, then goto 3 

4290 """ 

4291 Util.log("=== SR %s: gc ===" % srUuid) 

4292 

4293 signal.signal(signal.SIGTERM, receiveSignal) 

4294 

4295 if inBackground: 

4296 if daemonize(): 4296 ↛ exitline 4296 didn't return from function 'gc', because the condition on line 4296 was never false

4297 # we are now running in the background. Catch & log any errors 

4298 # because there is no other way to propagate them back at this 

4299 # point 

4300 

4301 run_gc(None, srUuid, dryRun) 

4302 os._exit(0) 

4303 else: 

4304 os._exit(run_gc(session, srUuid, dryRun, immediate=True)) 

4305 

4306 

4307def start_gc(session, sr_uuid): 

4308 """ 

4309 This function is used to try to start a backgrounded GC session by forking 

4310 the current process. If using the systemd version, call start_gc_service() instead. 

4311 """ 

4312 # don't bother if an instance already running (this is just an 

4313 # optimization to reduce the overhead of forking a new process if we 

4314 # don't have to, but the process will check the lock anyways) 

4315 lockRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, sr_uuid) 

4316 if not lockRunning.acquireNoblock(): 

4317 if should_preempt(session, sr_uuid): 

4318 util.SMlog("Aborting currently-running coalesce of garbage VDI") 

4319 try: 

4320 if not abort(sr_uuid, soft=True): 

4321 util.SMlog("The GC has already been scheduled to re-start") 

4322 except util.CommandException as e: 

4323 if e.code != errno.ETIMEDOUT: 

4324 raise 

4325 util.SMlog('failed to abort the GC') 

4326 else: 

4327 util.SMlog("A GC instance already running, not kicking") 

4328 return 

4329 else: 

4330 lockRunning.release() 

4331 

4332 util.SMlog(f"Starting GC file is {__file__}") 

4333 subprocess.run([__file__, '-b', '-u', sr_uuid, '-g'], 

4334 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) 

4335 

4336def _gc_service_cmd(sr_uuid, action, extra_args=None): 

4337 """ 

4338 Build and run the systemctl command for the GC service using util.doexec. 

4339 """ 

4340 sr_uuid_esc = sr_uuid.replace("-", "\\x2d") 

4341 cmd=["/usr/bin/systemctl", "--quiet"] 

4342 if extra_args: 

4343 cmd.extend(extra_args) 

4344 cmd += [action, f"SMGC@{sr_uuid_esc}"] 

4345 return util.doexec(cmd) 

4346 

4347 

4348def start_gc_service(sr_uuid, wait=False): 

4349 """ 

4350 This starts the templated systemd service which runs GC on the given SR UUID. 

4351 If the service was already started, this is a no-op. 

4352 

4353 Because the service is a one-shot with RemainAfterExit=no, when called with 

4354 wait=True this will run the service synchronously and will not return until the 

4355 run has finished. This is used to force a run of the GC instead of just kicking it 

4356 in the background. 

4357 """ 

4358 util.SMlog(f"Kicking SMGC@{sr_uuid}...") 

4359 _gc_service_cmd(sr_uuid, "start", extra_args=None if wait else ["--no-block"]) 

4360 

4361 

4362def stop_gc_service(sr_uuid): 

4363 """ 

4364 Stops the templated systemd service which runs GC on the given SR UUID. 

4365 """ 

4366 util.SMlog(f"Stopping SMGC@{sr_uuid}...") 

4367 (rc, _stdout, stderr) = _gc_service_cmd(sr_uuid, "stop") 

4368 if rc != 0: 4368 ↛ exitline 4368 didn't return from function 'stop_gc_service', because the condition on line 4368 was never false

4369 util.SMlog(f"Failed to stop gc service `SMGC@{sr_uuid}`: `{stderr}`") 

4370 

4371 

4372def wait_for_completion(sr_uuid): 

4373 while get_state(sr_uuid): 

4374 time.sleep(5) 

4375 

4376 

4377def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False): 

4378 """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure 

4379 the SR lock is held. 

4380 The following algorithm is used: 

4381 1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce 

4382 2. Scan the SR 

4383 3. GC 

4384 4. return 

4385 """ 

4386 Util.log("=== SR %s: gc_force ===" % srUuid) 

4387 init(srUuid) 

4388 sr = SR.getInstance(srUuid, session, lockSR, True) 

4389 if not lockGCActive.acquireNoblock(): 

4390 abort(srUuid) 

4391 else: 

4392 Util.log("Nothing was running, clear to proceed") 

4393 

4394 if force: 

4395 Util.log("FORCED: will continue even if there are COW image errors") 

4396 sr.scanLocked(force) 

4397 sr.cleanupCoalesceJournals() 

4398 

4399 try: 

4400 sr.cleanupCache() 

4401 sr.garbageCollect(dryRun) 

4402 finally: 

4403 sr.cleanup() 

4404 sr.logFilter.logState() 

4405 lockGCActive.release() 

4406 

4407 

4408def get_state(srUuid): 

4409 """Return whether GC/coalesce is currently running or not. This asks systemd for 

4410 the state of the templated SMGC service and will return True if it is "activating" 

4411 or "running" (for completeness, as in practice it will never achieve the latter state) 

4412 """ 

4413 sr_uuid_esc = srUuid.replace("-", "\\x2d") 

4414 cmd=[ "/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"] 

4415 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True) 

4416 state = result.stdout.decode('utf-8').rstrip() 

4417 if state == "activating" or state == "running": 

4418 return True 

4419 return False 

4420 

4421 

4422def should_preempt(session, srUuid): 

4423 sr = SR.getInstance(srUuid, session) 

4424 entries = sr.journaler.getAll(VDI.JRN_COALESCE) 

4425 if len(entries) == 0: 

4426 return False 

4427 elif len(entries) > 1: 

4428 raise util.SMException("More than one coalesce entry: " + str(entries)) 

4429 sr.scanLocked() 

4430 coalescedUuid = entries.popitem()[0] 

4431 garbage = sr.findGarbage() 

4432 for vdi in garbage: 

4433 if vdi.uuid == coalescedUuid: 

4434 return True 

4435 return False 

4436 

4437 

4438def get_coalesceable_leaves(session, srUuid, vdiUuids): 

4439 coalesceable = [] 

4440 sr = SR.getInstance(srUuid, session) 

4441 sr.scanLocked() 

4442 for uuid in vdiUuids: 

4443 vdi = sr.getVDI(uuid) 

4444 if not vdi: 

4445 raise util.SMException("VDI %s not found" % uuid) 

4446 if vdi.isLeafCoalesceable(): 

4447 coalesceable.append(uuid) 

4448 return coalesceable 

4449 

4450 

4451def cache_cleanup(session, srUuid, maxAge): 

4452 sr = SR.getInstance(srUuid, session) 

4453 return sr.cleanupCache(maxAge) 

4454 

4455 

4456def debug(sr_uuid, cmd, vdi_uuid): 

4457 Util.log("Debug command: %s" % cmd) 

4458 sr = SR.getInstance(sr_uuid, None) 

4459 if not isinstance(sr, LVMSR): 

4460 print("Error: not an LVM SR") 

4461 return 

4462 sr.scanLocked() 

4463 vdi = sr.getVDI(vdi_uuid) 

4464 if not vdi: 

4465 print("Error: VDI %s not found") 

4466 return 

4467 print("Running %s on SR %s" % (cmd, sr)) 

4468 print("VDI before: %s" % vdi) 

4469 if cmd == "activate": 

4470 vdi._activate() 

4471 print("VDI file: %s" % vdi.path) 

4472 if cmd == "deactivate": 

4473 ns = NS_PREFIX_LVM + sr.uuid 

4474 sr.lvmCache.deactivate(ns, vdi.uuid, vdi.fileName, False) 

4475 if cmd == "inflate": 

4476 vdi.inflateFully() 

4477 sr.cleanup() 

4478 if cmd == "deflate": 

4479 vdi.deflate() 

4480 sr.cleanup() 

4481 sr.scanLocked() 

4482 print("VDI after: %s" % vdi) 

4483 

4484 

4485def abort_optional_reenable(uuid): 

4486 print("Disabling GC/coalesce for %s" % uuid) 

4487 ret = _abort(uuid) 

4488 input("Press enter to re-enable...") 

4489 print("GC/coalesce re-enabled") 

4490 lockGCRunning.release() 

4491 if ret: 

4492 lockGCActive.release() 

4493 

4494 

4495############################################################################## 

4496# 

4497# CLI 

4498# 

4499def main(): 

4500 action = "" 

4501 maxAge = 0 

4502 uuid = "" 

4503 background = False 

4504 force = False 

4505 dryRun = False 

4506 debug_cmd = "" 

4507 vdi_uuid = "" 

4508 shortArgs = "gGc:aqxu:bfdt:v:" 

4509 longArgs = ["gc", "gc_force", "clean_cache", "abort", "query", "disable", 

4510 "uuid=", "background", "force", "dry-run", "debug=", "vdi_uuid="] 

4511 

4512 try: 

4513 opts, args = getopt.getopt(sys.argv[1:], shortArgs, longArgs) 

4514 except getopt.GetoptError: 

4515 usage() 

4516 for o, a in opts: 

4517 if o in ("-g", "--gc"): 

4518 action = "gc" 

4519 if o in ("-G", "--gc_force"): 

4520 action = "gc_force" 

4521 if o in ("-c", "--clean_cache"): 

4522 action = "clean_cache" 

4523 maxAge = int(a) 

4524 if o in ("-a", "--abort"): 

4525 action = "abort" 

4526 if o in ("-q", "--query"): 

4527 action = "query" 

4528 if o in ("-x", "--disable"): 

4529 action = "disable" 

4530 if o in ("-u", "--uuid"): 

4531 uuid = a 

4532 if o in ("-b", "--background"): 

4533 background = True 

4534 if o in ("-f", "--force"): 

4535 force = True 

4536 if o in ("-d", "--dry-run"): 

4537 Util.log("Dry run mode") 

4538 dryRun = True 

4539 if o in ("-t", "--debug"): 

4540 action = "debug" 

4541 debug_cmd = a 

4542 if o in ("-v", "--vdi_uuid"): 

4543 vdi_uuid = a 

4544 

4545 if not action or not uuid: 

4546 usage() 

4547 if action == "debug" and not (debug_cmd and vdi_uuid) or \ 

4548 action != "debug" and (debug_cmd or vdi_uuid): 

4549 usage() 

4550 

4551 if action != "query" and action != "debug": 

4552 print("All output goes to log") 

4553 

4554 if action == "gc": 

4555 gc(None, uuid, background, dryRun) 

4556 elif action == "gc_force": 

4557 gc_force(None, uuid, force, dryRun, True) 

4558 elif action == "clean_cache": 

4559 cache_cleanup(None, uuid, maxAge) 

4560 elif action == "abort": 

4561 abort(uuid) 

4562 elif action == "query": 

4563 print("Currently running: %s" % get_state(uuid)) 

4564 elif action == "disable": 

4565 abort_optional_reenable(uuid) 

4566 elif action == "debug": 

4567 debug(uuid, debug_cmd, vdi_uuid) 

4568 

4569 

4570if __name__ == '__main__': 4570 ↛ 4571line 4570 didn't jump to line 4571, because the condition on line 4570 was never true

4571 main()