Coverage for libs/sm/cleanup.py: 39%
2441 statements
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2026-07-10 07:59 +0000
1#
2# Copyright (C) Citrix Systems Inc.
3#
4# This program is free software; you can redistribute it and/or modify
5# it under the terms of the GNU Lesser General Public License as published
6# by the Free Software Foundation; version 2.1 only.
7#
8# This program is distributed in the hope that it will be useful,
9# but WITHOUT ANY WARRANTY; without even the implied warranty of
10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11# GNU Lesser General Public License for more details.
12#
13# You should have received a copy of the GNU Lesser General Public License
14# along with this program; if not, write to the Free Software Foundation, Inc.,
15# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16#
17import os
18import os.path
19import sys
20import time
21import signal
22import subprocess
23import datetime
24import traceback
25import base64
26import zlib
27import errno
28import stat
30from sm.ipc import IPCFlag
31from functools import reduce
32from time import monotonic as _time
33from sm import blktap2
34import XenAPI # pylint: disable=import-error
36from sm import lvutil
37from sm import vhdutil
38from sm import lvhdutil
39from sm import lvmcache
40from sm import journaler
41from sm import fjournaler
42from sm.core import lock
43from sm.core import util
44from sm.refcounter import RefCounter
45from sm.lvmanager import LVActivator
46from sm.srmetadata import LVMMetadataHandler
48# Disable automatic leaf-coalescing. Online leaf-coalesce is currently not
49# possible due to lvhd_stop_using_() not working correctly. However, we leave
50# this option available through the explicit LEAFCLSC_FORCE flag in the VDI
51# record for use by the offline tool (which makes the operation safe by pausing
52# the VM first)
53AUTO_ONLINE_LEAF_COALESCE_ENABLED = True
55FLAG_TYPE_ABORT = "abort" # flag to request aborting of GC/coalesce
57# process "lock", used simply as an indicator that a process already exists
58# that is doing GC/coalesce on this SR (such a process holds the lock, and we
59# check for the fact by trying the lock).
60lockGCRunning = None
62# process "lock" to indicate that the GC process has been activated but may not
63# yet be running, stops a second process from being started.
64LOCK_TYPE_GC_ACTIVE = "gc_active"
65lockGCActive = None
67# Default coalesce error rate limit, in messages per minute. A zero value
68# disables throttling, and a negative value disables error reporting.
69DEFAULT_COALESCE_ERR_RATE = 1.0 / 60
71COALESCE_LAST_ERR_TAG = 'last-coalesce-error'
72COALESCE_ERR_RATE_TAG = 'coalesce-error-rate'
73VAR_RUN = "/run/"
74SPEED_LOG_ROOT = VAR_RUN + "{uuid}.speed_log"
76N_RUNNING_AVERAGE = 10
78NON_PERSISTENT_DIR = '/run/nonpersistent/sm'
80# Signal Handler
81SIGTERM = False
84class AbortException(util.SMException):
85 pass
88def receiveSignal(signalNumber, frame):
89 global SIGTERM
91 util.SMlog("GC: recieved SIGTERM")
92 SIGTERM = True
93 return
96################################################################################
97#
98# Util
99#
100class Util:
101 RET_RC = 1
102 RET_STDOUT = 2
103 RET_STDERR = 4
105 UUID_LEN = 36
107 PREFIX = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024}
109 def log(text):
110 util.SMlog(text, ident="SMGC")
111 log = staticmethod(log)
113 def logException(tag):
114 info = sys.exc_info()
115 if info[0] == SystemExit: 115 ↛ 117line 115 didn't jump to line 117, because the condition on line 115 was never true
116 # this should not be happening when catching "Exception", but it is
117 sys.exit(0)
118 tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2]))
119 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
120 Util.log(" ***********************")
121 Util.log(" * E X C E P T I O N *")
122 Util.log(" ***********************")
123 Util.log("%s: EXCEPTION %s, %s" % (tag, info[0], info[1]))
124 Util.log(tb)
125 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
126 logException = staticmethod(logException)
128 def doexec(args, expectedRC, inputtext=None, ret=None, log=True):
129 "Execute a subprocess, then return its return code, stdout, stderr"
130 proc = subprocess.Popen(args,
131 stdin=subprocess.PIPE, \
132 stdout=subprocess.PIPE, \
133 stderr=subprocess.PIPE, \
134 shell=True, \
135 close_fds=True)
136 (stdout, stderr) = proc.communicate(inputtext)
137 stdout = str(stdout)
138 stderr = str(stderr)
139 rc = proc.returncode
140 if log:
141 Util.log("`%s`: %s" % (args, rc))
142 if type(expectedRC) != type([]):
143 expectedRC = [expectedRC]
144 if not rc in expectedRC:
145 reason = stderr.strip()
146 if stdout.strip():
147 reason = "%s (stdout: %s)" % (reason, stdout.strip())
148 Util.log("Failed: %s" % reason)
149 raise util.CommandException(rc, args, reason)
151 if ret == Util.RET_RC:
152 return rc
153 if ret == Util.RET_STDERR:
154 return stderr
155 return stdout
156 doexec = staticmethod(doexec)
158 def runAbortable(func, ret, ns, abortTest, pollInterval, timeOut):
159 """execute func in a separate thread and kill it if abortTest signals
160 so"""
161 abortSignaled = abortTest() # check now before we clear resultFlag
162 resultFlag = IPCFlag(ns)
163 resultFlag.clearAll()
164 pid = os.fork()
165 if pid:
166 startTime = _time()
167 try:
168 while True:
169 if resultFlag.test("success"):
170 Util.log(" Child process completed successfully")
171 resultFlag.clear("success")
172 return
173 if resultFlag.test("failure"):
174 resultFlag.clear("failure")
175 raise util.SMException("Child process exited with error")
176 if abortTest() or abortSignaled or SIGTERM:
177 os.killpg(pid, signal.SIGKILL)
178 raise AbortException("Aborting due to signal")
179 if timeOut and _time() - startTime > timeOut:
180 os.killpg(pid, signal.SIGKILL)
181 resultFlag.clearAll()
182 raise util.SMException("Timed out")
183 time.sleep(pollInterval)
184 finally:
185 wait_pid = 0
186 rc = -1
187 count = 0
188 while wait_pid == 0 and count < 10:
189 wait_pid, rc = os.waitpid(pid, os.WNOHANG)
190 if wait_pid == 0:
191 time.sleep(2)
192 count += 1
194 if wait_pid == 0:
195 Util.log("runAbortable: wait for process completion timed out")
196 else:
197 os.setpgrp()
198 try:
199 if func() == ret:
200 resultFlag.set("success")
201 else:
202 resultFlag.set("failure")
203 except Exception as e:
204 Util.log("Child process failed with : (%s)" % e)
205 resultFlag.set("failure")
206 Util.logException("This exception has occured")
207 os._exit(0)
208 runAbortable = staticmethod(runAbortable)
210 def num2str(number):
211 for prefix in ("G", "M", "K"): 211 ↛ 214line 211 didn't jump to line 214, because the loop on line 211 didn't complete
212 if number >= Util.PREFIX[prefix]:
213 return "%.3f%s" % (float(number) / Util.PREFIX[prefix], prefix)
214 return "%s" % number
215 num2str = staticmethod(num2str)
217 def numBits(val):
218 count = 0
219 while val:
220 count += val & 1
221 val = val >> 1
222 return count
223 numBits = staticmethod(numBits)
225 def countBits(bitmap1, bitmap2):
226 """return bit count in the bitmap produced by ORing the two bitmaps"""
227 len1 = len(bitmap1)
228 len2 = len(bitmap2)
229 lenLong = len1
230 lenShort = len2
231 bitmapLong = bitmap1
232 if len2 > len1:
233 lenLong = len2
234 lenShort = len1
235 bitmapLong = bitmap2
237 count = 0
238 for i in range(lenShort):
239 val = bitmap1[i] | bitmap2[i]
240 count += Util.numBits(val)
242 for i in range(i + 1, lenLong):
243 val = bitmapLong[i]
244 count += Util.numBits(val)
245 return count
246 countBits = staticmethod(countBits)
249################################################################################
250#
251# XAPI
252#
253class XAPI:
254 USER = "root"
255 PLUGIN_ON_SLAVE = "on-slave"
257 CONFIG_SM = 0
258 CONFIG_OTHER = 1
259 CONFIG_ON_BOOT = 2
260 CONFIG_ALLOW_CACHING = 3
262 CONFIG_NAME = {
263 CONFIG_SM: "sm-config",
264 CONFIG_OTHER: "other-config",
265 CONFIG_ON_BOOT: "on-boot",
266 CONFIG_ALLOW_CACHING: "allow_caching"
267 }
269 class LookupError(util.SMException):
270 pass
272 def getSession():
273 session = XenAPI.xapi_local()
274 session.xenapi.login_with_password(XAPI.USER, '', '', 'SM')
275 return session
276 getSession = staticmethod(getSession)
278 def __init__(self, session, srUuid):
279 self.sessionPrivate = False
280 self.session = session
281 if self.session is None:
282 self.session = self.getSession()
283 self.sessionPrivate = True
284 self._srRef = self.session.xenapi.SR.get_by_uuid(srUuid)
285 self.srRecord = self.session.xenapi.SR.get_record(self._srRef)
286 self.hostUuid = util.get_this_host()
287 self._hostRef = self.session.xenapi.host.get_by_uuid(self.hostUuid)
288 self.task = None
289 self.task_progress = {"coalescable": 0, "done": 0}
291 def __del__(self):
292 if self.sessionPrivate:
293 self.session.xenapi.session.logout()
295 @property
296 def srRef(self):
297 return self._srRef
299 def isPluggedHere(self):
300 pbds = self.getAttachedPBDs()
301 for pbdRec in pbds:
302 if pbdRec["host"] == self._hostRef:
303 return True
304 return False
306 def poolOK(self):
307 host_recs = self.session.xenapi.host.get_all_records()
308 for host_ref, host_rec in host_recs.items():
309 if not host_rec["enabled"]:
310 Util.log("Host %s not enabled" % host_rec["uuid"])
311 return False
312 return True
314 def isMaster(self):
315 if self.srRecord["shared"]:
316 pool = list(self.session.xenapi.pool.get_all_records().values())[0]
317 return pool["master"] == self._hostRef
318 else:
319 pbds = self.getAttachedPBDs()
320 if len(pbds) < 1:
321 raise util.SMException("Local SR not attached")
322 elif len(pbds) > 1:
323 raise util.SMException("Local SR multiply attached")
324 return pbds[0]["host"] == self._hostRef
326 def getAttachedPBDs(self):
327 """Return PBD records for all PBDs of this SR that are currently
328 attached"""
329 attachedPBDs = []
330 pbds = self.session.xenapi.PBD.get_all_records()
331 for pbdRec in pbds.values():
332 if pbdRec["SR"] == self._srRef and pbdRec["currently_attached"]:
333 attachedPBDs.append(pbdRec)
334 return attachedPBDs
336 def getOnlineHosts(self):
337 return util.get_online_hosts(self.session)
339 def ensureInactive(self, hostRef, args):
340 text = self.session.xenapi.host.call_plugin( \
341 hostRef, self.PLUGIN_ON_SLAVE, "multi", args)
342 Util.log("call-plugin returned: '%s'" % text)
344 def getRecordHost(self, hostRef):
345 return self.session.xenapi.host.get_record(hostRef)
347 def _getRefVDI(self, uuid):
348 return self.session.xenapi.VDI.get_by_uuid(uuid)
350 def getRefVDI(self, vdi):
351 return self._getRefVDI(vdi.uuid)
353 def getRecordVDI(self, uuid):
354 try:
355 ref = self._getRefVDI(uuid)
356 return self.session.xenapi.VDI.get_record(ref)
357 except XenAPI.Failure:
358 return None
360 def singleSnapshotVDI(self, vdi):
361 return self.session.xenapi.VDI.snapshot(vdi.getRef(),
362 {"type": "internal"})
364 def forgetVDI(self, srUuid, vdiUuid):
365 """Forget the VDI, but handle the case where the VDI has already been
366 forgotten (i.e. ignore errors)"""
367 try:
368 vdiRef = self.session.xenapi.VDI.get_by_uuid(vdiUuid)
369 self.session.xenapi.VDI.forget(vdiRef)
370 except XenAPI.Failure:
371 pass
373 def getConfigVDI(self, vdi, key):
374 kind = vdi.CONFIG_TYPE[key]
375 if kind == self.CONFIG_SM:
376 cfg = self.session.xenapi.VDI.get_sm_config(vdi.getRef())
377 elif kind == self.CONFIG_OTHER:
378 cfg = self.session.xenapi.VDI.get_other_config(vdi.getRef())
379 elif kind == self.CONFIG_ON_BOOT:
380 cfg = self.session.xenapi.VDI.get_on_boot(vdi.getRef())
381 elif kind == self.CONFIG_ALLOW_CACHING:
382 cfg = self.session.xenapi.VDI.get_allow_caching(vdi.getRef())
383 else:
384 assert(False)
385 Util.log("Got %s for %s: %s" % (self.CONFIG_NAME[kind], vdi, repr(cfg)))
386 return cfg
388 def removeFromConfigVDI(self, vdi, key):
389 kind = vdi.CONFIG_TYPE[key]
390 if kind == self.CONFIG_SM:
391 self.session.xenapi.VDI.remove_from_sm_config(vdi.getRef(), key)
392 elif kind == self.CONFIG_OTHER:
393 self.session.xenapi.VDI.remove_from_other_config(vdi.getRef(), key)
394 else:
395 assert(False)
397 def addToConfigVDI(self, vdi, key, val):
398 kind = vdi.CONFIG_TYPE[key]
399 if kind == self.CONFIG_SM:
400 self.session.xenapi.VDI.add_to_sm_config(vdi.getRef(), key, val)
401 elif kind == self.CONFIG_OTHER:
402 self.session.xenapi.VDI.add_to_other_config(vdi.getRef(), key, val)
403 else:
404 assert(False)
406 def isSnapshot(self, vdi):
407 return self.session.xenapi.VDI.get_is_a_snapshot(vdi.getRef())
409 def markCacheSRsDirty(self):
410 sr_refs = self.session.xenapi.SR.get_all_records_where( \
411 'field "local_cache_enabled" = "true"')
412 for sr_ref in sr_refs:
413 Util.log("Marking SR %s dirty" % sr_ref)
414 util.set_dirty(self.session, sr_ref)
416 def srUpdate(self):
417 Util.log("Starting asynch srUpdate for SR %s" % self.srRecord["uuid"])
418 abortFlag = IPCFlag(self.srRecord["uuid"])
419 task = self.session.xenapi.Async.SR.update(self._srRef)
420 cancelTask = True
421 try:
422 for i in range(60):
423 status = self.session.xenapi.task.get_status(task)
424 if not status == "pending":
425 Util.log("SR.update_asynch status changed to [%s]" % status)
426 cancelTask = False
427 return
428 if abortFlag.test(FLAG_TYPE_ABORT):
429 Util.log("Abort signalled during srUpdate, cancelling task...")
430 try:
431 self.session.xenapi.task.cancel(task)
432 cancelTask = False
433 Util.log("Task cancelled")
434 except:
435 pass
436 return
437 time.sleep(1)
438 finally:
439 if cancelTask:
440 self.session.xenapi.task.cancel(task)
441 self.session.xenapi.task.destroy(task)
442 Util.log("Asynch srUpdate still running, but timeout exceeded.")
444 def update_task(self):
445 self.session.xenapi.task.set_other_config(
446 self.task,
447 {
448 "applies_to": self._srRef
449 })
450 total = self.task_progress['coalescable'] + self.task_progress['done']
451 if (total > 0):
452 self.session.xenapi.task.set_progress(
453 self.task, float(self.task_progress['done']) / total)
455 def create_task(self, label, description):
456 self.task = self.session.xenapi.task.create(label, description)
457 self.update_task()
459 def update_task_progress(self, key, value):
460 self.task_progress[key] = value
461 if self.task:
462 self.update_task()
464 def set_task_status(self, status):
465 if self.task:
466 self.session.xenapi.task.set_status(self.task, status)
469################################################################################
470#
471# VDI
472#
473class VDI:
474 """Object representing a VDI of a VHD-based SR"""
476 POLL_INTERVAL = 1
477 POLL_TIMEOUT = 30
478 DEVICE_MAJOR = 202
479 DRIVER_NAME_VHD = "vhd"
481 # config keys & values
482 DB_VHD_PARENT = "vhd-parent"
483 DB_VDI_TYPE = "vdi_type"
484 DB_VHD_BLOCKS = "vhd-blocks"
485 DB_VDI_PAUSED = "paused"
486 DB_VDI_RELINKING = "relinking"
487 DB_VDI_ACTIVATING = "activating"
488 DB_GC = "gc"
489 DB_COALESCE = "coalesce"
490 DB_LEAFCLSC = "leaf-coalesce" # config key
491 DB_GC_NO_SPACE = "gc_no_space"
492 LEAFCLSC_DISABLED = "false" # set by user; means do not leaf-coalesce
493 LEAFCLSC_FORCE = "force" # set by user; means skip snap-coalesce
494 LEAFCLSC_OFFLINE = "offline" # set here for informational purposes: means
495 # no space to snap-coalesce or unable to keep
496 # up with VDI. This is not used by the SM, it
497 # might be used by external components.
498 DB_ONBOOT = "on-boot"
499 ONBOOT_RESET = "reset"
500 DB_ALLOW_CACHING = "allow_caching"
502 CONFIG_TYPE = {
503 DB_VHD_PARENT: XAPI.CONFIG_SM,
504 DB_VDI_TYPE: XAPI.CONFIG_SM,
505 DB_VHD_BLOCKS: XAPI.CONFIG_SM,
506 DB_VDI_PAUSED: XAPI.CONFIG_SM,
507 DB_VDI_RELINKING: XAPI.CONFIG_SM,
508 DB_VDI_ACTIVATING: XAPI.CONFIG_SM,
509 DB_GC: XAPI.CONFIG_OTHER,
510 DB_COALESCE: XAPI.CONFIG_OTHER,
511 DB_LEAFCLSC: XAPI.CONFIG_OTHER,
512 DB_ONBOOT: XAPI.CONFIG_ON_BOOT,
513 DB_ALLOW_CACHING: XAPI.CONFIG_ALLOW_CACHING,
514 DB_GC_NO_SPACE: XAPI.CONFIG_SM
515 }
517 LIVE_LEAF_COALESCE_MAX_SIZE = 20 * 1024 * 1024 # bytes
518 LIVE_LEAF_COALESCE_TIMEOUT = 10 # seconds
519 TIMEOUT_SAFETY_MARGIN = 0.5 # extra margin when calculating
520 # feasibility of leaf coalesce
522 JRN_RELINK = "relink" # journal entry type for relinking children
523 JRN_COALESCE = "coalesce" # to communicate which VDI is being coalesced
524 JRN_LEAF = "leaf" # used in coalesce-leaf
526 STR_TREE_INDENT = 4
528 def __init__(self, sr, uuid, raw):
529 self.sr = sr
530 self.scanError = True
531 self.uuid = uuid
532 self.raw = raw
533 self.fileName = ""
534 self.parentUuid = ""
535 self.sizeVirt = -1
536 self._sizeVHD = -1
537 self._sizeAllocated = -1
538 self._hidden = False
539 self.parent = None
540 self.children = []
541 self._vdiRef = None
542 self._clearRef()
544 @staticmethod
545 def extractUuid(path):
546 raise NotImplementedError("Implement in sub class")
548 def load(self, info=None):
549 """Load VDI info"""
550 pass # abstract
552 def getDriverName(self):
553 return self.DRIVER_NAME_VHD
555 def getRef(self):
556 if self._vdiRef is None:
557 self._vdiRef = self.sr.xapi.getRefVDI(self)
558 return self._vdiRef
560 def getConfig(self, key, default=None):
561 config = self.sr.xapi.getConfigVDI(self, key)
562 if key == self.DB_ONBOOT or key == self.DB_ALLOW_CACHING: 562 ↛ 563line 562 didn't jump to line 563, because the condition on line 562 was never true
563 val = config
564 else:
565 val = config.get(key)
566 if val:
567 return val
568 return default
570 def setConfig(self, key, val):
571 self.sr.xapi.removeFromConfigVDI(self, key)
572 self.sr.xapi.addToConfigVDI(self, key, val)
573 Util.log("Set %s = %s for %s" % (key, val, self))
575 def delConfig(self, key):
576 self.sr.xapi.removeFromConfigVDI(self, key)
577 Util.log("Removed %s from %s" % (key, self))
579 def ensureUnpaused(self):
580 if self.getConfig(self.DB_VDI_PAUSED) == "true":
581 Util.log("Unpausing VDI %s" % self)
582 self.unpause()
584 def pause(self, failfast=False):
585 if not blktap2.VDI.tap_pause(self.sr.xapi.session, self.sr.uuid,
586 self.uuid, failfast):
587 raise util.SMException("Failed to pause VDI %s" % self)
589 def _report_tapdisk_unpause_error(self):
590 try:
591 xapi = self.sr.xapi.session.xenapi
592 sr_ref = xapi.SR.get_by_uuid(self.sr.uuid)
593 msg_name = "failed to unpause tapdisk"
594 msg_body = "Failed to unpause tapdisk for VDI %s, " \
595 "VMs using this tapdisk have lost access " \
596 "to the corresponding disk(s)" % self.uuid
597 xapi.message.create(msg_name, "4", "SR", self.sr.uuid, msg_body)
598 except Exception as e:
599 util.SMlog("failed to generate message: %s" % e)
601 def unpause(self):
602 if not blktap2.VDI.tap_unpause(self.sr.xapi.session, self.sr.uuid,
603 self.uuid):
604 self._report_tapdisk_unpause_error()
605 raise util.SMException("Failed to unpause VDI %s" % self)
607 def refresh(self, ignoreNonexistent=True):
608 """Pause-unpause in one step"""
609 self.sr.lock()
610 try:
611 try:
612 if not blktap2.VDI.tap_refresh(self.sr.xapi.session, 612 ↛ 614line 612 didn't jump to line 614, because the condition on line 612 was never true
613 self.sr.uuid, self.uuid):
614 self._report_tapdisk_unpause_error()
615 raise util.SMException("Failed to refresh %s" % self)
616 except XenAPI.Failure as e:
617 if util.isInvalidVDI(e) and ignoreNonexistent:
618 Util.log("VDI %s not found, ignoring" % self)
619 return
620 raise
621 finally:
622 self.sr.unlock()
624 def isSnapshot(self):
625 return self.sr.xapi.isSnapshot(self)
627 def isAttachedRW(self):
628 return util.is_attached_rw(
629 self.sr.xapi.session.xenapi.VDI.get_sm_config(self.getRef()))
631 def getVHDBlocks(self):
632 val = self.updateBlockInfo()
633 bitmap = zlib.decompress(base64.b64decode(val))
634 return bitmap
636 def isCoalesceable(self):
637 """A VDI is coalesceable if it has no siblings and is not a leaf"""
638 return not self.scanError and \
639 self.parent and \
640 len(self.parent.children) == 1 and \
641 self.isHidden() and \
642 len(self.children) > 0
644 def isLeafCoalesceable(self):
645 """A VDI is leaf-coalesceable if it has no siblings and is a leaf"""
646 return not self.scanError and \
647 self.parent and \
648 len(self.parent.children) == 1 and \
649 not self.isHidden() and \
650 len(self.children) == 0
652 def canLiveCoalesce(self, speed):
653 """Can we stop-and-leaf-coalesce this VDI? The VDI must be
654 isLeafCoalesceable() already"""
655 feasibleSize = False
656 allowedDownTime = \
657 self.TIMEOUT_SAFETY_MARGIN * self.LIVE_LEAF_COALESCE_TIMEOUT
658 vhd_size = self.getAllocatedSize()
659 if speed:
660 feasibleSize = \
661 vhd_size // speed < allowedDownTime
662 else:
663 feasibleSize = \
664 vhd_size < self.LIVE_LEAF_COALESCE_MAX_SIZE
666 return (feasibleSize or
667 self.getConfig(self.DB_LEAFCLSC) == self.LEAFCLSC_FORCE)
669 def getAllPrunable(self):
670 if len(self.children) == 0: # base case
671 # it is possible to have a hidden leaf that was recently coalesced
672 # onto its parent, its children already relinked but not yet
673 # reloaded - in which case it may not be garbage collected yet:
674 # some tapdisks could still be using the file.
675 if self.sr.journaler.get(self.JRN_RELINK, self.uuid):
676 return []
677 if not self.scanError and self.isHidden():
678 return [self]
679 return []
681 thisPrunable = True
682 vdiList = []
683 for child in self.children:
684 childList = child.getAllPrunable()
685 vdiList.extend(childList)
686 if child not in childList:
687 thisPrunable = False
689 if not self.scanError and thisPrunable:
690 vdiList.append(self)
691 return vdiList
693 def getSizeVHD(self):
694 return self._sizeVHD
696 def getAllocatedSize(self):
697 return self._sizeAllocated
699 def getTreeRoot(self):
700 "Get the root of the tree that self belongs to"
701 root = self
702 while root.parent:
703 root = root.parent
704 return root
706 def getTreeHeight(self):
707 "Get the height of the subtree rooted at self"
708 if len(self.children) == 0:
709 return 1
711 maxChildHeight = 0
712 for child in self.children:
713 childHeight = child.getTreeHeight()
714 if childHeight > maxChildHeight:
715 maxChildHeight = childHeight
717 return maxChildHeight + 1
719 def getAllLeaves(self):
720 "Get all leaf nodes in the subtree rooted at self"
721 if len(self.children) == 0:
722 return [self]
724 leaves = []
725 for child in self.children:
726 leaves.extend(child.getAllLeaves())
727 return leaves
729 def updateBlockInfo(self):
730 val = base64.b64encode(self._queryVHDBlocks()).decode()
731 self.setConfig(VDI.DB_VHD_BLOCKS, val)
732 return val
734 def rename(self, uuid):
735 "Rename the VDI file"
736 assert(not self.sr.vdis.get(uuid))
737 self._clearRef()
738 oldUuid = self.uuid
739 self.uuid = uuid
740 self.children = []
741 # updating the children themselves is the responsibility of the caller
742 del self.sr.vdis[oldUuid]
743 self.sr.vdis[self.uuid] = self
745 def delete(self):
746 "Physically delete the VDI"
747 lock.Lock.cleanup(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
748 lock.Lock.cleanupAll(self.uuid)
749 self._clear()
751 def __str__(self):
752 strHidden = ""
753 if self.isHidden(): 753 ↛ 754line 753 didn't jump to line 754, because the condition on line 753 was never true
754 strHidden = "*"
755 strSizeVirt = "?"
756 if self.sizeVirt > 0: 756 ↛ 757line 756 didn't jump to line 757, because the condition on line 756 was never true
757 strSizeVirt = Util.num2str(self.sizeVirt)
758 strSizeVHD = "?"
759 if self._sizeVHD > 0: 759 ↛ 760line 759 didn't jump to line 760, because the condition on line 759 was never true
760 strSizeVHD = "/%s" % Util.num2str(self._sizeVHD)
761 strSizeAllocated = "?"
762 if self._sizeAllocated >= 0:
763 strSizeAllocated = "/%s" % Util.num2str(self._sizeAllocated)
764 strType = ""
765 if self.raw:
766 strType = "[RAW]"
767 strSizeVHD = ""
769 return "%s%s(%s%s%s)%s" % (strHidden, self.uuid[0:8], strSizeVirt,
770 strSizeVHD, strSizeAllocated, strType)
772 def validate(self, fast=False):
773 if not vhdutil.check(self.path, fast=fast): 773 ↛ 774line 773 didn't jump to line 774, because the condition on line 773 was never true
774 raise util.SMException("VHD %s corrupted" % self)
776 def _clear(self):
777 self.uuid = ""
778 self.path = ""
779 self.parentUuid = ""
780 self.parent = None
781 self._clearRef()
783 def _clearRef(self):
784 self._vdiRef = None
786 def _doCoalesce(self):
787 """Coalesce self onto parent. Only perform the actual coalescing of
788 VHD, but not the subsequent relinking. We'll do that as the next step,
789 after reloading the entire SR in case things have changed while we
790 were coalescing"""
791 self.validate()
792 self.parent.validate(True)
793 self.parent._increaseSizeVirt(self.sizeVirt)
794 self.sr._updateSlavesOnResize(self.parent)
795 self._coalesceVHD(0)
796 self.parent.validate(True)
797 #self._verifyContents(0)
798 self.parent.updateBlockInfo()
800 def _verifyContents(self, timeOut):
801 Util.log(" Coalesce verification on %s" % self)
802 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
803 Util.runAbortable(lambda: self._runTapdiskDiff(), True,
804 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
805 Util.log(" Coalesce verification succeeded")
807 def _runTapdiskDiff(self):
808 cmd = "tapdisk-diff -n %s:%s -m %s:%s" % \
809 (self.getDriverName(), self.path, \
810 self.parent.getDriverName(), self.parent.path)
811 Util.doexec(cmd, 0)
812 return True
814 def _reportCoalesceError(vdi, ce):
815 """Reports a coalesce error to XenCenter.
817 vdi: the VDI object on which the coalesce error occured
818 ce: the CommandException that was raised"""
820 msg_name = os.strerror(ce.code)
821 if ce.code == errno.ENOSPC:
822 # TODO We could add more information here, e.g. exactly how much
823 # space is required for the particular coalesce, as well as actions
824 # to be taken by the user and consequences of not taking these
825 # actions.
826 msg_body = 'Run out of space while coalescing.'
827 elif ce.code == errno.EIO:
828 msg_body = 'I/O error while coalescing.'
829 else:
830 msg_body = ''
831 util.SMlog('Coalesce failed on SR %s: %s (%s)'
832 % (vdi.sr.uuid, msg_name, msg_body))
834 # Create a XenCenter message, but don't spam.
835 xapi = vdi.sr.xapi.session.xenapi
836 sr_ref = xapi.SR.get_by_uuid(vdi.sr.uuid)
837 oth_cfg = xapi.SR.get_other_config(sr_ref)
838 if COALESCE_ERR_RATE_TAG in oth_cfg:
839 coalesce_err_rate = float(oth_cfg[COALESCE_ERR_RATE_TAG])
840 else:
841 coalesce_err_rate = DEFAULT_COALESCE_ERR_RATE
843 xcmsg = False
844 if coalesce_err_rate == 0:
845 xcmsg = True
846 elif coalesce_err_rate > 0:
847 now = datetime.datetime.now()
848 sm_cfg = xapi.SR.get_sm_config(sr_ref)
849 if COALESCE_LAST_ERR_TAG in sm_cfg:
850 # seconds per message (minimum distance in time between two
851 # messages in seconds)
852 spm = datetime.timedelta(seconds=(1.0 / coalesce_err_rate) * 60)
853 last = datetime.datetime.fromtimestamp(
854 float(sm_cfg[COALESCE_LAST_ERR_TAG]))
855 if now - last >= spm:
856 xapi.SR.remove_from_sm_config(sr_ref,
857 COALESCE_LAST_ERR_TAG)
858 xcmsg = True
859 else:
860 xcmsg = True
861 if xcmsg:
862 xapi.SR.add_to_sm_config(sr_ref, COALESCE_LAST_ERR_TAG,
863 str(now.strftime('%s')))
864 if xcmsg:
865 xapi.message.create(msg_name, "3", "SR", vdi.sr.uuid, msg_body)
866 _reportCoalesceError = staticmethod(_reportCoalesceError)
868 def _doCoalesceVHD(vdi):
869 try:
870 startTime = time.time()
871 vhdSize = vdi.getAllocatedSize()
872 # size is returned in sectors
873 coalesced_size = vhdutil.coalesce(vdi.path) * 512
874 endTime = time.time()
875 vdi.sr.recordStorageSpeed(startTime, endTime, coalesced_size)
876 except util.CommandException as ce:
877 # We use try/except for the following piece of code because it runs
878 # in a separate process context and errors will not be caught and
879 # reported by anyone.
880 try:
881 # Report coalesce errors back to user via XC
882 VDI._reportCoalesceError(vdi, ce)
883 except Exception as e:
884 util.SMlog('failed to create XenCenter message: %s' % e)
885 raise ce
886 except:
887 raise
888 _doCoalesceVHD = staticmethod(_doCoalesceVHD)
890 def _vdi_is_raw(self, vdi_path):
891 """
892 Given path to vdi determine if it is raw
893 """
894 uuid = self.extractUuid(vdi_path)
895 return self.sr.vdis[uuid].raw
897 def _coalesceVHD(self, timeOut):
898 Util.log(" Running VHD coalesce on %s" % self)
899 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 899 ↛ exitline 899 didn't run the lambda on line 899
900 try:
901 util.fistpoint.activate_custom_fn(
902 "cleanup_coalesceVHD_inject_failure",
903 util.inject_failure)
904 Util.runAbortable(lambda: VDI._doCoalesceVHD(self), None,
905 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
906 except:
907 #exception at this phase could indicate a failure in vhd coalesce
908 # or a kill of vhd coalesce by runAbortable due to timeOut
909 # Try a repair and reraise the exception
910 parent = ""
911 try:
912 parent = vhdutil.getParent(self.path, lambda x: x.strip()) 912 ↛ exitline 912 didn't run the lambda on line 912
913 if not self._vdi_is_raw(parent):
914 # Repair error is logged and ignored. Error reraised later
915 util.SMlog('Coalesce failed on %s, attempting repair on ' \
916 'parent %s' % (self.uuid, parent))
917 vhdutil.repair(parent)
918 except Exception as e:
919 util.SMlog('(error ignored) Failed to repair parent %s ' \
920 'after failed coalesce on %s, err: %s' %
921 (parent, self.path, e))
922 raise
924 util.fistpoint.activate("LVHDRT_coalescing_VHD_data", self.sr.uuid)
926 def _relinkSkip(self):
927 """Relink children of this VDI to point to the parent of this VDI"""
928 abortFlag = IPCFlag(self.sr.uuid)
929 for child in self.children:
930 if abortFlag.test(FLAG_TYPE_ABORT): 930 ↛ 931line 930 didn't jump to line 931, because the condition on line 930 was never true
931 raise AbortException("Aborting due to signal")
932 Util.log(" Relinking %s from %s to %s" % \
933 (child, self, self.parent))
934 util.fistpoint.activate("LVHDRT_relinking_grandchildren", self.sr.uuid)
935 child._setParent(self.parent)
936 self.children = []
938 def _reloadChildren(self, vdiSkip):
939 """Pause & unpause all VDIs in the subtree to cause blktap to reload
940 the VHD metadata for this file in any online VDI"""
941 abortFlag = IPCFlag(self.sr.uuid)
942 for child in self.children:
943 if child == vdiSkip:
944 continue
945 if abortFlag.test(FLAG_TYPE_ABORT): 945 ↛ 946line 945 didn't jump to line 946, because the condition on line 945 was never true
946 raise AbortException("Aborting due to signal")
947 Util.log(" Reloading VDI %s" % child)
948 child._reload()
950 def _reload(self):
951 """Pause & unpause to cause blktap to reload the VHD metadata"""
952 for child in self.children: 952 ↛ 953line 952 didn't jump to line 953, because the loop on line 952 never started
953 child._reload()
955 # only leaves can be attached
956 if len(self.children) == 0: 956 ↛ exitline 956 didn't return from function '_reload', because the condition on line 956 was never false
957 try:
958 self.delConfig(VDI.DB_VDI_RELINKING)
959 except XenAPI.Failure as e:
960 if not util.isInvalidVDI(e):
961 raise
962 self.refresh()
964 def _tagChildrenForRelink(self):
965 if len(self.children) == 0:
966 retries = 0
967 try:
968 while retries < 15:
969 retries += 1
970 if self.getConfig(VDI.DB_VDI_ACTIVATING) is not None:
971 Util.log("VDI %s is activating, wait to relink" %
972 self.uuid)
973 else:
974 self.setConfig(VDI.DB_VDI_RELINKING, "True")
976 if self.getConfig(VDI.DB_VDI_ACTIVATING):
977 self.delConfig(VDI.DB_VDI_RELINKING)
978 Util.log("VDI %s started activating while tagging" %
979 self.uuid)
980 else:
981 return
982 time.sleep(2)
984 raise util.SMException("Failed to tag vdi %s for relink" % self)
985 except XenAPI.Failure as e:
986 if not util.isInvalidVDI(e):
987 raise
989 for child in self.children:
990 child._tagChildrenForRelink()
992 def _loadInfoParent(self):
993 ret = vhdutil.getParent(self.path, lvhdutil.extractUuid)
994 if ret:
995 self.parentUuid = ret
997 def _setParent(self, parent):
998 vhdutil.setParent(self.path, parent.path, False)
999 self.parent = parent
1000 self.parentUuid = parent.uuid
1001 parent.children.append(self)
1002 try:
1003 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1004 Util.log("Updated the vhd-parent field for child %s with %s" % \
1005 (self.uuid, self.parentUuid))
1006 except:
1007 Util.log("Failed to update %s with vhd-parent field %s" % \
1008 (self.uuid, self.parentUuid))
1010 def _ensureParentActiveForRelink(self):
1011 pass
1013 def isHidden(self):
1014 if self._hidden is None: 1014 ↛ 1015line 1014 didn't jump to line 1015, because the condition on line 1014 was never true
1015 self._loadInfoHidden()
1016 return self._hidden
1018 def _loadInfoHidden(self):
1019 hidden = vhdutil.getHidden(self.path)
1020 self._hidden = (hidden != 0)
1022 def _setHidden(self, hidden=True):
1023 self._hidden = None
1024 vhdutil.setHidden(self.path, hidden)
1025 self._hidden = hidden
1027 def _increaseSizeVirt(self, size, atomic=True):
1028 """ensure the virtual size of 'self' is at least 'size'. Note that
1029 resizing a VHD must always be offline and atomically: the file must
1030 not be open by anyone and no concurrent operations may take place.
1031 Thus we use the Agent API call for performing paused atomic
1032 operations. If the caller is already in the atomic context, it must
1033 call with atomic = False"""
1034 if self.sizeVirt >= size: 1034 ↛ 1036line 1034 didn't jump to line 1036, because the condition on line 1034 was never false
1035 return
1036 Util.log(" Expanding VHD virt size for VDI %s: %s -> %s" % \
1037 (self, Util.num2str(self.sizeVirt), Util.num2str(size)))
1039 msize = vhdutil.getMaxResizeSize(self.path) * 1024 * 1024
1040 if (size <= msize):
1041 vhdutil.setSizeVirtFast(self.path, size)
1042 else:
1043 if atomic:
1044 vdiList = self._getAllSubtree()
1045 self.sr.lock()
1046 try:
1047 self.sr.pauseVDIs(vdiList)
1048 try:
1049 self._setSizeVirt(size)
1050 finally:
1051 self.sr.unpauseVDIs(vdiList)
1052 finally:
1053 self.sr.unlock()
1054 else:
1055 self._setSizeVirt(size)
1057 self.sizeVirt = vhdutil.getSizeVirt(self.path)
1059 def _setSizeVirt(self, size):
1060 """WARNING: do not call this method directly unless all VDIs in the
1061 subtree are guaranteed to be unplugged (and remain so for the duration
1062 of the operation): this operation is only safe for offline VHDs"""
1063 jFile = os.path.join(self.sr.path, self.uuid)
1064 vhdutil.setSizeVirt(self.path, size, jFile)
1066 def _queryVHDBlocks(self):
1067 return vhdutil.getBlockBitmap(self.path)
1069 def _getCoalescedSizeData(self):
1070 """Get the data size of the resulting VHD if we coalesce self onto
1071 parent. We calculate the actual size by using the VHD block allocation
1072 information (as opposed to just adding up the two VHD sizes to get an
1073 upper bound)"""
1074 # make sure we don't use stale BAT info from vdi_rec since the child
1075 # was writable all this time
1076 self.delConfig(VDI.DB_VHD_BLOCKS)
1077 blocksChild = self.getVHDBlocks()
1078 blocksParent = self.parent.getVHDBlocks()
1079 numBlocks = Util.countBits(blocksChild, blocksParent)
1080 Util.log("Num combined blocks = %d" % numBlocks)
1081 sizeData = numBlocks * vhdutil.VHD_BLOCK_SIZE
1082 assert(sizeData <= self.sizeVirt)
1083 return sizeData
1085 def _calcExtraSpaceForCoalescing(self):
1086 sizeData = self._getCoalescedSizeData()
1087 sizeCoalesced = sizeData + vhdutil.calcOverheadBitmap(sizeData) + \
1088 vhdutil.calcOverheadEmpty(self.sizeVirt)
1089 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1090 return sizeCoalesced - self.parent.getSizeVHD()
1092 def _calcExtraSpaceForLeafCoalescing(self):
1093 """How much extra space in the SR will be required to
1094 [live-]leaf-coalesce this VDI"""
1095 # the space requirements are the same as for inline coalesce
1096 return self._calcExtraSpaceForCoalescing()
1098 def _calcExtraSpaceForSnapshotCoalescing(self):
1099 """How much extra space in the SR will be required to
1100 snapshot-coalesce this VDI"""
1101 return self._calcExtraSpaceForCoalescing() + \
1102 vhdutil.calcOverheadEmpty(self.sizeVirt) # extra snap leaf
1104 def _getAllSubtree(self):
1105 """Get self and all VDIs in the subtree of self as a flat list"""
1106 vdiList = [self]
1107 for child in self.children:
1108 vdiList.extend(child._getAllSubtree())
1109 return vdiList
1112class FileVDI(VDI):
1113 """Object representing a VDI in a file-based SR (EXT or NFS)"""
1115 @staticmethod
1116 def extractUuid(path):
1117 path = os.path.basename(path.strip())
1118 if not (path.endswith(vhdutil.FILE_EXTN_VHD) or \ 1118 ↛ 1120line 1118 didn't jump to line 1120, because the condition on line 1118 was never true
1119 path.endswith(vhdutil.FILE_EXTN_RAW)):
1120 return None
1121 uuid = path.replace(vhdutil.FILE_EXTN_VHD, "").replace( \
1122 vhdutil.FILE_EXTN_RAW, "")
1123 # TODO: validate UUID format
1124 return uuid
1126 def __init__(self, sr, uuid, raw):
1127 VDI.__init__(self, sr, uuid, raw)
1128 if self.raw: 1128 ↛ 1129line 1128 didn't jump to line 1129, because the condition on line 1128 was never true
1129 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_RAW)
1130 else:
1131 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_VHD)
1133 def load(self, info=None):
1134 if not info:
1135 if not util.pathexists(self.path):
1136 raise util.SMException("%s not found" % self.path)
1137 try:
1138 info = vhdutil.getVHDInfo(self.path, self.extractUuid)
1139 except util.SMException:
1140 Util.log(" [VDI %s: failed to read VHD metadata]" % self.uuid)
1141 return
1142 self.parent = None
1143 self.children = []
1144 self.parentUuid = info.parentUuid
1145 self.sizeVirt = info.sizeVirt
1146 self._sizeVHD = info.sizePhys
1147 self._sizeAllocated = info.sizeAllocated
1148 self._hidden = info.hidden
1149 self.scanError = False
1150 self.path = os.path.join(self.sr.path, "%s%s" % \
1151 (self.uuid, vhdutil.FILE_EXTN_VHD))
1153 def rename(self, uuid):
1154 oldPath = self.path
1155 VDI.rename(self, uuid)
1156 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_VHD)
1157 self.path = os.path.join(self.sr.path, self.fileName)
1158 assert(not util.pathexists(self.path))
1159 Util.log("Renaming %s -> %s" % (oldPath, self.path))
1160 os.rename(oldPath, self.path)
1162 def delete(self):
1163 if len(self.children) > 0: 1163 ↛ 1164line 1163 didn't jump to line 1164, because the condition on line 1163 was never true
1164 raise util.SMException("VDI %s has children, can't delete" % \
1165 self.uuid)
1166 try:
1167 self.sr.lock()
1168 try:
1169 os.unlink(self.path)
1170 self.sr.forgetVDI(self.uuid)
1171 finally:
1172 self.sr.unlock()
1173 except OSError:
1174 raise util.SMException("os.unlink(%s) failed" % self.path)
1175 VDI.delete(self)
1177 def getAllocatedSize(self):
1178 if self._sizeAllocated == -1: 1178 ↛ 1179line 1178 didn't jump to line 1179, because the condition on line 1178 was never true
1179 self._sizeAllocated = vhdutil.getAllocatedSize(self.path)
1180 return self._sizeAllocated
1183class LVHDVDI(VDI):
1184 """Object representing a VDI in an LVHD SR"""
1186 JRN_ZERO = "zero" # journal entry type for zeroing out end of parent
1187 DRIVER_NAME_RAW = "aio"
1189 def load(self, vdiInfo):
1190 self.parent = None
1191 self.children = []
1192 self._sizeVHD = -1
1193 self._sizeAllocated = -1
1194 self.scanError = vdiInfo.scanError
1195 self.sizeLV = vdiInfo.sizeLV
1196 self.sizeVirt = vdiInfo.sizeVirt
1197 self.fileName = vdiInfo.lvName
1198 self.lvActive = vdiInfo.lvActive
1199 self.lvOpen = vdiInfo.lvOpen
1200 self.lvReadonly = vdiInfo.lvReadonly
1201 self._hidden = vdiInfo.hidden
1202 self.parentUuid = vdiInfo.parentUuid
1203 self.path = os.path.join(self.sr.path, self.fileName)
1205 @staticmethod
1206 def extractUuid(path):
1207 return lvhdutil.extractUuid(path)
1209 def getDriverName(self):
1210 if self.raw:
1211 return self.DRIVER_NAME_RAW
1212 return self.DRIVER_NAME_VHD
1214 def inflate(self, size):
1215 """inflate the LV containing the VHD to 'size'"""
1216 if self.raw:
1217 return
1218 self._activate()
1219 self.sr.lock()
1220 try:
1221 lvhdutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, size)
1222 util.fistpoint.activate("LVHDRT_inflating_the_parent", self.sr.uuid)
1223 finally:
1224 self.sr.unlock()
1225 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1226 self._sizeVHD = -1
1227 self._sizeAllocated = -1
1229 def deflate(self):
1230 """deflate the LV containing the VHD to minimum"""
1231 if self.raw:
1232 return
1233 self._activate()
1234 self.sr.lock()
1235 try:
1236 lvhdutil.deflate(self.sr.lvmCache, self.fileName, self.getSizeVHD())
1237 finally:
1238 self.sr.unlock()
1239 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1240 self._sizeVHD = -1
1241 self._sizeAllocated = -1
1243 def inflateFully(self):
1244 self.inflate(lvhdutil.calcSizeVHDLV(self.sizeVirt))
1246 def inflateParentForCoalesce(self):
1247 """Inflate the parent only as much as needed for the purposes of
1248 coalescing"""
1249 if self.parent.raw:
1250 return
1251 inc = self._calcExtraSpaceForCoalescing()
1252 if inc > 0:
1253 util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent", self.sr.uuid)
1254 self.parent.inflate(self.parent.sizeLV + inc)
1256 def updateBlockInfo(self):
1257 if not self.raw:
1258 return VDI.updateBlockInfo(self)
1260 def rename(self, uuid):
1261 oldUuid = self.uuid
1262 oldLVName = self.fileName
1263 VDI.rename(self, uuid)
1264 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + self.uuid
1265 if self.raw:
1266 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + self.uuid
1267 self.path = os.path.join(self.sr.path, self.fileName)
1268 assert(not self.sr.lvmCache.checkLV(self.fileName))
1270 self.sr.lvmCache.rename(oldLVName, self.fileName)
1271 if self.sr.lvActivator.get(oldUuid, False):
1272 self.sr.lvActivator.replace(oldUuid, self.uuid, self.fileName, False)
1274 ns = lvhdutil.NS_PREFIX_LVM + self.sr.uuid
1275 (cnt, bcnt) = RefCounter.check(oldUuid, ns)
1276 RefCounter.set(self.uuid, cnt, bcnt, ns)
1277 RefCounter.reset(oldUuid, ns)
1279 def delete(self):
1280 if len(self.children) > 0:
1281 raise util.SMException("VDI %s has children, can't delete" % \
1282 self.uuid)
1283 self.sr.lock()
1284 try:
1285 self.sr.lvmCache.remove(self.fileName)
1286 self.sr.forgetVDI(self.uuid)
1287 finally:
1288 self.sr.unlock()
1289 RefCounter.reset(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
1290 VDI.delete(self)
1292 def getSizeVHD(self):
1293 if self._sizeVHD == -1:
1294 self._loadInfoSizeVHD()
1295 return self._sizeVHD
1297 def _loadInfoSizeVHD(self):
1298 """Get the physical utilization of the VHD file. We do it individually
1299 (and not using the VHD batch scanner) as an optimization: this info is
1300 relatively expensive and we need it only for VDI's involved in
1301 coalescing."""
1302 if self.raw:
1303 return
1304 self._activate()
1305 self._sizeVHD = vhdutil.getSizePhys(self.path)
1306 if self._sizeVHD <= 0:
1307 raise util.SMException("phys size of %s = %d" % \
1308 (self, self._sizeVHD))
1310 def getAllocatedSize(self):
1311 if self._sizeAllocated == -1:
1312 self._loadInfoSizeAllocated()
1313 return self._sizeAllocated
1315 def _loadInfoSizeAllocated(self):
1316 """
1317 Get the allocated size of the VHD volume.
1318 """
1319 if self.raw:
1320 return
1321 self._activate()
1322 self._sizeAllocated = vhdutil.getAllocatedSize(self.path)
1324 def _loadInfoHidden(self):
1325 if self.raw:
1326 self._hidden = self.sr.lvmCache.getHidden(self.fileName)
1327 else:
1328 VDI._loadInfoHidden(self)
1330 def _setHidden(self, hidden=True):
1331 if self.raw:
1332 self._hidden = None
1333 self.sr.lvmCache.setHidden(self.fileName, hidden)
1334 self._hidden = hidden
1335 else:
1336 VDI._setHidden(self, hidden)
1338 def __str__(self):
1339 strType = "VHD"
1340 if self.raw:
1341 strType = "RAW"
1342 strHidden = ""
1343 if self.isHidden():
1344 strHidden = "*"
1345 strSizeVHD = ""
1346 if self._sizeVHD > 0:
1347 strSizeVHD = Util.num2str(self._sizeVHD)
1348 strSizeAllocated = ""
1349 if self._sizeAllocated >= 0:
1350 strSizeAllocated = Util.num2str(self._sizeAllocated)
1351 strActive = "n"
1352 if self.lvActive:
1353 strActive = "a"
1354 if self.lvOpen:
1355 strActive += "o"
1356 return "%s%s[%s](%s/%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType,
1357 Util.num2str(self.sizeVirt), strSizeVHD, strSizeAllocated,
1358 Util.num2str(self.sizeLV), strActive)
1360 def validate(self, fast=False):
1361 if not self.raw:
1362 VDI.validate(self, fast)
1364 def _doCoalesce(self):
1365 """LVHD parents must first be activated, inflated, and made writable"""
1366 try:
1367 self._activateChain()
1368 self.sr.lvmCache.setReadonly(self.parent.fileName, False)
1369 self.parent.validate()
1370 self.inflateParentForCoalesce()
1371 VDI._doCoalesce(self)
1372 finally:
1373 self.parent._loadInfoSizeVHD()
1374 self.parent.deflate()
1375 self.sr.lvmCache.setReadonly(self.parent.fileName, True)
1377 def _setParent(self, parent):
1378 self._activate()
1379 if self.lvReadonly:
1380 self.sr.lvmCache.setReadonly(self.fileName, False)
1382 try:
1383 vhdutil.setParent(self.path, parent.path, parent.raw)
1384 finally:
1385 if self.lvReadonly:
1386 self.sr.lvmCache.setReadonly(self.fileName, True)
1387 self._deactivate()
1388 self.parent = parent
1389 self.parentUuid = parent.uuid
1390 parent.children.append(self)
1391 try:
1392 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1393 Util.log("Updated the vhd-parent field for child %s with %s" % \
1394 (self.uuid, self.parentUuid))
1395 except:
1396 Util.log("Failed to update the vhd-parent with %s for child %s" % \
1397 (self.parentUuid, self.uuid))
1399 def _activate(self):
1400 self.sr.lvActivator.activate(self.uuid, self.fileName, False)
1402 def _activateChain(self):
1403 vdi = self
1404 while vdi:
1405 vdi._activate()
1406 vdi = vdi.parent
1408 def _deactivate(self):
1409 self.sr.lvActivator.deactivate(self.uuid, False)
1411 def _ensureParentActiveForRelink(self):
1412 self.parent._activate()
1414 def _increaseSizeVirt(self, size, atomic=True):
1415 "ensure the virtual size of 'self' is at least 'size'"
1416 self._activate()
1417 if not self.raw:
1418 VDI._increaseSizeVirt(self, size, atomic)
1419 return
1421 # raw VDI case
1422 offset = self.sizeLV
1423 if self.sizeVirt < size:
1424 oldSize = self.sizeLV
1425 self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size)
1426 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV))
1427 self.sr.lvmCache.setSize(self.fileName, self.sizeLV)
1428 offset = oldSize
1429 unfinishedZero = False
1430 jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid)
1431 if jval:
1432 unfinishedZero = True
1433 offset = int(jval)
1434 length = self.sizeLV - offset
1435 if not length:
1436 return
1438 if unfinishedZero:
1439 Util.log(" ==> Redoing unfinished zeroing out")
1440 else:
1441 self.sr.journaler.create(self.JRN_ZERO, self.uuid, \
1442 str(offset))
1443 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
1444 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1445 func = lambda: util.zeroOut(self.path, offset, length)
1446 Util.runAbortable(func, True, self.sr.uuid, abortTest,
1447 VDI.POLL_INTERVAL, 0)
1448 self.sr.journaler.remove(self.JRN_ZERO, self.uuid)
1450 def _setSizeVirt(self, size):
1451 """WARNING: do not call this method directly unless all VDIs in the
1452 subtree are guaranteed to be unplugged (and remain so for the duration
1453 of the operation): this operation is only safe for offline VHDs"""
1454 self._activate()
1455 jFile = lvhdutil.createVHDJournalLV(self.sr.lvmCache, self.uuid,
1456 vhdutil.MAX_VHD_JOURNAL_SIZE)
1457 try:
1458 lvhdutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid,
1459 size, jFile)
1460 finally:
1461 lvhdutil.deleteVHDJournalLV(self.sr.lvmCache, self.uuid)
1463 def _queryVHDBlocks(self):
1464 self._activate()
1465 return VDI._queryVHDBlocks(self)
1467 def _calcExtraSpaceForCoalescing(self):
1468 if self.parent.raw:
1469 return 0 # raw parents are never deflated in the first place
1470 sizeCoalesced = lvhdutil.calcSizeVHDLV(self._getCoalescedSizeData())
1471 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1472 return sizeCoalesced - self.parent.sizeLV
1474 def _calcExtraSpaceForLeafCoalescing(self):
1475 """How much extra space in the SR will be required to
1476 [live-]leaf-coalesce this VDI"""
1477 # we can deflate the leaf to minimize the space requirements
1478 deflateDiff = self.sizeLV - lvhdutil.calcSizeLV(self.getSizeVHD())
1479 return self._calcExtraSpaceForCoalescing() - deflateDiff
1481 def _calcExtraSpaceForSnapshotCoalescing(self):
1482 return self._calcExtraSpaceForCoalescing() + \
1483 lvhdutil.calcSizeLV(self.getSizeVHD())
1486################################################################################
1487#
1488# SR
1489#
1490class SR:
1491 class LogFilter:
1492 def __init__(self, sr):
1493 self.sr = sr
1494 self.stateLogged = False
1495 self.prevState = {}
1496 self.currState = {}
1498 def logState(self):
1499 changes = ""
1500 self.currState.clear()
1501 for vdi in self.sr.vdiTrees:
1502 self.currState[vdi.uuid] = self._getTreeStr(vdi)
1503 if not self.prevState.get(vdi.uuid) or \
1504 self.prevState[vdi.uuid] != self.currState[vdi.uuid]:
1505 changes += self.currState[vdi.uuid]
1507 for uuid in self.prevState:
1508 if not self.currState.get(uuid):
1509 changes += "Tree %s gone\n" % uuid
1511 result = "SR %s (%d VDIs in %d VHD trees): " % \
1512 (self.sr, len(self.sr.vdis), len(self.sr.vdiTrees))
1514 if len(changes) > 0:
1515 if self.stateLogged:
1516 result += "showing only VHD trees that changed:"
1517 result += "\n%s" % changes
1518 else:
1519 result += "no changes"
1521 for line in result.split("\n"):
1522 Util.log("%s" % line)
1523 self.prevState.clear()
1524 for key, val in self.currState.items():
1525 self.prevState[key] = val
1526 self.stateLogged = True
1528 def logNewVDI(self, uuid):
1529 if self.stateLogged:
1530 Util.log("Found new VDI when scanning: %s" % uuid)
1532 def _getTreeStr(self, vdi, indent=8):
1533 treeStr = "%s%s\n" % (" " * indent, vdi)
1534 for child in vdi.children:
1535 treeStr += self._getTreeStr(child, indent + VDI.STR_TREE_INDENT)
1536 return treeStr
1538 TYPE_FILE = "file"
1539 TYPE_LVHD = "lvhd"
1540 TYPES = [TYPE_LVHD, TYPE_FILE]
1542 LOCK_RETRY_INTERVAL = 3
1543 LOCK_RETRY_ATTEMPTS = 20
1544 LOCK_RETRY_ATTEMPTS_LOCK = 100
1546 SCAN_RETRY_ATTEMPTS = 3
1548 JRN_CLONE = "clone" # journal entry type for the clone operation (from SM)
1549 TMP_RENAME_PREFIX = "OLD_"
1551 KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline"
1552 KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override"
1554 def getInstance(uuid, xapiSession, createLock=True, force=False):
1555 xapi = XAPI(xapiSession, uuid)
1556 type = normalizeType(xapi.srRecord["type"])
1557 if type == SR.TYPE_FILE:
1558 return FileSR(uuid, xapi, createLock, force)
1559 elif type == SR.TYPE_LVHD:
1560 return LVHDSR(uuid, xapi, createLock, force)
1561 raise util.SMException("SR type %s not recognized" % type)
1562 getInstance = staticmethod(getInstance)
1564 def __init__(self, uuid, xapi, createLock, force):
1565 self.logFilter = self.LogFilter(self)
1566 self.uuid = uuid
1567 self.path = ""
1568 self.name = ""
1569 self.vdis = {}
1570 self.vdiTrees = []
1571 self.journaler = None
1572 self.xapi = xapi
1573 self._locked = 0
1574 self._srLock = None
1575 if createLock: 1575 ↛ 1576line 1575 didn't jump to line 1576, because the condition on line 1575 was never true
1576 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
1577 else:
1578 Util.log("Requested no SR locking")
1579 self.name = self.xapi.srRecord["name_label"]
1580 self._failedCoalesceTargets = []
1582 if not self.xapi.isPluggedHere():
1583 if force: 1583 ↛ 1584line 1583 didn't jump to line 1584, because the condition on line 1583 was never true
1584 Util.log("SR %s not attached on this host, ignoring" % uuid)
1585 else:
1586 if not self.wait_for_plug():
1587 raise util.SMException("SR %s not attached on this host" % uuid)
1589 if force: 1589 ↛ 1590line 1589 didn't jump to line 1590, because the condition on line 1589 was never true
1590 Util.log("Not checking if we are Master (SR %s)" % uuid)
1591 elif not self.xapi.isMaster(): 1591 ↛ 1592line 1591 didn't jump to line 1592, because the condition on line 1591 was never true
1592 raise util.SMException("This host is NOT master, will not run")
1594 self.no_space_candidates = {}
1596 def msg_cleared(self, xapi_session, msg_ref):
1597 try:
1598 msg = xapi_session.xenapi.message.get_record(msg_ref)
1599 except XenAPI.Failure:
1600 return True
1602 return msg is None
1604 def check_no_space_candidates(self):
1605 xapi_session = self.xapi.getSession()
1607 msg_id = self.xapi.srRecord["sm_config"].get(VDI.DB_GC_NO_SPACE)
1608 if self.no_space_candidates:
1609 if msg_id is None or self.msg_cleared(xapi_session, msg_id):
1610 util.SMlog("Could not coalesce due to a lack of space "
1611 f"in SR {self.uuid}")
1612 msg_body = ("Unable to perform data coalesce due to a lack "
1613 f"of space in SR {self.uuid}")
1614 msg_id = xapi_session.xenapi.message.create(
1615 'SM_GC_NO_SPACE',
1616 3,
1617 "SR",
1618 self.uuid,
1619 msg_body)
1620 xapi_session.xenapi.SR.remove_from_sm_config(
1621 self.xapi.srRef, VDI.DB_GC_NO_SPACE)
1622 xapi_session.xenapi.SR.add_to_sm_config(
1623 self.xapi.srRef, VDI.DB_GC_NO_SPACE, msg_id)
1625 for candidate in self.no_space_candidates.values():
1626 candidate.setConfig(VDI.DB_GC_NO_SPACE, msg_id)
1627 elif msg_id is not None:
1628 # Everything was coalescable, remove the message
1629 xapi_session.xenapi.SR.remove_from_sm_config(self.xapi.srRef, VDI.DB_GC_NO_SPACE)
1630 xapi_session.xenapi.message.destroy(msg_id)
1632 def clear_no_space_msg(self, vdi):
1633 msg_id = None
1634 try:
1635 msg_id = vdi.getConfig(VDI.DB_GC_NO_SPACE)
1636 except XenAPI.Failure:
1637 pass
1639 self.no_space_candidates.pop(vdi.uuid, None)
1640 if msg_id is not None: 1640 ↛ exitline 1640 didn't return from function 'clear_no_space_msg', because the condition on line 1640 was never false
1641 vdi.delConfig(VDI.DB_GC_NO_SPACE)
1644 def wait_for_plug(self):
1645 for _ in range(1, 10):
1646 time.sleep(2)
1647 if self.xapi.isPluggedHere():
1648 return True
1649 return False
1651 def gcEnabled(self, refresh=True):
1652 if refresh:
1653 self.xapi.srRecord = \
1654 self.xapi.session.xenapi.SR.get_record(self.xapi._srRef)
1655 if self.xapi.srRecord["other_config"].get(VDI.DB_GC) == "false":
1656 Util.log("GC is disabled for this SR, abort")
1657 return False
1658 return True
1660 def scan(self, force=False):
1661 """Scan the SR and load VDI info for each VDI. If called repeatedly,
1662 update VDI objects if they already exist"""
1663 pass # abstract
1665 def scanLocked(self, force=False):
1666 self.lock()
1667 try:
1668 self.scan(force)
1669 finally:
1670 self.unlock()
1672 def getVDI(self, uuid):
1673 return self.vdis.get(uuid)
1675 def hasWork(self):
1676 if len(self.findGarbage()) > 0:
1677 return True
1678 if self.findCoalesceable():
1679 return True
1680 if self.findLeafCoalesceable():
1681 return True
1682 if self.needUpdateBlockInfo():
1683 return True
1684 return False
1686 def findCoalesceable(self):
1687 """Find a coalesceable VDI. Return a vdi that should be coalesced
1688 (choosing one among all coalesceable candidates according to some
1689 criteria) or None if there is no VDI that could be coalesced"""
1691 candidates = []
1693 srSwitch = self.xapi.srRecord["other_config"].get(VDI.DB_COALESCE)
1694 if srSwitch == "false":
1695 Util.log("Coalesce disabled for this SR")
1696 return candidates
1698 # finish any VDI for which a relink journal entry exists first
1699 journals = self.journaler.getAll(VDI.JRN_RELINK)
1700 for uuid in journals:
1701 vdi = self.getVDI(uuid)
1702 if vdi and vdi not in self._failedCoalesceTargets:
1703 return vdi
1705 for vdi in self.vdis.values():
1706 if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets:
1707 candidates.append(vdi)
1708 Util.log("%s is coalescable" % vdi.uuid)
1710 self.xapi.update_task_progress("coalescable", len(candidates))
1712 # pick one in the tallest tree
1713 treeHeight = dict()
1714 for c in candidates:
1715 height = c.getTreeRoot().getTreeHeight()
1716 if treeHeight.get(height):
1717 treeHeight[height].append(c)
1718 else:
1719 treeHeight[height] = [c]
1721 freeSpace = self.getFreeSpace()
1722 heights = list(treeHeight.keys())
1723 heights.sort(reverse=True)
1724 for h in heights:
1725 for c in treeHeight[h]:
1726 spaceNeeded = c._calcExtraSpaceForCoalescing()
1727 if spaceNeeded <= freeSpace:
1728 Util.log("Coalesce candidate: %s (tree height %d)" % (c, h))
1729 self.clear_no_space_msg(c)
1730 return c
1731 else:
1732 self.no_space_candidates[c.uuid] = c
1733 Util.log("No space to coalesce %s (free space: %d)" % \
1734 (c, freeSpace))
1735 return None
1737 def getSwitch(self, key):
1738 return self.xapi.srRecord["other_config"].get(key)
1740 def forbiddenBySwitch(self, switch, condition, fail_msg):
1741 srSwitch = self.getSwitch(switch)
1742 ret = False
1743 if srSwitch:
1744 ret = srSwitch == condition
1746 if ret:
1747 Util.log(fail_msg)
1749 return ret
1751 def leafCoalesceForbidden(self):
1752 return (self.forbiddenBySwitch(VDI.DB_COALESCE,
1753 "false",
1754 "Coalesce disabled for this SR") or
1755 self.forbiddenBySwitch(VDI.DB_LEAFCLSC,
1756 VDI.LEAFCLSC_DISABLED,
1757 "Leaf-coalesce disabled for this SR"))
1759 def findLeafCoalesceable(self):
1760 """Find leaf-coalesceable VDIs in each VHD tree"""
1762 candidates = []
1763 if self.leafCoalesceForbidden():
1764 return candidates
1766 self.gatherLeafCoalesceable(candidates)
1768 self.xapi.update_task_progress("coalescable", len(candidates))
1770 freeSpace = self.getFreeSpace()
1771 for candidate in candidates:
1772 # check the space constraints to see if leaf-coalesce is actually
1773 # feasible for this candidate
1774 spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing()
1775 spaceNeededLive = spaceNeeded
1776 if spaceNeeded > freeSpace:
1777 spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing()
1778 if candidate.canLiveCoalesce(self.getStorageSpeed()):
1779 spaceNeeded = spaceNeededLive
1781 if spaceNeeded <= freeSpace:
1782 Util.log("Leaf-coalesce candidate: %s" % candidate)
1783 self.clear_no_space_msg(candidate)
1784 return candidate
1785 else:
1786 Util.log("No space to leaf-coalesce %s (free space: %d)" % \
1787 (candidate, freeSpace))
1788 if spaceNeededLive <= freeSpace:
1789 Util.log("...but enough space if skip snap-coalesce")
1790 candidate.setConfig(VDI.DB_LEAFCLSC,
1791 VDI.LEAFCLSC_OFFLINE)
1792 self.no_space_candidates[candidate.uuid] = candidate
1794 return None
1796 def gatherLeafCoalesceable(self, candidates):
1797 for vdi in self.vdis.values():
1798 if not vdi.isLeafCoalesceable():
1799 continue
1800 if vdi in self._failedCoalesceTargets:
1801 continue
1802 if vdi.getConfig(vdi.DB_ONBOOT) == vdi.ONBOOT_RESET:
1803 Util.log("Skipping reset-on-boot %s" % vdi)
1804 continue
1805 if vdi.getConfig(vdi.DB_ALLOW_CACHING):
1806 Util.log("Skipping allow_caching=true %s" % vdi)
1807 continue
1808 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED:
1809 Util.log("Leaf-coalesce disabled for %s" % vdi)
1810 continue
1811 if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or
1812 vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE):
1813 continue
1814 candidates.append(vdi)
1816 def coalesce(self, vdi, dryRun=False):
1817 """Coalesce vdi onto parent"""
1818 Util.log("Coalescing %s -> %s" % (vdi, vdi.parent))
1819 if dryRun: 1819 ↛ 1820line 1819 didn't jump to line 1820, because the condition on line 1819 was never true
1820 return
1822 try:
1823 self._coalesce(vdi)
1824 except util.SMException as e:
1825 if isinstance(e, AbortException): 1825 ↛ 1826line 1825 didn't jump to line 1826, because the condition on line 1825 was never true
1826 self.cleanup()
1827 raise
1828 else:
1829 self._failedCoalesceTargets.append(vdi)
1830 Util.logException("coalesce")
1831 Util.log("Coalesce failed, skipping")
1832 self.cleanup()
1834 def coalesceLeaf(self, vdi, dryRun=False):
1835 """Leaf-coalesce vdi onto parent"""
1836 Util.log("Leaf-coalescing %s -> %s" % (vdi, vdi.parent))
1837 if dryRun:
1838 return
1840 try:
1841 uuid = vdi.uuid
1842 try:
1843 # "vdi" object will no longer be valid after this call
1844 self._coalesceLeaf(vdi)
1845 finally:
1846 vdi = self.getVDI(uuid)
1847 if vdi:
1848 vdi.delConfig(vdi.DB_LEAFCLSC)
1849 except AbortException:
1850 self.cleanup()
1851 raise
1852 except (util.SMException, XenAPI.Failure) as e:
1853 self._failedCoalesceTargets.append(vdi)
1854 Util.logException("leaf-coalesce")
1855 Util.log("Leaf-coalesce failed on %s, skipping" % vdi)
1856 self.cleanup()
1858 def garbageCollect(self, dryRun=False):
1859 vdiList = self.findGarbage()
1860 Util.log("Found %d VDIs for deletion:" % len(vdiList))
1861 for vdi in vdiList:
1862 Util.log(" %s" % vdi)
1863 if not dryRun:
1864 self.deleteVDIs(vdiList)
1865 self.cleanupJournals(dryRun)
1867 def findGarbage(self):
1868 vdiList = []
1869 for vdi in self.vdiTrees:
1870 vdiList.extend(vdi.getAllPrunable())
1871 return vdiList
1873 def deleteVDIs(self, vdiList):
1874 for vdi in vdiList:
1875 if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT):
1876 raise AbortException("Aborting due to signal")
1877 Util.log("Deleting unlinked VDI %s" % vdi)
1878 self.deleteVDI(vdi)
1880 def deleteVDI(self, vdi):
1881 assert(len(vdi.children) == 0)
1882 del self.vdis[vdi.uuid]
1883 if vdi.parent: 1883 ↛ 1885line 1883 didn't jump to line 1885, because the condition on line 1883 was never false
1884 vdi.parent.children.remove(vdi)
1885 if vdi in self.vdiTrees: 1885 ↛ 1886line 1885 didn't jump to line 1886, because the condition on line 1885 was never true
1886 self.vdiTrees.remove(vdi)
1887 vdi.delete()
1889 def forgetVDI(self, vdiUuid):
1890 self.xapi.forgetVDI(self.uuid, vdiUuid)
1892 def pauseVDIs(self, vdiList):
1893 paused = []
1894 failed = False
1895 for vdi in vdiList:
1896 try:
1897 vdi.pause()
1898 paused.append(vdi)
1899 except:
1900 Util.logException("pauseVDIs")
1901 failed = True
1902 break
1904 if failed:
1905 self.unpauseVDIs(paused)
1906 raise util.SMException("Failed to pause VDIs")
1908 def unpauseVDIs(self, vdiList):
1909 failed = False
1910 for vdi in vdiList:
1911 try:
1912 vdi.unpause()
1913 except:
1914 Util.log("ERROR: Failed to unpause VDI %s" % vdi)
1915 failed = True
1916 if failed:
1917 raise util.SMException("Failed to unpause VDIs")
1919 def getFreeSpace(self):
1920 return 0
1922 def cleanup(self):
1923 Util.log("In cleanup")
1924 return
1926 def __str__(self):
1927 if self.name:
1928 ret = "%s ('%s')" % (self.uuid[0:4], self.name)
1929 else:
1930 ret = "%s" % self.uuid
1931 return ret
1933 def lock(self):
1934 """Acquire the SR lock. Nested acquire()'s are ok. Check for Abort
1935 signal to avoid deadlocking (trying to acquire the SR lock while the
1936 lock is held by a process that is trying to abort us)"""
1937 if not self._srLock:
1938 return
1940 if self._locked == 0:
1941 abortFlag = IPCFlag(self.uuid)
1942 for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK):
1943 if self._srLock.acquireNoblock():
1944 self._locked += 1
1945 return
1946 if abortFlag.test(FLAG_TYPE_ABORT):
1947 raise AbortException("Abort requested")
1948 time.sleep(SR.LOCK_RETRY_INTERVAL)
1949 raise util.SMException("Unable to acquire the SR lock")
1951 self._locked += 1
1953 def unlock(self):
1954 if not self._srLock: 1954 ↛ 1956line 1954 didn't jump to line 1956, because the condition on line 1954 was never false
1955 return
1956 assert(self._locked > 0)
1957 self._locked -= 1
1958 if self._locked == 0:
1959 self._srLock.release()
1961 def needUpdateBlockInfo(self):
1962 for vdi in self.vdis.values():
1963 if vdi.scanError or len(vdi.children) == 0:
1964 continue
1965 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
1966 return True
1967 return False
1969 def updateBlockInfo(self):
1970 for vdi in self.vdis.values():
1971 if vdi.scanError or len(vdi.children) == 0:
1972 continue
1973 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
1974 vdi.updateBlockInfo()
1976 def cleanupCoalesceJournals(self):
1977 """Remove stale coalesce VDI indicators"""
1978 entries = self.journaler.getAll(VDI.JRN_COALESCE)
1979 for uuid, jval in entries.items():
1980 self.journaler.remove(VDI.JRN_COALESCE, uuid)
1982 def cleanupJournals(self, dryRun=False):
1983 """delete journal entries for non-existing VDIs"""
1984 for t in [LVHDVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]:
1985 entries = self.journaler.getAll(t)
1986 for uuid, jval in entries.items():
1987 if self.getVDI(uuid):
1988 continue
1989 if t == SR.JRN_CLONE:
1990 baseUuid, clonUuid = jval.split("_")
1991 if self.getVDI(baseUuid):
1992 continue
1993 Util.log(" Deleting stale '%s' journal entry for %s "
1994 "(%s)" % (t, uuid, jval))
1995 if not dryRun:
1996 self.journaler.remove(t, uuid)
1998 def cleanupCache(self, maxAge=-1):
1999 return 0
2001 def _coalesce(self, vdi):
2002 if self.journaler.get(vdi.JRN_RELINK, vdi.uuid): 2002 ↛ 2005line 2002 didn't jump to line 2005, because the condition on line 2002 was never true
2003 # this means we had done the actual coalescing already and just
2004 # need to finish relinking and/or refreshing the children
2005 Util.log("==> Coalesce apparently already done: skipping")
2007 # The parent volume must be active for the parent change to occur.
2008 # The parent volume may become inactive if the host is rebooted.
2009 vdi._ensureParentActiveForRelink()
2010 else:
2011 # JRN_COALESCE is used to check which VDI is being coalesced in
2012 # order to decide whether to abort the coalesce. We remove the
2013 # journal as soon as the VHD coalesce step is done, because we
2014 # don't expect the rest of the process to take long
2015 self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1")
2016 vdi._doCoalesce()
2017 self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid)
2019 util.fistpoint.activate("LVHDRT_before_create_relink_journal", self.uuid)
2021 # we now need to relink the children: lock the SR to prevent ops
2022 # like SM.clone from manipulating the VDIs we'll be relinking and
2023 # rescan the SR first in case the children changed since the last
2024 # scan
2025 self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1")
2027 self.lock()
2028 try:
2029 vdi.parent._tagChildrenForRelink()
2030 self.scan()
2031 vdi._relinkSkip()
2032 finally:
2033 self.unlock()
2034 # Reload the children to leave things consistent
2035 vdi.parent._reloadChildren(vdi)
2037 self.journaler.remove(vdi.JRN_RELINK, vdi.uuid)
2038 self.deleteVDI(vdi)
2040 class CoalesceTracker:
2041 GRACE_ITERATIONS = 2
2042 MAX_ITERATIONS_NO_PROGRESS = 3
2043 MAX_ITERATIONS = 20
2044 MAX_INCREASE_FROM_MINIMUM = 1.2
2045 HISTORY_STRING = "Iteration: {its} -- Initial size {initSize}" \
2046 " --> Final size {finSize}"
2048 def __init__(self, sr):
2049 self.itsNoProgress = 0
2050 self.its = 0
2051 self.minSize = float("inf")
2052 self._history = []
2053 self.reason = ""
2054 self.startSize = None
2055 self.finishSize = None
2056 self.sr = sr
2057 self.grace_remaining = self.GRACE_ITERATIONS
2059 @property
2060 def history(self):
2061 return [x['msg'] for x in self._history]
2063 def moving_average(self):
2064 """
2065 Calculate a three point moving average
2066 """
2067 assert len(self._history) >= 3
2069 mv_average = sum([x['finalsize'] for x in self._history]) / len(self._history)
2070 util.SMlog(f'Calculated moving average as {mv_average}')
2071 return mv_average
2073 def abortCoalesce(self, prevSize, curSize):
2074 self.its += 1
2075 self._history.append(
2076 {
2077 'finalsize': curSize,
2078 'msg': self.HISTORY_STRING.format(its=self.its,
2079 initSize=prevSize,
2080 finSize=curSize)
2081 }
2082 )
2084 self.finishSize = curSize
2086 if self.startSize is None:
2087 self.startSize = prevSize
2089 if curSize < self.minSize:
2090 self.minSize = curSize
2092 if prevSize < self.minSize:
2093 self.minSize = prevSize
2095 if self.its < 4:
2096 # Perform at least three iterations
2097 return False
2099 if prevSize >= curSize or curSize < self.moving_average():
2100 # We made progress
2101 return False
2102 else:
2103 self.itsNoProgress += 1
2104 Util.log("No progress, attempt:"
2105 " {attempt}".format(attempt=self.itsNoProgress))
2106 util.fistpoint.activate("cleanup_tracker_no_progress", self.sr.uuid)
2108 if self.its > self.MAX_ITERATIONS:
2109 max = self.MAX_ITERATIONS
2110 self.reason = \
2111 "Max iterations ({max}) exceeded".format(max=max)
2112 return True
2114 if self.itsNoProgress > self.MAX_ITERATIONS_NO_PROGRESS:
2115 max = self.MAX_ITERATIONS_NO_PROGRESS
2116 self.reason = \
2117 "No progress made for {max} iterations".format(max=max)
2118 return True
2120 maxSizeFromMin = self.MAX_INCREASE_FROM_MINIMUM * self.minSize
2121 if curSize > maxSizeFromMin:
2122 self.grace_remaining -= 1
2123 if self.grace_remaining == 0:
2124 self.reason = "Unexpected bump in size," \
2125 " compared to minimum achieved"
2127 return True
2129 return False
2131 def printSizes(self):
2132 Util.log("Starting size was {size}"
2133 .format(size=self.startSize))
2134 Util.log("Final size was {size}"
2135 .format(size=self.finishSize))
2136 Util.log("Minimum size achieved was {size}"
2137 .format(size=self.minSize))
2139 def printReasoning(self):
2140 Util.log("Aborted coalesce")
2141 for hist in self.history:
2142 Util.log(hist)
2143 Util.log(self.reason)
2144 self.printSizes()
2146 def printSummary(self):
2147 if self.its == 0:
2148 return
2150 if self.reason: 2150 ↛ 2151line 2150 didn't jump to line 2151, because the condition on line 2150 was never true
2151 Util.log("Aborted coalesce")
2152 Util.log(self.reason)
2153 else:
2154 Util.log("Coalesce summary")
2156 Util.log(f"Performed {self.its} iterations")
2157 self.printSizes()
2160 def _coalesceLeaf(self, vdi):
2161 """Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot
2162 complete due to external changes, namely vdi_delete and vdi_snapshot
2163 that alter leaf-coalescibility of vdi"""
2164 tracker = self.CoalesceTracker(self)
2165 while not vdi.canLiveCoalesce(self.getStorageSpeed()):
2166 prevSizeVHD = vdi.getSizeVHD()
2167 if not self._snapshotCoalesce(vdi): 2167 ↛ 2168line 2167 didn't jump to line 2168, because the condition on line 2167 was never true
2168 return False
2169 if tracker.abortCoalesce(prevSizeVHD, vdi.getSizeVHD()):
2170 tracker.printReasoning()
2171 raise util.SMException("VDI {uuid} could not be coalesced"
2172 .format(uuid=vdi.uuid))
2173 tracker.printSummary()
2174 return self._liveLeafCoalesce(vdi)
2176 def calcStorageSpeed(self, startTime, endTime, vhdSize):
2177 speed = None
2178 total_time = endTime - startTime
2179 if total_time > 0:
2180 speed = float(vhdSize) / float(total_time)
2181 return speed
2183 def writeSpeedToFile(self, speed):
2184 content = []
2185 speedFile = None
2186 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2187 self.lock()
2188 try:
2189 Util.log("Writing to file: {myfile}".format(myfile=path))
2190 lines = ""
2191 if not os.path.isfile(path):
2192 lines = str(speed) + "\n"
2193 else:
2194 speedFile = open(path, "r+")
2195 content = speedFile.readlines()
2196 content.append(str(speed) + "\n")
2197 if len(content) > N_RUNNING_AVERAGE:
2198 del content[0]
2199 lines = "".join(content)
2201 util.atomicFileWrite(path, VAR_RUN, lines)
2202 finally:
2203 if speedFile is not None:
2204 speedFile.close()
2205 Util.log("Closing file: {myfile}".format(myfile=path))
2206 self.unlock()
2208 def recordStorageSpeed(self, startTime, endTime, vhdSize):
2209 speed = self.calcStorageSpeed(startTime, endTime, vhdSize)
2210 if speed is None:
2211 return
2213 self.writeSpeedToFile(speed)
2215 def getStorageSpeed(self):
2216 speedFile = None
2217 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2218 self.lock()
2219 try:
2220 speed = None
2221 if os.path.isfile(path):
2222 speedFile = open(path)
2223 content = speedFile.readlines()
2224 try:
2225 content = [float(i) for i in content]
2226 except ValueError:
2227 Util.log("Something bad in the speed log:{log}".
2228 format(log=speedFile.readlines()))
2229 return speed
2231 if len(content):
2232 speed = sum(content) / float(len(content))
2233 if speed <= 0: 2233 ↛ 2235line 2233 didn't jump to line 2235, because the condition on line 2233 was never true
2234 # Defensive, should be impossible.
2235 Util.log("Bad speed: {speed} calculated for SR: {uuid}".
2236 format(speed=speed, uuid=self.uuid))
2237 speed = None
2238 else:
2239 Util.log("Speed file empty for SR: {uuid}".
2240 format(uuid=self.uuid))
2241 else:
2242 Util.log("Speed log missing for SR: {uuid}".
2243 format(uuid=self.uuid))
2244 return speed
2245 finally:
2246 if not (speedFile is None):
2247 speedFile.close()
2248 self.unlock()
2250 def _snapshotCoalesce(self, vdi):
2251 # Note that because we are not holding any locks here, concurrent SM
2252 # operations may change this tree under our feet. In particular, vdi
2253 # can be deleted, or it can be snapshotted.
2254 assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED)
2255 Util.log("Single-snapshotting %s" % vdi)
2256 util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid)
2257 try:
2258 ret = self.xapi.singleSnapshotVDI(vdi)
2259 Util.log("Single-snapshot returned: %s" % ret)
2260 except XenAPI.Failure as e:
2261 if util.isInvalidVDI(e):
2262 Util.log("The VDI appears to have been concurrently deleted")
2263 return False
2264 raise
2265 self.scanLocked()
2266 tempSnap = vdi.parent
2267 if not tempSnap.isCoalesceable():
2268 Util.log("The VDI appears to have been concurrently snapshotted")
2269 return False
2270 Util.log("Coalescing parent %s" % tempSnap)
2271 util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid)
2272 vhdSize = vdi.getSizeVHD()
2273 self._coalesce(tempSnap)
2274 if not vdi.isLeafCoalesceable():
2275 Util.log("The VDI tree appears to have been altered since")
2276 return False
2277 return True
2279 def _liveLeafCoalesce(self, vdi):
2280 util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid)
2281 self.lock()
2282 try:
2283 self.scan()
2284 if not self.getVDI(vdi.uuid):
2285 Util.log("The VDI appears to have been deleted meanwhile")
2286 return False
2287 if not vdi.isLeafCoalesceable():
2288 Util.log("The VDI is no longer leaf-coalesceable")
2289 return False
2291 uuid = vdi.uuid
2292 vdi.pause(failfast=True)
2293 try:
2294 try:
2295 # "vdi" object will no longer be valid after this call
2296 self._doCoalesceLeaf(vdi)
2297 except:
2298 Util.logException("_doCoalesceLeaf")
2299 self._handleInterruptedCoalesceLeaf()
2300 raise
2301 finally:
2302 vdi = self.getVDI(uuid)
2303 if vdi:
2304 vdi.ensureUnpaused()
2305 vdiOld = self.getVDI(self.TMP_RENAME_PREFIX + uuid)
2306 if vdiOld:
2307 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2308 self.deleteVDI(vdiOld)
2309 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2310 finally:
2311 self.cleanup()
2312 self.unlock()
2313 self.logFilter.logState()
2314 return True
2316 def _doCoalesceLeaf(self, vdi):
2317 """Actual coalescing of a leaf VDI onto parent. Must be called in an
2318 offline/atomic context"""
2319 self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid)
2320 self._prepareCoalesceLeaf(vdi)
2321 vdi.parent._setHidden(False)
2322 vdi.parent._increaseSizeVirt(vdi.sizeVirt, False)
2323 vdi.validate(True)
2324 vdi.parent.validate(True)
2325 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
2326 timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT
2327 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE: 2327 ↛ 2328line 2327 didn't jump to line 2328, because the condition on line 2327 was never true
2328 Util.log("Leaf-coalesce forced, will not use timeout")
2329 timeout = 0
2330 vdi._coalesceVHD(timeout)
2331 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
2332 vdi.parent.validate(True)
2333 #vdi._verifyContents(timeout / 2)
2335 # rename
2336 vdiUuid = vdi.uuid
2337 oldName = vdi.fileName
2338 origParentUuid = vdi.parent.uuid
2339 vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid)
2340 util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid)
2341 vdi.parent.rename(vdiUuid)
2342 util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid)
2343 self._updateSlavesOnRename(vdi.parent, oldName, origParentUuid)
2345 # Note that "vdi.parent" is now the single remaining leaf and "vdi" is
2346 # garbage
2348 # update the VDI record
2349 if vdi.parent.raw: 2349 ↛ 2350line 2349 didn't jump to line 2350, because the condition on line 2349 was never true
2350 vdi.parent.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_RAW)
2351 vdi.parent.delConfig(VDI.DB_VHD_BLOCKS)
2352 util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid)
2354 self._updateNode(vdi)
2356 # delete the obsolete leaf & inflate the parent (in that order, to
2357 # minimize free space requirements)
2358 parent = vdi.parent
2359 vdi._setHidden(True)
2360 vdi.parent.children = []
2361 vdi.parent = None
2363 if parent.parent is None:
2364 parent.delConfig(VDI.DB_VHD_PARENT)
2366 extraSpace = self._calcExtraSpaceNeeded(vdi, parent)
2367 freeSpace = self.getFreeSpace()
2368 if freeSpace < extraSpace: 2368 ↛ 2371line 2368 didn't jump to line 2371, because the condition on line 2368 was never true
2369 # don't delete unless we need the space: deletion is time-consuming
2370 # because it requires contacting the slaves, and we're paused here
2371 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2372 self.deleteVDI(vdi)
2373 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2375 util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid)
2376 self.journaler.remove(VDI.JRN_LEAF, vdiUuid)
2378 self.forgetVDI(origParentUuid)
2379 self._finishCoalesceLeaf(parent)
2380 self._updateSlavesOnResize(parent)
2382 def _calcExtraSpaceNeeded(self, child, parent):
2383 assert(not parent.raw) # raw parents not supported
2384 extra = child.getSizeVHD() - parent.getSizeVHD()
2385 if extra < 0: 2385 ↛ 2386line 2385 didn't jump to line 2386, because the condition on line 2385 was never true
2386 extra = 0
2387 return extra
2389 def _prepareCoalesceLeaf(self, vdi):
2390 pass
2392 def _updateNode(self, vdi):
2393 pass
2395 def _finishCoalesceLeaf(self, parent):
2396 pass
2398 def _updateSlavesOnUndoLeafCoalesce(self, parent, child):
2399 pass
2401 def _updateSlavesOnRename(self, vdi, oldName, origParentUuid):
2402 pass
2404 def _updateSlavesOnResize(self, vdi):
2405 pass
2407 def _removeStaleVDIs(self, uuidsPresent):
2408 for uuid in list(self.vdis.keys()):
2409 if not uuid in uuidsPresent:
2410 Util.log("VDI %s disappeared since last scan" % \
2411 self.vdis[uuid])
2412 del self.vdis[uuid]
2414 def _handleInterruptedCoalesceLeaf(self):
2415 """An interrupted leaf-coalesce operation may leave the VHD tree in an
2416 inconsistent state. If the old-leaf VDI is still present, we revert the
2417 operation (in case the original error is persistent); otherwise we must
2418 finish the operation"""
2419 # abstract
2420 pass
2422 def _buildTree(self, force):
2423 self.vdiTrees = []
2424 for vdi in self.vdis.values():
2425 if vdi.parentUuid:
2426 parent = self.getVDI(vdi.parentUuid)
2427 if not parent:
2428 if vdi.uuid.startswith(self.TMP_RENAME_PREFIX):
2429 self.vdiTrees.append(vdi)
2430 continue
2431 if force:
2432 Util.log("ERROR: Parent VDI %s not found! (for %s)" % \
2433 (vdi.parentUuid, vdi.uuid))
2434 self.vdiTrees.append(vdi)
2435 continue
2436 else:
2437 raise util.SMException("Parent VDI %s of %s not " \
2438 "found" % (vdi.parentUuid, vdi.uuid))
2439 vdi.parent = parent
2440 parent.children.append(vdi)
2441 else:
2442 self.vdiTrees.append(vdi)
2445class FileSR(SR):
2446 TYPE = SR.TYPE_FILE
2447 CACHE_FILE_EXT = ".vhdcache"
2448 # cache cleanup actions
2449 CACHE_ACTION_KEEP = 0
2450 CACHE_ACTION_REMOVE = 1
2451 CACHE_ACTION_REMOVE_IF_INACTIVE = 2
2453 def __init__(self, uuid, xapi, createLock, force):
2454 SR.__init__(self, uuid, xapi, createLock, force)
2455 self.path = "/run/sr-mount/%s" % self.uuid
2456 self.journaler = fjournaler.Journaler(self.path)
2458 def scan(self, force=False):
2459 if not util.pathexists(self.path):
2460 raise util.SMException("directory %s not found!" % self.uuid)
2461 vhds = self._scan(force)
2462 for uuid, vhdInfo in vhds.items():
2463 vdi = self.getVDI(uuid)
2464 if not vdi:
2465 self.logFilter.logNewVDI(uuid)
2466 vdi = FileVDI(self, uuid, False)
2467 self.vdis[uuid] = vdi
2468 vdi.load(vhdInfo)
2469 uuidsPresent = list(vhds.keys())
2470 rawList = [x for x in os.listdir(self.path) if x.endswith(vhdutil.FILE_EXTN_RAW)]
2471 for rawName in rawList:
2472 uuid = FileVDI.extractUuid(rawName)
2473 uuidsPresent.append(uuid)
2474 vdi = self.getVDI(uuid)
2475 if not vdi:
2476 self.logFilter.logNewVDI(uuid)
2477 vdi = FileVDI(self, uuid, True)
2478 self.vdis[uuid] = vdi
2479 self._removeStaleVDIs(uuidsPresent)
2480 self._buildTree(force)
2481 self.logFilter.logState()
2482 self._handleInterruptedCoalesceLeaf()
2484 def getFreeSpace(self):
2485 return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path)
2487 def deleteVDIs(self, vdiList):
2488 rootDeleted = False
2489 for vdi in vdiList:
2490 if not vdi.parent:
2491 rootDeleted = True
2492 break
2493 SR.deleteVDIs(self, vdiList)
2494 if self.xapi.srRecord["type"] == "nfs" and rootDeleted:
2495 self.xapi.markCacheSRsDirty()
2497 def cleanupCache(self, maxAge=-1):
2498 """Clean up IntelliCache cache files. Caches for leaf nodes are
2499 removed when the leaf node no longer exists or its allow-caching
2500 attribute is not set. Caches for parent nodes are removed when the
2501 parent node no longer exists or it hasn't been used in more than
2502 <maxAge> hours.
2503 Return number of caches removed.
2504 """
2505 numRemoved = 0
2506 cacheFiles = [x for x in os.listdir(self.path) if self._isCacheFileName(x)]
2507 Util.log("Found %d cache files" % len(cacheFiles))
2508 cutoff = datetime.datetime.now() - datetime.timedelta(hours=maxAge)
2509 for cacheFile in cacheFiles:
2510 uuid = cacheFile[:-len(self.CACHE_FILE_EXT)]
2511 action = self.CACHE_ACTION_KEEP
2512 rec = self.xapi.getRecordVDI(uuid)
2513 if not rec:
2514 Util.log("Cache %s: VDI doesn't exist" % uuid)
2515 action = self.CACHE_ACTION_REMOVE
2516 elif rec["managed"] and not rec["allow_caching"]:
2517 Util.log("Cache %s: caching disabled" % uuid)
2518 action = self.CACHE_ACTION_REMOVE
2519 elif not rec["managed"] and maxAge >= 0:
2520 lastAccess = datetime.datetime.fromtimestamp( \
2521 os.path.getatime(os.path.join(self.path, cacheFile)))
2522 if lastAccess < cutoff:
2523 Util.log("Cache %s: older than %d hrs" % (uuid, maxAge))
2524 action = self.CACHE_ACTION_REMOVE_IF_INACTIVE
2526 if action == self.CACHE_ACTION_KEEP:
2527 Util.log("Keeping cache %s" % uuid)
2528 continue
2530 lockId = uuid
2531 parentUuid = None
2532 if rec and rec["managed"]:
2533 parentUuid = rec["sm_config"].get("vhd-parent")
2534 if parentUuid:
2535 lockId = parentUuid
2537 cacheLock = lock.Lock(blktap2.VDI.LOCK_CACHE_SETUP, lockId)
2538 cacheLock.acquire()
2539 try:
2540 if self._cleanupCache(uuid, action):
2541 numRemoved += 1
2542 finally:
2543 cacheLock.release()
2544 return numRemoved
2546 def _cleanupCache(self, uuid, action):
2547 assert(action != self.CACHE_ACTION_KEEP)
2548 rec = self.xapi.getRecordVDI(uuid)
2549 if rec and rec["allow_caching"]:
2550 Util.log("Cache %s appears to have become valid" % uuid)
2551 return False
2553 fullPath = os.path.join(self.path, uuid + self.CACHE_FILE_EXT)
2554 tapdisk = blktap2.Tapdisk.find_by_path(fullPath)
2555 if tapdisk:
2556 if action == self.CACHE_ACTION_REMOVE_IF_INACTIVE:
2557 Util.log("Cache %s still in use" % uuid)
2558 return False
2559 Util.log("Shutting down tapdisk for %s" % fullPath)
2560 tapdisk.shutdown()
2562 Util.log("Deleting file %s" % fullPath)
2563 os.unlink(fullPath)
2564 return True
2566 def _isCacheFileName(self, name):
2567 return (len(name) == Util.UUID_LEN + len(self.CACHE_FILE_EXT)) and \
2568 name.endswith(self.CACHE_FILE_EXT)
2570 def _scan(self, force):
2571 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2572 error = False
2573 pattern = os.path.join(self.path, "*%s" % vhdutil.FILE_EXTN_VHD)
2574 vhds = vhdutil.getAllVHDs(pattern, FileVDI.extractUuid)
2575 for uuid, vhdInfo in vhds.items():
2576 if vhdInfo.error:
2577 error = True
2578 break
2579 if not error:
2580 return vhds
2581 Util.log("Scan error on attempt %d" % i)
2582 if force:
2583 return vhds
2584 raise util.SMException("Scan error")
2586 def deleteVDI(self, vdi):
2587 self._checkSlaves(vdi)
2588 SR.deleteVDI(self, vdi)
2590 def _checkSlaves(self, vdi):
2591 onlineHosts = self.xapi.getOnlineHosts()
2592 abortFlag = IPCFlag(self.uuid)
2593 for pbdRecord in self.xapi.getAttachedPBDs():
2594 hostRef = pbdRecord["host"]
2595 if hostRef == self.xapi._hostRef:
2596 continue
2597 if abortFlag.test(FLAG_TYPE_ABORT):
2598 raise AbortException("Aborting due to signal")
2599 try:
2600 self._checkSlave(hostRef, vdi)
2601 except util.CommandException:
2602 if hostRef in onlineHosts:
2603 raise
2605 def _checkSlave(self, hostRef, vdi):
2606 call = (hostRef, "nfs-on-slave", "check", {'path': vdi.path})
2607 Util.log("Checking with slave: %s" % repr(call))
2608 _host = self.xapi.session.xenapi.host
2609 text = _host.call_plugin( * call)
2611 def _handleInterruptedCoalesceLeaf(self):
2612 entries = self.journaler.getAll(VDI.JRN_LEAF)
2613 for uuid, parentUuid in entries.items():
2614 fileList = os.listdir(self.path)
2615 childName = uuid + vhdutil.FILE_EXTN_VHD
2616 tmpChildName = self.TMP_RENAME_PREFIX + uuid + vhdutil.FILE_EXTN_VHD
2617 parentName1 = parentUuid + vhdutil.FILE_EXTN_VHD
2618 parentName2 = parentUuid + vhdutil.FILE_EXTN_RAW
2619 parentPresent = (parentName1 in fileList or parentName2 in fileList)
2620 if parentPresent or tmpChildName in fileList:
2621 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
2622 else:
2623 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
2624 self.journaler.remove(VDI.JRN_LEAF, uuid)
2625 vdi = self.getVDI(uuid)
2626 if vdi:
2627 vdi.ensureUnpaused()
2629 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2630 Util.log("*** UNDO LEAF-COALESCE")
2631 parent = self.getVDI(parentUuid)
2632 if not parent:
2633 parent = self.getVDI(childUuid)
2634 if not parent:
2635 raise util.SMException("Neither %s nor %s found" % \
2636 (parentUuid, childUuid))
2637 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
2638 parent.rename(parentUuid)
2639 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
2641 child = self.getVDI(childUuid)
2642 if not child:
2643 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
2644 if not child:
2645 raise util.SMException("Neither %s nor %s found" % \
2646 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
2647 Util.log("Renaming child back to %s" % childUuid)
2648 child.rename(childUuid)
2649 Util.log("Updating the VDI record")
2650 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
2651 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
2652 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
2654 if child.isHidden():
2655 child._setHidden(False)
2656 if not parent.isHidden():
2657 parent._setHidden(True)
2658 self._updateSlavesOnUndoLeafCoalesce(parent, child)
2659 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
2660 Util.log("*** leaf-coalesce undo successful")
2661 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
2662 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
2664 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2665 Util.log("*** FINISH LEAF-COALESCE")
2666 vdi = self.getVDI(childUuid)
2667 if not vdi:
2668 Util.log(f"_finishInterruptedCoalesceLeaf, vdi {childUuid} not found, aborting")
2669 raise util.SMException("VDI %s not found" % childUuid)
2670 try:
2671 self.forgetVDI(parentUuid)
2672 except XenAPI.Failure:
2673 Util.logException('_finishInterruptedCoalesceLeaf')
2674 pass
2675 self._updateSlavesOnResize(vdi)
2676 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
2677 Util.log("*** finished leaf-coalesce successfully")
2680class LVHDSR(SR):
2681 TYPE = SR.TYPE_LVHD
2682 SUBTYPES = ["lvhdoiscsi", "lvhdohba"]
2684 def __init__(self, uuid, xapi, createLock, force):
2685 SR.__init__(self, uuid, xapi, createLock, force)
2686 self.vgName = "%s%s" % (lvhdutil.VG_PREFIX, self.uuid)
2687 self.path = os.path.join(lvhdutil.VG_LOCATION, self.vgName)
2688 self.lvmCache = lvmcache.LVMCache(self.vgName)
2689 self.lvActivator = LVActivator(self.uuid, self.lvmCache)
2690 self.journaler = journaler.Journaler(self.lvmCache)
2692 def deleteVDI(self, vdi):
2693 if self.lvActivator.get(vdi.uuid, False):
2694 self.lvActivator.deactivate(vdi.uuid, False)
2695 self._checkSlaves(vdi)
2696 SR.deleteVDI(self, vdi)
2698 def forgetVDI(self, vdiUuid):
2699 SR.forgetVDI(self, vdiUuid)
2700 mdpath = os.path.join(self.path, lvutil.MDVOLUME_NAME)
2701 LVMMetadataHandler(mdpath).deleteVdiFromMetadata(vdiUuid)
2703 def getFreeSpace(self):
2704 stats = lvutil._getVGstats(self.vgName)
2705 return stats['physical_size'] - stats['physical_utilisation']
2707 def cleanup(self):
2708 if not self.lvActivator.deactivateAll():
2709 Util.log("ERROR deactivating LVs while cleaning up")
2711 def needUpdateBlockInfo(self):
2712 for vdi in self.vdis.values():
2713 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2714 continue
2715 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2716 return True
2717 return False
2719 def updateBlockInfo(self):
2720 numUpdated = 0
2721 for vdi in self.vdis.values():
2722 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2723 continue
2724 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2725 vdi.updateBlockInfo()
2726 numUpdated += 1
2727 if numUpdated:
2728 # deactivate the LVs back sooner rather than later. If we don't
2729 # now, by the time this thread gets to deactivations, another one
2730 # might have leaf-coalesced a node and deleted it, making the child
2731 # inherit the refcount value and preventing the correct decrement
2732 self.cleanup()
2734 def scan(self, force=False):
2735 vdis = self._scan(force)
2736 for uuid, vdiInfo in vdis.items():
2737 vdi = self.getVDI(uuid)
2738 if not vdi:
2739 self.logFilter.logNewVDI(uuid)
2740 vdi = LVHDVDI(self, uuid,
2741 vdiInfo.vdiType == vhdutil.VDI_TYPE_RAW)
2742 self.vdis[uuid] = vdi
2743 vdi.load(vdiInfo)
2744 self._removeStaleVDIs(vdis.keys())
2745 self._buildTree(force)
2746 self.logFilter.logState()
2747 self._handleInterruptedCoalesceLeaf()
2749 def _scan(self, force):
2750 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2751 error = False
2752 self.lvmCache.refresh()
2753 vdis = lvhdutil.getVDIInfo(self.lvmCache)
2754 for uuid, vdiInfo in vdis.items():
2755 if vdiInfo.scanError:
2756 error = True
2757 break
2758 if not error:
2759 return vdis
2760 Util.log("Scan error, retrying (%d)" % i)
2761 if force:
2762 return vdis
2763 raise util.SMException("Scan error")
2765 def _removeStaleVDIs(self, uuidsPresent):
2766 for uuid in list(self.vdis.keys()):
2767 if not uuid in uuidsPresent:
2768 Util.log("VDI %s disappeared since last scan" % \
2769 self.vdis[uuid])
2770 del self.vdis[uuid]
2771 if self.lvActivator.get(uuid, False):
2772 self.lvActivator.remove(uuid, False)
2774 def _liveLeafCoalesce(self, vdi):
2775 """If the parent is raw and the child was resized (virt. size), then
2776 we'll need to resize the parent, which can take a while due to zeroing
2777 out of the extended portion of the LV. Do it before pausing the child
2778 to avoid a protracted downtime"""
2779 if vdi.parent.raw and vdi.sizeVirt > vdi.parent.sizeVirt:
2780 self.lvmCache.setReadonly(vdi.parent.fileName, False)
2781 vdi.parent._increaseSizeVirt(vdi.sizeVirt)
2783 return SR._liveLeafCoalesce(self, vdi)
2785 def _prepareCoalesceLeaf(self, vdi):
2786 vdi._activateChain()
2787 self.lvmCache.setReadonly(vdi.parent.fileName, False)
2788 vdi.deflate()
2789 vdi.inflateParentForCoalesce()
2791 def _updateNode(self, vdi):
2792 # fix the refcounts: the remaining node should inherit the binary
2793 # refcount from the leaf (because if it was online, it should remain
2794 # refcounted as such), but the normal refcount from the parent (because
2795 # this node is really the parent node) - minus 1 if it is online (since
2796 # non-leaf nodes increment their normal counts when they are online and
2797 # we are now a leaf, storing that 1 in the binary refcount).
2798 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
2799 cCnt, cBcnt = RefCounter.check(vdi.uuid, ns)
2800 pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns)
2801 pCnt = pCnt - cBcnt
2802 assert(pCnt >= 0)
2803 RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns)
2805 def _finishCoalesceLeaf(self, parent):
2806 if not parent.isSnapshot() or parent.isAttachedRW():
2807 parent.inflateFully()
2808 else:
2809 parent.deflate()
2811 def _calcExtraSpaceNeeded(self, child, parent):
2812 return lvhdutil.calcSizeVHDLV(parent.sizeVirt) - parent.sizeLV
2814 def _handleInterruptedCoalesceLeaf(self):
2815 entries = self.journaler.getAll(VDI.JRN_LEAF)
2816 for uuid, parentUuid in entries.items():
2817 childLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + uuid
2818 tmpChildLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
2819 self.TMP_RENAME_PREFIX + uuid
2820 parentLV1 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + parentUuid
2821 parentLV2 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + parentUuid
2822 parentPresent = (self.lvmCache.checkLV(parentLV1) or \
2823 self.lvmCache.checkLV(parentLV2))
2824 if parentPresent or self.lvmCache.checkLV(tmpChildLV):
2825 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
2826 else:
2827 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
2828 self.journaler.remove(VDI.JRN_LEAF, uuid)
2829 vdi = self.getVDI(uuid)
2830 if vdi:
2831 vdi.ensureUnpaused()
2833 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2834 Util.log("*** UNDO LEAF-COALESCE")
2835 parent = self.getVDI(parentUuid)
2836 if not parent:
2837 parent = self.getVDI(childUuid)
2838 if not parent:
2839 raise util.SMException("Neither %s nor %s found" % \
2840 (parentUuid, childUuid))
2841 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
2842 parent.rename(parentUuid)
2843 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
2845 child = self.getVDI(childUuid)
2846 if not child:
2847 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
2848 if not child:
2849 raise util.SMException("Neither %s nor %s found" % \
2850 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
2851 Util.log("Renaming child back to %s" % childUuid)
2852 child.rename(childUuid)
2853 Util.log("Updating the VDI record")
2854 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
2855 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
2856 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
2858 # refcount (best effort - assume that it had succeeded if the
2859 # second rename succeeded; if not, this adjustment will be wrong,
2860 # leading to a non-deactivation of the LV)
2861 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
2862 cCnt, cBcnt = RefCounter.check(child.uuid, ns)
2863 pCnt, pBcnt = RefCounter.check(parent.uuid, ns)
2864 pCnt = pCnt + cBcnt
2865 RefCounter.set(parent.uuid, pCnt, 0, ns)
2866 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid)
2868 parent.deflate()
2869 child.inflateFully()
2870 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid)
2871 if child.isHidden():
2872 child._setHidden(False)
2873 if not parent.isHidden():
2874 parent._setHidden(True)
2875 if not parent.lvReadonly:
2876 self.lvmCache.setReadonly(parent.fileName, True)
2877 self._updateSlavesOnUndoLeafCoalesce(parent, child)
2878 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
2879 Util.log("*** leaf-coalesce undo successful")
2880 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
2881 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
2883 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2884 Util.log("*** FINISH LEAF-COALESCE")
2885 vdi = self.getVDI(childUuid)
2886 if not vdi:
2887 raise util.SMException("VDI %s not found" % childUuid)
2888 vdi.inflateFully()
2889 util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid)
2890 try:
2891 self.forgetVDI(parentUuid)
2892 except XenAPI.Failure:
2893 pass
2894 self._updateSlavesOnResize(vdi)
2895 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
2896 Util.log("*** finished leaf-coalesce successfully")
2898 def _checkSlaves(self, vdi):
2899 """Confirm with all slaves in the pool that 'vdi' is not in use. We
2900 try to check all slaves, including those that the Agent believes are
2901 offline, but ignore failures for offline hosts. This is to avoid cases
2902 where the Agent thinks a host is offline but the host is up."""
2903 args = {"vgName": self.vgName,
2904 "action1": "deactivateNoRefcount",
2905 "lvName1": vdi.fileName,
2906 "action2": "cleanupLockAndRefcount",
2907 "uuid2": vdi.uuid,
2908 "ns2": lvhdutil.NS_PREFIX_LVM + self.uuid}
2909 onlineHosts = self.xapi.getOnlineHosts()
2910 abortFlag = IPCFlag(self.uuid)
2911 for pbdRecord in self.xapi.getAttachedPBDs():
2912 hostRef = pbdRecord["host"]
2913 if hostRef == self.xapi._hostRef:
2914 continue
2915 if abortFlag.test(FLAG_TYPE_ABORT):
2916 raise AbortException("Aborting due to signal")
2917 Util.log("Checking with slave %s (path %s)" % (
2918 self.xapi.getRecordHost(hostRef)['hostname'], vdi.path))
2919 try:
2920 self.xapi.ensureInactive(hostRef, args)
2921 except XenAPI.Failure:
2922 if hostRef in onlineHosts:
2923 raise
2925 def _updateSlavesOnUndoLeafCoalesce(self, parent, child):
2926 slaves = util.get_slaves_attached_on(self.xapi.session, [child.uuid])
2927 if not slaves:
2928 Util.log("Update-on-leaf-undo: VDI %s not attached on any slave" % \
2929 child)
2930 return
2932 tmpName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
2933 self.TMP_RENAME_PREFIX + child.uuid
2934 args = {"vgName": self.vgName,
2935 "action1": "deactivateNoRefcount",
2936 "lvName1": tmpName,
2937 "action2": "deactivateNoRefcount",
2938 "lvName2": child.fileName,
2939 "action3": "refresh",
2940 "lvName3": child.fileName,
2941 "action4": "refresh",
2942 "lvName4": parent.fileName}
2943 for slave in slaves:
2944 Util.log("Updating %s, %s, %s on slave %s" % \
2945 (tmpName, child.fileName, parent.fileName,
2946 self.xapi.getRecordHost(slave)['hostname']))
2947 text = self.xapi.session.xenapi.host.call_plugin( \
2948 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
2949 Util.log("call-plugin returned: '%s'" % text)
2951 def _updateSlavesOnRename(self, vdi, oldNameLV, origParentUuid):
2952 slaves = util.get_slaves_attached_on(self.xapi.session, [vdi.uuid])
2953 if not slaves:
2954 Util.log("Update-on-rename: VDI %s not attached on any slave" % vdi)
2955 return
2957 args = {"vgName": self.vgName,
2958 "action1": "deactivateNoRefcount",
2959 "lvName1": oldNameLV,
2960 "action2": "refresh",
2961 "lvName2": vdi.fileName,
2962 "action3": "cleanupLockAndRefcount",
2963 "uuid3": origParentUuid,
2964 "ns3": lvhdutil.NS_PREFIX_LVM + self.uuid}
2965 for slave in slaves:
2966 Util.log("Updating %s to %s on slave %s" % \
2967 (oldNameLV, vdi.fileName,
2968 self.xapi.getRecordHost(slave)['hostname']))
2969 text = self.xapi.session.xenapi.host.call_plugin( \
2970 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
2971 Util.log("call-plugin returned: '%s'" % text)
2973 def _updateSlavesOnResize(self, vdi):
2974 uuids = [x.uuid for x in vdi.getAllLeaves()]
2975 slaves = util.get_slaves_attached_on(self.xapi.session, uuids)
2976 if not slaves:
2977 util.SMlog("Update-on-resize: %s not attached on any slave" % vdi)
2978 return
2979 lvhdutil.lvRefreshOnSlaves(self.xapi.session, self.uuid, self.vgName,
2980 vdi.fileName, vdi.uuid, slaves)
2983################################################################################
2984#
2985# Helpers
2986#
2987def daemonize():
2988 pid = os.fork()
2989 if pid:
2990 os.waitpid(pid, 0)
2991 Util.log("New PID [%d]" % pid)
2992 return False
2993 os.chdir("/")
2994 os.setsid()
2995 pid = os.fork()
2996 if pid:
2997 Util.log("Will finish as PID [%d]" % pid)
2998 os._exit(0)
2999 for fd in [0, 1, 2]:
3000 try:
3001 os.close(fd)
3002 except OSError:
3003 pass
3004 # we need to fill those special fd numbers or pread won't work
3005 sys.stdin = open("/dev/null", 'r')
3006 sys.stderr = open("/dev/null", 'w')
3007 sys.stdout = open("/dev/null", 'w')
3008 # As we're a new process we need to clear the lock objects
3009 lock.Lock.clearAll()
3010 return True
3013def normalizeType(type):
3014 if type in LVHDSR.SUBTYPES:
3015 type = SR.TYPE_LVHD
3016 if type in ["lvm", "lvmoiscsi", "lvmohba"]:
3017 # temporary while LVHD is symlinked as LVM
3018 type = SR.TYPE_LVHD
3019 if type in ["ext", "nfs", "ocfsoiscsi", "ocfsohba", "smb"]:
3020 type = SR.TYPE_FILE
3021 if not type in SR.TYPES:
3022 raise util.SMException("Unsupported SR type: %s" % type)
3023 return type
3025GCPAUSE_DEFAULT_SLEEP = 5 * 60
3028def _gc_init_file(sr_uuid):
3029 return os.path.join(NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init')
3032def _create_init_file(sr_uuid):
3033 util.makedirs(os.path.join(NON_PERSISTENT_DIR, str(sr_uuid)))
3034 with open(os.path.join(
3035 NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init'), 'w+') as f:
3036 f.write('1')
3039def _gcLoopPause(sr, dryRun=False, immediate=False):
3040 if immediate:
3041 return
3043 # Check to see if the GCPAUSE_FISTPOINT is present. If so the fist
3044 # point will just return. Otherwise, fall back on an abortable sleep.
3046 if util.fistpoint.is_active(util.GCPAUSE_FISTPOINT):
3048 util.fistpoint.activate_custom_fn(util.GCPAUSE_FISTPOINT, 3048 ↛ exitline 3048 didn't jump to the function exit
3049 lambda *args: None)
3050 elif os.path.exists(_gc_init_file(sr.uuid)):
3051 def abortTest():
3052 return IPCFlag(sr.uuid).test(FLAG_TYPE_ABORT)
3054 # If time.sleep hangs we are in deep trouble, however for
3055 # completeness we set the timeout of the abort thread to
3056 # 110% of GCPAUSE_DEFAULT_SLEEP.
3057 Util.log("GC active, about to go quiet")
3058 Util.runAbortable(lambda: time.sleep(GCPAUSE_DEFAULT_SLEEP), 3058 ↛ exitline 3058 didn't run the lambda on line 3058
3059 None, sr.uuid, abortTest, VDI.POLL_INTERVAL,
3060 GCPAUSE_DEFAULT_SLEEP * 1.1)
3061 Util.log("GC active, quiet period ended")
3064def _gcLoop(sr, dryRun=False, immediate=False):
3065 if not lockGCActive.acquireNoblock(): 3065 ↛ 3066line 3065 didn't jump to line 3066, because the condition on line 3065 was never true
3066 Util.log("Another GC instance already active, exiting")
3067 return
3069 # Check we're still attached after acquiring locks
3070 if not sr.xapi.isPluggedHere():
3071 Util.log("SR no longer attached, exiting")
3072 return
3074 # Clean up Intellicache files
3075 sr.cleanupCache()
3077 # Track how many we do
3078 coalesced = 0
3079 task_status = "success"
3080 try:
3081 # Check if any work needs to be done
3082 if not sr.xapi.isPluggedHere(): 3082 ↛ 3083line 3082 didn't jump to line 3083, because the condition on line 3082 was never true
3083 Util.log("SR no longer attached, exiting")
3084 return
3085 sr.scanLocked()
3086 if not sr.hasWork():
3087 Util.log("No work, exiting")
3088 return
3089 sr.xapi.create_task(
3090 "Garbage Collection",
3091 "Garbage collection for SR %s" % sr.uuid)
3092 _gcLoopPause(sr, dryRun, immediate=immediate)
3093 while True:
3094 if SIGTERM:
3095 Util.log("Term requested")
3096 return
3098 if not sr.xapi.isPluggedHere(): 3098 ↛ 3099line 3098 didn't jump to line 3099, because the condition on line 3098 was never true
3099 Util.log("SR no longer attached, exiting")
3100 break
3101 sr.scanLocked()
3102 if not sr.hasWork():
3103 Util.log("No work, exiting")
3104 break
3106 if not lockGCRunning.acquireNoblock(): 3106 ↛ 3107line 3106 didn't jump to line 3107, because the condition on line 3106 was never true
3107 Util.log("Unable to acquire GC running lock.")
3108 return
3109 try:
3110 if not sr.gcEnabled(): 3110 ↛ 3111line 3110 didn't jump to line 3111, because the condition on line 3110 was never true
3111 break
3113 sr.xapi.update_task_progress("done", coalesced)
3115 sr.cleanupCoalesceJournals()
3116 # Create the init file here in case startup is waiting on it
3117 _create_init_file(sr.uuid)
3118 sr.scanLocked()
3119 sr.updateBlockInfo()
3121 howmany = len(sr.findGarbage())
3122 if howmany > 0:
3123 Util.log("Found %d orphaned vdis" % howmany)
3124 sr.lock()
3125 try:
3126 sr.garbageCollect(dryRun)
3127 finally:
3128 sr.unlock()
3129 sr.xapi.srUpdate()
3131 candidate = sr.findCoalesceable()
3132 if candidate:
3133 util.fistpoint.activate(
3134 "LVHDRT_finding_a_suitable_pair", sr.uuid)
3135 sr.coalesce(candidate, dryRun)
3136 sr.xapi.srUpdate()
3137 coalesced += 1
3138 continue
3140 candidate = sr.findLeafCoalesceable()
3141 if candidate: 3141 ↛ 3148line 3141 didn't jump to line 3148, because the condition on line 3141 was never false
3142 sr.coalesceLeaf(candidate, dryRun)
3143 sr.xapi.srUpdate()
3144 coalesced += 1
3145 continue
3147 finally:
3148 lockGCRunning.release() 3148 ↛ 3153line 3148 didn't jump to line 3153, because the break on line 3111 wasn't executed
3149 except:
3150 task_status = "failure"
3151 raise
3152 finally:
3153 sr.xapi.set_task_status(task_status)
3154 Util.log("GC process exiting, no work left")
3155 _create_init_file(sr.uuid)
3156 lockGCActive.release()
3159def _gc(session, srUuid, dryRun=False, immediate=False):
3160 init(srUuid)
3161 sr = SR.getInstance(srUuid, session)
3162 if not sr.gcEnabled(False): 3162 ↛ 3163line 3162 didn't jump to line 3163, because the condition on line 3162 was never true
3163 return
3165 try:
3166 _gcLoop(sr, dryRun, immediate=immediate)
3167 finally:
3168 sr.check_no_space_candidates()
3169 sr.cleanup()
3170 sr.logFilter.logState()
3171 del sr.xapi
3174def _abort(srUuid, soft=False):
3175 """Aborts an GC/coalesce.
3177 srUuid: the UUID of the SR whose GC/coalesce must be aborted
3178 soft: If set to True and there is a pending abort signal, the function
3179 doesn't do anything. If set to False, a new abort signal is issued.
3181 returns: If soft is set to False, we return True holding lockGCActive. If
3182 soft is set to False and an abort signal is pending, we return False
3183 without holding lockGCActive. An exception is raised in case of error."""
3184 Util.log("=== SR %s: abort ===" % (srUuid))
3185 init(srUuid)
3186 if not lockGCActive.acquireNoblock():
3187 gotLock = False
3188 Util.log("Aborting currently-running instance (SR %s)" % srUuid)
3189 abortFlag = IPCFlag(srUuid)
3190 if not abortFlag.set(FLAG_TYPE_ABORT, soft):
3191 return False
3192 for i in range(SR.LOCK_RETRY_ATTEMPTS):
3193 gotLock = lockGCActive.acquireNoblock()
3194 if gotLock:
3195 break
3196 time.sleep(SR.LOCK_RETRY_INTERVAL)
3197 abortFlag.clear(FLAG_TYPE_ABORT)
3198 if not gotLock:
3199 raise util.CommandException(code=errno.ETIMEDOUT,
3200 reason="SR %s: error aborting existing process" % srUuid)
3201 return True
3204def init(srUuid):
3205 global lockGCRunning
3206 if not lockGCRunning: 3206 ↛ 3207line 3206 didn't jump to line 3207, because the condition on line 3206 was never true
3207 lockGCRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, srUuid)
3208 global lockGCActive
3209 if not lockGCActive: 3209 ↛ 3210line 3209 didn't jump to line 3210, because the condition on line 3209 was never true
3210 lockGCActive = LockActive(srUuid)
3213class LockActive:
3214 """
3215 Wraps the use of LOCK_TYPE_GC_ACTIVE such that the lock cannot be acquired
3216 if another process holds the SR lock.
3217 """
3218 def __init__(self, srUuid):
3219 self._lock = lock.Lock(LOCK_TYPE_GC_ACTIVE, srUuid)
3220 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, srUuid)
3222 def acquireNoblock(self):
3223 self._srLock.acquire()
3225 try:
3226 return self._lock.acquireNoblock()
3227 finally:
3228 self._srLock.release()
3230 def release(self):
3231 self._lock.release()
3234##############################################################################
3235#
3236# API
3237#
3238def abort(srUuid, soft=False):
3239 """Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair.
3240 """
3241 if _abort(srUuid, soft):
3242 stop_gc_service(srUuid)
3243 Util.log("abort: releasing the process lock")
3244 lockGCActive.release()
3245 return True
3246 else:
3247 return False
3250def run_gc(session, srUuid, dryRun, immediate=False):
3251 try:
3252 _gc(session, srUuid, dryRun, immediate=immediate)
3253 return 0
3254 except AbortException:
3255 Util.log("Aborted")
3256 return 2
3257 except Exception:
3258 Util.logException("gc")
3259 Util.log("* * * * * SR %s: ERROR\n" % srUuid)
3260 return 1
3263def gc(session, srUuid, inBackground, dryRun=False):
3264 """Garbage collect all deleted VDIs in SR "srUuid". Fork & return
3265 immediately if inBackground=True.
3267 The following algorithm is used:
3268 1. If we are already GC'ing in this SR, return
3269 2. If we are already coalescing a VDI pair:
3270 a. Scan the SR and determine if the VDI pair is GC'able
3271 b. If the pair is not GC'able, return
3272 c. If the pair is GC'able, abort coalesce
3273 3. Scan the SR
3274 4. If there is nothing to collect, nor to coalesce, return
3275 5. If there is something to collect, GC all, then goto 3
3276 6. If there is something to coalesce, coalesce one pair, then goto 3
3277 """
3278 Util.log("=== SR %s: gc ===" % srUuid)
3280 signal.signal(signal.SIGTERM, receiveSignal)
3282 if inBackground:
3283 if daemonize(): 3283 ↛ exitline 3283 didn't return from function 'gc', because the condition on line 3283 was never false
3284 # we are now running in the background. Catch & log any errors
3285 # because there is no other way to propagate them back at this
3286 # point
3288 run_gc(None, srUuid, dryRun)
3289 os._exit(0)
3290 else:
3291 os._exit(run_gc(session, srUuid, dryRun, immediate=True))
3294def start_gc(session, sr_uuid):
3295 """
3296 This function is used to try to start a backgrounded GC session by forking
3297 the current process. If using the systemd version, call start_gc_service() instead.
3298 """
3299 # don't bother if an instance already running (this is just an
3300 # optimization to reduce the overhead of forking a new process if we
3301 # don't have to, but the process will check the lock anyways)
3302 lockRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, sr_uuid)
3303 if not lockRunning.acquireNoblock():
3304 if should_preempt(session, sr_uuid):
3305 util.SMlog("Aborting currently-running coalesce of garbage VDI")
3306 try:
3307 if not abort(sr_uuid, soft=True):
3308 util.SMlog("The GC has already been scheduled to re-start")
3309 except util.CommandException as e:
3310 if e.code != errno.ETIMEDOUT:
3311 raise
3312 util.SMlog('failed to abort the GC')
3313 else:
3314 util.SMlog("A GC instance already running, not kicking")
3315 return
3316 else:
3317 lockRunning.release()
3319 util.SMlog(f"Starting GC file is {__file__}")
3320 subprocess.run([__file__, '-b', '-u', sr_uuid, '-g'],
3321 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3323def _gc_service_cmd(sr_uuid, action, extra_args=None):
3324 """
3325 Build and run the systemctl command for the GC service using util.doexec.
3326 """
3327 sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
3328 cmd=["/usr/bin/systemctl", "--quiet"]
3329 if extra_args: 3329 ↛ 3331line 3329 didn't jump to line 3331, because the condition on line 3329 was never false
3330 cmd.extend(extra_args)
3331 cmd += [action, f"SMGC@{sr_uuid_esc}"]
3332 return util.doexec(cmd)
3335def start_gc_service(sr_uuid, wait=False):
3336 """
3337 This starts the templated systemd service which runs GC on the given SR UUID.
3338 If the service was already started, this is a no-op.
3340 Because the service is a one-shot with RemainAfterExit=no, when called with
3341 wait=True this will run the service synchronously and will not return until the
3342 run has finished. This is used to force a run of the GC instead of just kicking it
3343 in the background.
3344 """
3345 util.SMlog(f"Kicking SMGC@{sr_uuid}...")
3346 _gc_service_cmd(sr_uuid, "start", extra_args=None if wait else ["--no-block"])
3349def stop_gc_service(sr_uuid):
3350 """
3351 Stops the templated systemd service which runs GC on the given SR UUID.
3352 """
3353 util.SMlog(f"Stopping SMGC@{sr_uuid}...")
3354 (rc, _stdout, stderr) = _gc_service_cmd(sr_uuid, "stop")
3355 if rc != 0:
3356 util.SMlog(f"Failed to stop gc service `SMGC@{sr_uuid}`: `{stderr}`")
3359def wait_for_completion(sr_uuid):
3360 while get_state(sr_uuid):
3361 time.sleep(5)
3364def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False):
3365 """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure
3366 the SR lock is held.
3367 The following algorithm is used:
3368 1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce
3369 2. Scan the SR
3370 3. GC
3371 4. return
3372 """
3373 Util.log("=== SR %s: gc_force ===" % srUuid)
3374 init(srUuid)
3375 sr = SR.getInstance(srUuid, session, lockSR, True)
3376 if not lockGCActive.acquireNoblock():
3377 abort(srUuid)
3378 else:
3379 Util.log("Nothing was running, clear to proceed")
3381 if force:
3382 Util.log("FORCED: will continue even if there are VHD errors")
3383 sr.scanLocked(force)
3384 sr.cleanupCoalesceJournals()
3386 try:
3387 sr.cleanupCache()
3388 sr.garbageCollect(dryRun)
3389 finally:
3390 sr.cleanup()
3391 sr.logFilter.logState()
3392 lockGCActive.release()
3395def get_state(srUuid):
3396 """Return whether GC/coalesce is currently running or not. This asks systemd for
3397 the state of the templated SMGC service and will return True if it is "activating"
3398 or "running" (for completeness, as in practice it will never achieve the latter state)
3399 """
3400 sr_uuid_esc = srUuid.replace("-", "\\x2d")
3401 cmd=[ "/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"]
3402 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3403 state = result.stdout.decode('utf-8').rstrip()
3404 if state == "activating" or state == "running":
3405 return True
3406 return False
3409def should_preempt(session, srUuid):
3410 sr = SR.getInstance(srUuid, session)
3411 entries = sr.journaler.getAll(VDI.JRN_COALESCE)
3412 if len(entries) == 0:
3413 return False
3414 elif len(entries) > 1:
3415 raise util.SMException("More than one coalesce entry: " + str(entries))
3416 sr.scanLocked()
3417 coalescedUuid = entries.popitem()[0]
3418 garbage = sr.findGarbage()
3419 for vdi in garbage:
3420 if vdi.uuid == coalescedUuid:
3421 return True
3422 return False
3425def get_coalesceable_leaves(session, srUuid, vdiUuids):
3426 coalesceable = []
3427 sr = SR.getInstance(srUuid, session)
3428 sr.scanLocked()
3429 for uuid in vdiUuids:
3430 vdi = sr.getVDI(uuid)
3431 if not vdi:
3432 raise util.SMException("VDI %s not found" % uuid)
3433 if vdi.isLeafCoalesceable():
3434 coalesceable.append(uuid)
3435 return coalesceable
3438def cache_cleanup(session, srUuid, maxAge):
3439 sr = SR.getInstance(srUuid, session)
3440 return sr.cleanupCache(maxAge)
3443def debug(sr_uuid, cmd, vdi_uuid):
3444 Util.log("Debug command: %s" % cmd)
3445 sr = SR.getInstance(sr_uuid, None)
3446 if not isinstance(sr, LVHDSR):
3447 print("Error: not an LVHD SR")
3448 return
3449 sr.scanLocked()
3450 vdi = sr.getVDI(vdi_uuid)
3451 if not vdi:
3452 print("Error: VDI %s not found")
3453 return
3454 print("Running %s on SR %s" % (cmd, sr))
3455 print("VDI before: %s" % vdi)
3456 if cmd == "activate":
3457 vdi._activate()
3458 print("VDI file: %s" % vdi.path)
3459 if cmd == "deactivate":
3460 ns = lvhdutil.NS_PREFIX_LVM + sr.uuid
3461 sr.lvmCache.deactivate(ns, vdi.uuid, vdi.fileName, False)
3462 if cmd == "inflate":
3463 vdi.inflateFully()
3464 sr.cleanup()
3465 if cmd == "deflate":
3466 vdi.deflate()
3467 sr.cleanup()
3468 sr.scanLocked()
3469 print("VDI after: %s" % vdi)
3472def abort_optional_reenable(uuid):
3473 print("Disabling GC/coalesce for %s" % uuid)
3474 ret = _abort(uuid)
3475 input("Press enter to re-enable...")
3476 print("GC/coalesce re-enabled")
3477 lockGCRunning.release()
3478 if ret:
3479 lockGCActive.release()