Coverage for drivers/cleanup.py : 32%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# Script to coalesce and garbage collect VHD-based SR's in the background
19#
21from sm_typing import Optional, override
23import os
24import os.path
25import sys
26import time
27import signal
28import subprocess
29import getopt
30import datetime
31import traceback
32import base64
33import zlib
34import errno
35import stat
37import XenAPI # pylint: disable=import-error
38import util
39import lvutil
40import vhdutil
41import lvhdutil
42import lvmcache
43import journaler
44import fjournaler
45import lock
46import blktap2
47import xs_errors
48from refcounter import RefCounter
49from ipc import IPCFlag
50from lvmanager import LVActivator
51from srmetadata import LVMMetadataHandler, VDI_TYPE_TAG
52from functools import reduce
53from time import monotonic as _time
55try:
56 from linstorjournaler import LinstorJournaler
57 from linstorvhdutil import LinstorVhdUtil
58 from linstorvolumemanager import get_controller_uri
59 from linstorvolumemanager import LinstorVolumeManager
60 from linstorvolumemanager import LinstorVolumeManagerError
61 from linstorvolumemanager import PERSISTENT_PREFIX as LINSTOR_PERSISTENT_PREFIX
63 LINSTOR_AVAILABLE = True
64except ImportError:
65 LINSTOR_AVAILABLE = False
67# Disable automatic leaf-coalescing. Online leaf-coalesce is currently not
68# possible due to lvhd_stop_using_() not working correctly. However, we leave
69# this option available through the explicit LEAFCLSC_FORCE flag in the VDI
70# record for use by the offline tool (which makes the operation safe by pausing
71# the VM first)
72AUTO_ONLINE_LEAF_COALESCE_ENABLED = True
74FLAG_TYPE_ABORT = "abort" # flag to request aborting of GC/coalesce
76# process "lock", used simply as an indicator that a process already exists
77# that is doing GC/coalesce on this SR (such a process holds the lock, and we
78# check for the fact by trying the lock).
79lockGCRunning = None
81# process "lock" to indicate that the GC process has been activated but may not
82# yet be running, stops a second process from being started.
83LOCK_TYPE_GC_ACTIVE = "gc_active"
84lockGCActive = None
86# Default coalesce error rate limit, in messages per minute. A zero value
87# disables throttling, and a negative value disables error reporting.
88DEFAULT_COALESCE_ERR_RATE = 1.0 / 60
90COALESCE_LAST_ERR_TAG = 'last-coalesce-error'
91COALESCE_ERR_RATE_TAG = 'coalesce-error-rate'
92VAR_RUN = "/var/run/"
93SPEED_LOG_ROOT = VAR_RUN + "{uuid}.speed_log"
95N_RUNNING_AVERAGE = 10
97NON_PERSISTENT_DIR = '/run/nonpersistent/sm'
100class AbortException(util.SMException):
101 pass
104################################################################################
105#
106# Util
107#
108class Util:
109 RET_RC = 1
110 RET_STDOUT = 2
111 RET_STDERR = 4
113 UUID_LEN = 36
115 PREFIX = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024}
117 @staticmethod
118 def log(text) -> None:
119 util.SMlog(text, ident="SMGC")
121 @staticmethod
122 def logException(tag):
123 info = sys.exc_info()
124 if info[0] == SystemExit: 124 ↛ 126line 124 didn't jump to line 126, because the condition on line 124 was never true
125 # this should not be happening when catching "Exception", but it is
126 sys.exit(0)
127 tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2]))
128 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
129 Util.log(" ***********************")
130 Util.log(" * E X C E P T I O N *")
131 Util.log(" ***********************")
132 Util.log("%s: EXCEPTION %s, %s" % (tag, info[0], info[1]))
133 Util.log(tb)
134 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
136 @staticmethod
137 def doexec(args, expectedRC, inputtext=None, ret=None, log=True):
138 "Execute a subprocess, then return its return code, stdout, stderr"
139 proc = subprocess.Popen(args,
140 stdin=subprocess.PIPE, \
141 stdout=subprocess.PIPE, \
142 stderr=subprocess.PIPE, \
143 shell=True, \
144 close_fds=True)
145 (stdout, stderr) = proc.communicate(inputtext)
146 stdout = str(stdout)
147 stderr = str(stderr)
148 rc = proc.returncode
149 if log:
150 Util.log("`%s`: %s" % (args, rc))
151 if type(expectedRC) != type([]):
152 expectedRC = [expectedRC]
153 if not rc in expectedRC:
154 reason = stderr.strip()
155 if stdout.strip():
156 reason = "%s (stdout: %s)" % (reason, stdout.strip())
157 Util.log("Failed: %s" % reason)
158 raise util.CommandException(rc, args, reason)
160 if ret == Util.RET_RC:
161 return rc
162 if ret == Util.RET_STDERR:
163 return stderr
164 return stdout
166 @staticmethod
167 def runAbortable(func, ret, ns, abortTest, pollInterval, timeOut):
168 """execute func in a separate thread and kill it if abortTest signals
169 so"""
170 abortSignaled = abortTest() # check now before we clear resultFlag
171 resultFlag = IPCFlag(ns)
172 resultFlag.clearAll()
173 pid = os.fork()
174 if pid:
175 startTime = _time()
176 try:
177 while True:
178 if resultFlag.test("success"):
179 Util.log(" Child process completed successfully")
180 resultFlag.clear("success")
181 return
182 if resultFlag.test("failure"):
183 resultFlag.clear("failure")
184 raise util.SMException("Child process exited with error")
185 if abortTest() or abortSignaled:
186 os.killpg(pid, signal.SIGKILL)
187 raise AbortException("Aborting due to signal")
188 if timeOut and _time() - startTime > timeOut:
189 os.killpg(pid, signal.SIGKILL)
190 resultFlag.clearAll()
191 raise util.SMException("Timed out")
192 time.sleep(pollInterval)
193 finally:
194 wait_pid = 0
195 rc = -1
196 count = 0
197 while wait_pid == 0 and count < 10:
198 wait_pid, rc = os.waitpid(pid, os.WNOHANG)
199 if wait_pid == 0:
200 time.sleep(2)
201 count += 1
203 if wait_pid == 0:
204 Util.log("runAbortable: wait for process completion timed out")
205 else:
206 os.setpgrp()
207 try:
208 if func() == ret:
209 resultFlag.set("success")
210 else:
211 resultFlag.set("failure")
212 except Exception as e:
213 Util.log("Child process failed with : (%s)" % e)
214 resultFlag.set("failure")
215 Util.logException("This exception has occured")
216 os._exit(0)
218 @staticmethod
219 def num2str(number):
220 for prefix in ("G", "M", "K"): 220 ↛ 223line 220 didn't jump to line 223, because the loop on line 220 didn't complete
221 if number >= Util.PREFIX[prefix]:
222 return "%.3f%s" % (float(number) / Util.PREFIX[prefix], prefix)
223 return "%s" % number
225 @staticmethod
226 def numBits(val):
227 count = 0
228 while val:
229 count += val & 1
230 val = val >> 1
231 return count
233 @staticmethod
234 def countBits(bitmap1, bitmap2):
235 """return bit count in the bitmap produced by ORing the two bitmaps"""
236 len1 = len(bitmap1)
237 len2 = len(bitmap2)
238 lenLong = len1
239 lenShort = len2
240 bitmapLong = bitmap1
241 if len2 > len1:
242 lenLong = len2
243 lenShort = len1
244 bitmapLong = bitmap2
246 count = 0
247 for i in range(lenShort):
248 val = bitmap1[i] | bitmap2[i]
249 count += Util.numBits(val)
251 for i in range(i + 1, lenLong):
252 val = bitmapLong[i]
253 count += Util.numBits(val)
254 return count
256 @staticmethod
257 def getThisScript():
258 thisScript = util.get_real_path(__file__)
259 if thisScript.endswith(".pyc"):
260 thisScript = thisScript[:-1]
261 return thisScript
264################################################################################
265#
266# XAPI
267#
268class XAPI:
269 USER = "root"
270 PLUGIN_ON_SLAVE = "on-slave"
272 CONFIG_SM = 0
273 CONFIG_OTHER = 1
274 CONFIG_ON_BOOT = 2
275 CONFIG_ALLOW_CACHING = 3
277 CONFIG_NAME = {
278 CONFIG_SM: "sm-config",
279 CONFIG_OTHER: "other-config",
280 CONFIG_ON_BOOT: "on-boot",
281 CONFIG_ALLOW_CACHING: "allow_caching"
282 }
284 class LookupError(util.SMException):
285 pass
287 @staticmethod
288 def getSession():
289 session = XenAPI.xapi_local()
290 session.xenapi.login_with_password(XAPI.USER, '', '', 'SM')
291 return session
293 def __init__(self, session, srUuid):
294 self.sessionPrivate = False
295 self.session = session
296 if self.session is None:
297 self.session = self.getSession()
298 self.sessionPrivate = True
299 self._srRef = self.session.xenapi.SR.get_by_uuid(srUuid)
300 self.srRecord = self.session.xenapi.SR.get_record(self._srRef)
301 self.hostUuid = util.get_this_host()
302 self._hostRef = self.session.xenapi.host.get_by_uuid(self.hostUuid)
303 self.task = None
304 self.task_progress = {"coalescable": 0, "done": 0}
306 def __del__(self):
307 if self.sessionPrivate:
308 self.session.xenapi.session.logout()
310 def isPluggedHere(self):
311 pbds = self.getAttachedPBDs()
312 for pbdRec in pbds:
313 if pbdRec["host"] == self._hostRef:
314 return True
315 return False
317 def poolOK(self):
318 host_recs = self.session.xenapi.host.get_all_records()
319 for host_ref, host_rec in host_recs.items():
320 if not host_rec["enabled"]:
321 Util.log("Host %s not enabled" % host_rec["uuid"])
322 return False
323 return True
325 def isMaster(self):
326 if self.srRecord["shared"]:
327 pool = list(self.session.xenapi.pool.get_all_records().values())[0]
328 return pool["master"] == self._hostRef
329 else:
330 pbds = self.getAttachedPBDs()
331 if len(pbds) < 1:
332 raise util.SMException("Local SR not attached")
333 elif len(pbds) > 1:
334 raise util.SMException("Local SR multiply attached")
335 return pbds[0]["host"] == self._hostRef
337 def getAttachedPBDs(self):
338 """Return PBD records for all PBDs of this SR that are currently
339 attached"""
340 attachedPBDs = []
341 pbds = self.session.xenapi.PBD.get_all_records()
342 for pbdRec in pbds.values():
343 if pbdRec["SR"] == self._srRef and pbdRec["currently_attached"]:
344 attachedPBDs.append(pbdRec)
345 return attachedPBDs
347 def getOnlineHosts(self):
348 return util.get_online_hosts(self.session)
350 def ensureInactive(self, hostRef, args):
351 text = self.session.xenapi.host.call_plugin( \
352 hostRef, self.PLUGIN_ON_SLAVE, "multi", args)
353 Util.log("call-plugin returned: '%s'" % text)
355 def getRecordHost(self, hostRef):
356 return self.session.xenapi.host.get_record(hostRef)
358 def _getRefVDI(self, uuid):
359 return self.session.xenapi.VDI.get_by_uuid(uuid)
361 def getRefVDI(self, vdi):
362 return self._getRefVDI(vdi.uuid)
364 def getRecordVDI(self, uuid):
365 try:
366 ref = self._getRefVDI(uuid)
367 return self.session.xenapi.VDI.get_record(ref)
368 except XenAPI.Failure:
369 return None
371 def singleSnapshotVDI(self, vdi):
372 return self.session.xenapi.VDI.snapshot(vdi.getRef(),
373 {"type": "internal"})
375 def forgetVDI(self, srUuid, vdiUuid):
376 """Forget the VDI, but handle the case where the VDI has already been
377 forgotten (i.e. ignore errors)"""
378 try:
379 vdiRef = self.session.xenapi.VDI.get_by_uuid(vdiUuid)
380 self.session.xenapi.VDI.forget(vdiRef)
381 except XenAPI.Failure:
382 pass
384 def getConfigVDI(self, vdi, key):
385 kind = vdi.CONFIG_TYPE[key]
386 if kind == self.CONFIG_SM:
387 cfg = self.session.xenapi.VDI.get_sm_config(vdi.getRef())
388 elif kind == self.CONFIG_OTHER:
389 cfg = self.session.xenapi.VDI.get_other_config(vdi.getRef())
390 elif kind == self.CONFIG_ON_BOOT:
391 cfg = self.session.xenapi.VDI.get_on_boot(vdi.getRef())
392 elif kind == self.CONFIG_ALLOW_CACHING:
393 cfg = self.session.xenapi.VDI.get_allow_caching(vdi.getRef())
394 else:
395 assert(False)
396 Util.log("Got %s for %s: %s" % (self.CONFIG_NAME[kind], vdi, repr(cfg)))
397 return cfg
399 def removeFromConfigVDI(self, vdi, key):
400 kind = vdi.CONFIG_TYPE[key]
401 if kind == self.CONFIG_SM:
402 self.session.xenapi.VDI.remove_from_sm_config(vdi.getRef(), key)
403 elif kind == self.CONFIG_OTHER:
404 self.session.xenapi.VDI.remove_from_other_config(vdi.getRef(), key)
405 else:
406 assert(False)
408 def addToConfigVDI(self, vdi, key, val):
409 kind = vdi.CONFIG_TYPE[key]
410 if kind == self.CONFIG_SM:
411 self.session.xenapi.VDI.add_to_sm_config(vdi.getRef(), key, val)
412 elif kind == self.CONFIG_OTHER:
413 self.session.xenapi.VDI.add_to_other_config(vdi.getRef(), key, val)
414 else:
415 assert(False)
417 def isSnapshot(self, vdi):
418 return self.session.xenapi.VDI.get_is_a_snapshot(vdi.getRef())
420 def markCacheSRsDirty(self):
421 sr_refs = self.session.xenapi.SR.get_all_records_where( \
422 'field "local_cache_enabled" = "true"')
423 for sr_ref in sr_refs:
424 Util.log("Marking SR %s dirty" % sr_ref)
425 util.set_dirty(self.session, sr_ref)
427 def srUpdate(self):
428 Util.log("Starting asynch srUpdate for SR %s" % self.srRecord["uuid"])
429 abortFlag = IPCFlag(self.srRecord["uuid"])
430 task = self.session.xenapi.Async.SR.update(self._srRef)
431 cancelTask = True
432 try:
433 for i in range(60):
434 status = self.session.xenapi.task.get_status(task)
435 if not status == "pending":
436 Util.log("SR.update_asynch status changed to [%s]" % status)
437 cancelTask = False
438 return
439 if abortFlag.test(FLAG_TYPE_ABORT):
440 Util.log("Abort signalled during srUpdate, cancelling task...")
441 try:
442 self.session.xenapi.task.cancel(task)
443 cancelTask = False
444 Util.log("Task cancelled")
445 except:
446 pass
447 return
448 time.sleep(1)
449 finally:
450 if cancelTask:
451 self.session.xenapi.task.cancel(task)
452 self.session.xenapi.task.destroy(task)
453 Util.log("Asynch srUpdate still running, but timeout exceeded.")
455 def update_task(self):
456 self.session.xenapi.task.set_other_config(
457 self.task,
458 {
459 "applies_to": self._srRef
460 })
461 total = self.task_progress['coalescable'] + self.task_progress['done']
462 if (total > 0):
463 self.session.xenapi.task.set_progress(
464 self.task, float(self.task_progress['done']) / total)
466 def create_task(self, label, description):
467 self.task = self.session.xenapi.task.create(label, description)
468 self.update_task()
470 def update_task_progress(self, key, value):
471 self.task_progress[key] = value
472 if self.task:
473 self.update_task()
475 def set_task_status(self, status):
476 if self.task:
477 self.session.xenapi.task.set_status(self.task, status)
480################################################################################
481#
482# VDI
483#
484class VDI(object):
485 """Object representing a VDI of a VHD-based SR"""
487 POLL_INTERVAL = 1
488 POLL_TIMEOUT = 30
489 DEVICE_MAJOR = 202
490 DRIVER_NAME_VHD = "vhd"
492 # config keys & values
493 DB_VHD_PARENT = "vhd-parent"
494 DB_VDI_TYPE = "vdi_type"
495 DB_VHD_BLOCKS = "vhd-blocks"
496 DB_VDI_PAUSED = "paused"
497 DB_VDI_RELINKING = "relinking"
498 DB_VDI_ACTIVATING = "activating"
499 DB_GC = "gc"
500 DB_COALESCE = "coalesce"
501 DB_LEAFCLSC = "leaf-coalesce" # config key
502 LEAFCLSC_DISABLED = "false" # set by user; means do not leaf-coalesce
503 LEAFCLSC_FORCE = "force" # set by user; means skip snap-coalesce
504 LEAFCLSC_OFFLINE = "offline" # set here for informational purposes: means
505 # no space to snap-coalesce or unable to keep
506 # up with VDI. This is not used by the SM, it
507 # might be used by external components.
508 DB_ONBOOT = "on-boot"
509 ONBOOT_RESET = "reset"
510 DB_ALLOW_CACHING = "allow_caching"
512 CONFIG_TYPE = {
513 DB_VHD_PARENT: XAPI.CONFIG_SM,
514 DB_VDI_TYPE: XAPI.CONFIG_SM,
515 DB_VHD_BLOCKS: XAPI.CONFIG_SM,
516 DB_VDI_PAUSED: XAPI.CONFIG_SM,
517 DB_VDI_RELINKING: XAPI.CONFIG_SM,
518 DB_VDI_ACTIVATING: XAPI.CONFIG_SM,
519 DB_GC: XAPI.CONFIG_OTHER,
520 DB_COALESCE: XAPI.CONFIG_OTHER,
521 DB_LEAFCLSC: XAPI.CONFIG_OTHER,
522 DB_ONBOOT: XAPI.CONFIG_ON_BOOT,
523 DB_ALLOW_CACHING: XAPI.CONFIG_ALLOW_CACHING,
524 }
526 LIVE_LEAF_COALESCE_MAX_SIZE = 20 * 1024 * 1024 # bytes
527 LIVE_LEAF_COALESCE_TIMEOUT = 10 # seconds
528 TIMEOUT_SAFETY_MARGIN = 0.5 # extra margin when calculating
529 # feasibility of leaf coalesce
531 JRN_RELINK = "relink" # journal entry type for relinking children
532 JRN_COALESCE = "coalesce" # to communicate which VDI is being coalesced
533 JRN_LEAF = "leaf" # used in coalesce-leaf
535 STR_TREE_INDENT = 4
537 def __init__(self, sr, uuid, raw):
538 self.sr = sr
539 self.scanError = True
540 self.uuid = uuid
541 self.raw = raw
542 self.fileName = ""
543 self.parentUuid = ""
544 self.sizeVirt = -1
545 self._sizeVHD = -1
546 self._sizeAllocated = -1
547 self.hidden = False
548 self.parent = None
549 self.children = []
550 self._vdiRef = None
551 self._clearRef()
553 @staticmethod
554 def extractUuid(path):
555 raise NotImplementedError("Implement in sub class")
557 def load(self, info=None) -> None:
558 """Load VDI info"""
559 pass
561 def getDriverName(self) -> str:
562 return self.DRIVER_NAME_VHD
564 def getRef(self):
565 if self._vdiRef is None:
566 self._vdiRef = self.sr.xapi.getRefVDI(self)
567 return self._vdiRef
569 def getConfig(self, key, default=None):
570 config = self.sr.xapi.getConfigVDI(self, key)
571 if key == self.DB_ONBOOT or key == self.DB_ALLOW_CACHING: 571 ↛ 572line 571 didn't jump to line 572, because the condition on line 571 was never true
572 val = config
573 else:
574 val = config.get(key)
575 if val:
576 return val
577 return default
579 def setConfig(self, key, val):
580 self.sr.xapi.removeFromConfigVDI(self, key)
581 self.sr.xapi.addToConfigVDI(self, key, val)
582 Util.log("Set %s = %s for %s" % (key, val, self))
584 def delConfig(self, key):
585 self.sr.xapi.removeFromConfigVDI(self, key)
586 Util.log("Removed %s from %s" % (key, self))
588 def ensureUnpaused(self):
589 if self.getConfig(self.DB_VDI_PAUSED) == "true":
590 Util.log("Unpausing VDI %s" % self)
591 self.unpause()
593 def pause(self, failfast=False) -> None:
594 if not blktap2.VDI.tap_pause(self.sr.xapi.session, self.sr.uuid,
595 self.uuid, failfast):
596 raise util.SMException("Failed to pause VDI %s" % self)
598 def _report_tapdisk_unpause_error(self):
599 try:
600 xapi = self.sr.xapi.session.xenapi
601 sr_ref = xapi.SR.get_by_uuid(self.sr.uuid)
602 msg_name = "failed to unpause tapdisk"
603 msg_body = "Failed to unpause tapdisk for VDI %s, " \
604 "VMs using this tapdisk have lost access " \
605 "to the corresponding disk(s)" % self.uuid
606 xapi.message.create(msg_name, "4", "SR", self.sr.uuid, msg_body)
607 except Exception as e:
608 util.SMlog("failed to generate message: %s" % e)
610 def unpause(self):
611 if not blktap2.VDI.tap_unpause(self.sr.xapi.session, self.sr.uuid,
612 self.uuid):
613 self._report_tapdisk_unpause_error()
614 raise util.SMException("Failed to unpause VDI %s" % self)
616 def refresh(self, ignoreNonexistent=True):
617 """Pause-unpause in one step"""
618 self.sr.lock()
619 try:
620 try:
621 if not blktap2.VDI.tap_refresh(self.sr.xapi.session, 621 ↛ 623line 621 didn't jump to line 623, because the condition on line 621 was never true
622 self.sr.uuid, self.uuid):
623 self._report_tapdisk_unpause_error()
624 raise util.SMException("Failed to refresh %s" % self)
625 except XenAPI.Failure as e:
626 if util.isInvalidVDI(e) and ignoreNonexistent:
627 Util.log("VDI %s not found, ignoring" % self)
628 return
629 raise
630 finally:
631 self.sr.unlock()
633 def isSnapshot(self):
634 return self.sr.xapi.isSnapshot(self)
636 def isAttachedRW(self):
637 return util.is_attached_rw(
638 self.sr.xapi.session.xenapi.VDI.get_sm_config(self.getRef()))
640 def getVHDBlocks(self):
641 val = self.updateBlockInfo()
642 bitmap = zlib.decompress(base64.b64decode(val))
643 return bitmap
645 def isCoalesceable(self):
646 """A VDI is coalesceable if it has no siblings and is not a leaf"""
647 return not self.scanError and \
648 self.parent and \
649 len(self.parent.children) == 1 and \
650 self.hidden and \
651 len(self.children) > 0
653 def isLeafCoalesceable(self):
654 """A VDI is leaf-coalesceable if it has no siblings and is a leaf"""
655 return not self.scanError and \
656 self.parent and \
657 len(self.parent.children) == 1 and \
658 not self.hidden and \
659 len(self.children) == 0
661 def canLiveCoalesce(self, speed):
662 """Can we stop-and-leaf-coalesce this VDI? The VDI must be
663 isLeafCoalesceable() already"""
664 feasibleSize = False
665 allowedDownTime = \
666 self.TIMEOUT_SAFETY_MARGIN * self.LIVE_LEAF_COALESCE_TIMEOUT
667 vhd_size = self.getAllocatedSize()
668 if speed:
669 feasibleSize = \
670 vhd_size // speed < allowedDownTime
671 else:
672 feasibleSize = \
673 vhd_size < self.LIVE_LEAF_COALESCE_MAX_SIZE
675 return (feasibleSize or
676 self.getConfig(self.DB_LEAFCLSC) == self.LEAFCLSC_FORCE)
678 def getAllPrunable(self):
679 if len(self.children) == 0: # base case
680 # it is possible to have a hidden leaf that was recently coalesced
681 # onto its parent, its children already relinked but not yet
682 # reloaded - in which case it may not be garbage collected yet:
683 # some tapdisks could still be using the file.
684 if self.sr.journaler.get(self.JRN_RELINK, self.uuid):
685 return []
686 if not self.scanError and self.hidden:
687 return [self]
688 return []
690 thisPrunable = True
691 vdiList = []
692 for child in self.children:
693 childList = child.getAllPrunable()
694 vdiList.extend(childList)
695 if child not in childList:
696 thisPrunable = False
698 # We can destroy the current VDI if all childs are hidden BUT the
699 # current VDI must be hidden too to do that!
700 # Example in this case (after a failed live leaf coalesce):
701 #
702 # SMGC: [32436] SR 07ed ('linstor-nvme-sr') (2 VDIs in 1 VHD trees):
703 # SMGC: [32436] b5458d61(1.000G/4.127M)
704 # SMGC: [32436] *OLD_b545(1.000G/4.129M)
705 #
706 # OLD_b545 is hidden and must be removed, but b5458d61 not.
707 # Normally we are not in this function when the delete action is
708 # executed but in `_liveLeafCoalesce`.
710 if not self.scanError and not self.hidden and thisPrunable:
711 vdiList.append(self)
712 return vdiList
714 def getSizeVHD(self) -> int:
715 return self._sizeVHD
717 def getAllocatedSize(self) -> int:
718 return self._sizeAllocated
720 def getTreeRoot(self):
721 "Get the root of the tree that self belongs to"
722 root = self
723 while root.parent:
724 root = root.parent
725 return root
727 def getTreeHeight(self):
728 "Get the height of the subtree rooted at self"
729 if len(self.children) == 0:
730 return 1
732 maxChildHeight = 0
733 for child in self.children:
734 childHeight = child.getTreeHeight()
735 if childHeight > maxChildHeight:
736 maxChildHeight = childHeight
738 return maxChildHeight + 1
740 def getAllLeaves(self):
741 "Get all leaf nodes in the subtree rooted at self"
742 if len(self.children) == 0:
743 return [self]
745 leaves = []
746 for child in self.children:
747 leaves.extend(child.getAllLeaves())
748 return leaves
750 def updateBlockInfo(self) -> Optional[str]:
751 val = base64.b64encode(self._queryVHDBlocks()).decode()
752 self.setConfig(VDI.DB_VHD_BLOCKS, val)
753 return val
755 def rename(self, uuid) -> None:
756 "Rename the VDI file"
757 assert(not self.sr.vdis.get(uuid))
758 self._clearRef()
759 oldUuid = self.uuid
760 self.uuid = uuid
761 self.children = []
762 # updating the children themselves is the responsibility of the caller
763 del self.sr.vdis[oldUuid]
764 self.sr.vdis[self.uuid] = self
766 def delete(self) -> None:
767 "Physically delete the VDI"
768 lock.Lock.cleanup(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
769 lock.Lock.cleanupAll(self.uuid)
770 self._clear()
772 def getParent(self) -> str:
773 return vhdutil.getParent(self.path, lambda x: x.strip()) 773 ↛ exitline 773 didn't run the lambda on line 773
775 def repair(self, parent) -> None:
776 vhdutil.repair(parent)
778 @override
779 def __str__(self) -> str:
780 strHidden = ""
781 if self.hidden: 781 ↛ 782line 781 didn't jump to line 782, because the condition on line 781 was never true
782 strHidden = "*"
783 strSizeVirt = "?"
784 if self.sizeVirt > 0: 784 ↛ 785line 784 didn't jump to line 785, because the condition on line 784 was never true
785 strSizeVirt = Util.num2str(self.sizeVirt)
786 strSizeVHD = "?"
787 if self._sizeVHD > 0: 787 ↛ 788line 787 didn't jump to line 788, because the condition on line 787 was never true
788 strSizeVHD = "/%s" % Util.num2str(self._sizeVHD)
789 strSizeAllocated = "?"
790 if self._sizeAllocated >= 0:
791 strSizeAllocated = "/%s" % Util.num2str(self._sizeAllocated)
792 strType = ""
793 if self.raw:
794 strType = "[RAW]"
795 strSizeVHD = ""
797 return "%s%s(%s%s%s)%s" % (strHidden, self.uuid[0:8], strSizeVirt,
798 strSizeVHD, strSizeAllocated, strType)
800 def validate(self, fast=False) -> None:
801 if not vhdutil.check(self.path, fast=fast): 801 ↛ 802line 801 didn't jump to line 802, because the condition on line 801 was never true
802 raise util.SMException("VHD %s corrupted" % self)
804 def _clear(self):
805 self.uuid = ""
806 self.path = ""
807 self.parentUuid = ""
808 self.parent = None
809 self._clearRef()
811 def _clearRef(self):
812 self._vdiRef = None
814 def _doCoalesce(self) -> None:
815 """Coalesce self onto parent. Only perform the actual coalescing of
816 VHD, but not the subsequent relinking. We'll do that as the next step,
817 after reloading the entire SR in case things have changed while we
818 were coalescing"""
819 self.validate()
820 self.parent.validate(True)
821 self.parent._increaseSizeVirt(self.sizeVirt)
822 self.sr._updateSlavesOnResize(self.parent)
823 self._coalesceVHD(0)
824 self.parent.validate(True)
825 #self._verifyContents(0)
826 self.parent.updateBlockInfo()
828 def _verifyContents(self, timeOut):
829 Util.log(" Coalesce verification on %s" % self)
830 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
831 Util.runAbortable(lambda: self._runTapdiskDiff(), True,
832 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
833 Util.log(" Coalesce verification succeeded")
835 def _runTapdiskDiff(self):
836 cmd = "tapdisk-diff -n %s:%s -m %s:%s" % \
837 (self.getDriverName(), self.path, \
838 self.parent.getDriverName(), self.parent.path)
839 Util.doexec(cmd, 0)
840 return True
842 @staticmethod
843 def _reportCoalesceError(vdi, ce):
844 """Reports a coalesce error to XenCenter.
846 vdi: the VDI object on which the coalesce error occured
847 ce: the CommandException that was raised"""
849 msg_name = os.strerror(ce.code)
850 if ce.code == errno.ENOSPC:
851 # TODO We could add more information here, e.g. exactly how much
852 # space is required for the particular coalesce, as well as actions
853 # to be taken by the user and consequences of not taking these
854 # actions.
855 msg_body = 'Run out of space while coalescing.'
856 elif ce.code == errno.EIO:
857 msg_body = 'I/O error while coalescing.'
858 else:
859 msg_body = ''
860 util.SMlog('Coalesce failed on SR %s: %s (%s)'
861 % (vdi.sr.uuid, msg_name, msg_body))
863 # Create a XenCenter message, but don't spam.
864 xapi = vdi.sr.xapi.session.xenapi
865 sr_ref = xapi.SR.get_by_uuid(vdi.sr.uuid)
866 oth_cfg = xapi.SR.get_other_config(sr_ref)
867 if COALESCE_ERR_RATE_TAG in oth_cfg:
868 coalesce_err_rate = float(oth_cfg[COALESCE_ERR_RATE_TAG])
869 else:
870 coalesce_err_rate = DEFAULT_COALESCE_ERR_RATE
872 xcmsg = False
873 if coalesce_err_rate == 0:
874 xcmsg = True
875 elif coalesce_err_rate > 0:
876 now = datetime.datetime.now()
877 sm_cfg = xapi.SR.get_sm_config(sr_ref)
878 if COALESCE_LAST_ERR_TAG in sm_cfg:
879 # seconds per message (minimum distance in time between two
880 # messages in seconds)
881 spm = datetime.timedelta(seconds=(1.0 / coalesce_err_rate) * 60)
882 last = datetime.datetime.fromtimestamp(
883 float(sm_cfg[COALESCE_LAST_ERR_TAG]))
884 if now - last >= spm:
885 xapi.SR.remove_from_sm_config(sr_ref,
886 COALESCE_LAST_ERR_TAG)
887 xcmsg = True
888 else:
889 xcmsg = True
890 if xcmsg:
891 xapi.SR.add_to_sm_config(sr_ref, COALESCE_LAST_ERR_TAG,
892 str(now.strftime('%s')))
893 if xcmsg:
894 xapi.message.create(msg_name, "3", "SR", vdi.sr.uuid, msg_body)
896 def coalesce(self) -> int:
897 # size is returned in sectors
898 return vhdutil.coalesce(self.path) * 512
900 @staticmethod
901 def _doCoalesceVHD(vdi):
902 try:
903 startTime = time.time()
904 vhdSize = vdi.getAllocatedSize()
905 coalesced_size = vdi.coalesce()
906 endTime = time.time()
907 vdi.sr.recordStorageSpeed(startTime, endTime, coalesced_size)
908 except util.CommandException as ce:
909 # We use try/except for the following piece of code because it runs
910 # in a separate process context and errors will not be caught and
911 # reported by anyone.
912 try:
913 # Report coalesce errors back to user via XC
914 VDI._reportCoalesceError(vdi, ce)
915 except Exception as e:
916 util.SMlog('failed to create XenCenter message: %s' % e)
917 raise ce
918 except:
919 raise
921 def _vdi_is_raw(self, vdi_path):
922 """
923 Given path to vdi determine if it is raw
924 """
925 uuid = self.extractUuid(vdi_path)
926 return self.sr.vdis[uuid].raw
928 def _coalesceVHD(self, timeOut):
929 Util.log(" Running VHD coalesce on %s" % self)
930 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT) 930 ↛ exitline 930 didn't run the lambda on line 930
931 try:
932 util.fistpoint.activate_custom_fn(
933 "cleanup_coalesceVHD_inject_failure",
934 util.inject_failure)
935 Util.runAbortable(lambda: VDI._doCoalesceVHD(self), None,
936 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
937 except:
938 #exception at this phase could indicate a failure in vhd coalesce
939 # or a kill of vhd coalesce by runAbortable due to timeOut
940 # Try a repair and reraise the exception
941 parent = ""
942 try:
943 parent = self.getParent()
944 if not self._vdi_is_raw(parent):
945 # Repair error is logged and ignored. Error reraised later
946 util.SMlog('Coalesce failed on %s, attempting repair on ' \
947 'parent %s' % (self.uuid, parent))
948 self.repair(parent)
949 except Exception as e:
950 util.SMlog('(error ignored) Failed to repair parent %s ' \
951 'after failed coalesce on %s, err: %s' %
952 (parent, self.path, e))
953 raise
955 util.fistpoint.activate("LVHDRT_coalescing_VHD_data", self.sr.uuid)
957 def _relinkSkip(self) -> None:
958 """Relink children of this VDI to point to the parent of this VDI"""
959 abortFlag = IPCFlag(self.sr.uuid)
960 for child in self.children:
961 if abortFlag.test(FLAG_TYPE_ABORT): 961 ↛ 962line 961 didn't jump to line 962, because the condition on line 961 was never true
962 raise AbortException("Aborting due to signal")
963 Util.log(" Relinking %s from %s to %s" % \
964 (child, self, self.parent))
965 util.fistpoint.activate("LVHDRT_relinking_grandchildren", self.sr.uuid)
966 child._setParent(self.parent)
967 self.children = []
969 def _reloadChildren(self, vdiSkip):
970 """Pause & unpause all VDIs in the subtree to cause blktap to reload
971 the VHD metadata for this file in any online VDI"""
972 abortFlag = IPCFlag(self.sr.uuid)
973 for child in self.children:
974 if child == vdiSkip:
975 continue
976 if abortFlag.test(FLAG_TYPE_ABORT): 976 ↛ 977line 976 didn't jump to line 977, because the condition on line 976 was never true
977 raise AbortException("Aborting due to signal")
978 Util.log(" Reloading VDI %s" % child)
979 child._reload()
981 def _reload(self):
982 """Pause & unpause to cause blktap to reload the VHD metadata"""
983 for child in self.children: 983 ↛ 984line 983 didn't jump to line 984, because the loop on line 983 never started
984 child._reload()
986 # only leaves can be attached
987 if len(self.children) == 0: 987 ↛ exitline 987 didn't return from function '_reload', because the condition on line 987 was never false
988 try:
989 self.delConfig(VDI.DB_VDI_RELINKING)
990 except XenAPI.Failure as e:
991 if not util.isInvalidVDI(e):
992 raise
993 self.refresh()
995 def _tagChildrenForRelink(self):
996 if len(self.children) == 0:
997 retries = 0
998 try:
999 while retries < 15:
1000 retries += 1
1001 if self.getConfig(VDI.DB_VDI_ACTIVATING) is not None:
1002 Util.log("VDI %s is activating, wait to relink" %
1003 self.uuid)
1004 else:
1005 self.setConfig(VDI.DB_VDI_RELINKING, "True")
1007 if self.getConfig(VDI.DB_VDI_ACTIVATING):
1008 self.delConfig(VDI.DB_VDI_RELINKING)
1009 Util.log("VDI %s started activating while tagging" %
1010 self.uuid)
1011 else:
1012 return
1013 time.sleep(2)
1015 raise util.SMException("Failed to tag vdi %s for relink" % self)
1016 except XenAPI.Failure as e:
1017 if not util.isInvalidVDI(e):
1018 raise
1020 for child in self.children:
1021 child._tagChildrenForRelink()
1023 def _loadInfoParent(self):
1024 ret = vhdutil.getParent(self.path, lvhdutil.extractUuid)
1025 if ret:
1026 self.parentUuid = ret
1028 def _setParent(self, parent) -> None:
1029 vhdutil.setParent(self.path, parent.path, False)
1030 self.parent = parent
1031 self.parentUuid = parent.uuid
1032 parent.children.append(self)
1033 try:
1034 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1035 Util.log("Updated the vhd-parent field for child %s with %s" % \
1036 (self.uuid, self.parentUuid))
1037 except:
1038 Util.log("Failed to update %s with vhd-parent field %s" % \
1039 (self.uuid, self.parentUuid))
1041 def _loadInfoHidden(self) -> None:
1042 hidden = vhdutil.getHidden(self.path)
1043 self.hidden = (hidden != 0)
1045 def _setHidden(self, hidden=True) -> None:
1046 vhdutil.setHidden(self.path, hidden)
1047 self.hidden = hidden
1049 def _increaseSizeVirt(self, size, atomic=True) -> None:
1050 """ensure the virtual size of 'self' is at least 'size'. Note that
1051 resizing a VHD must always be offline and atomically: the file must
1052 not be open by anyone and no concurrent operations may take place.
1053 Thus we use the Agent API call for performing paused atomic
1054 operations. If the caller is already in the atomic context, it must
1055 call with atomic = False"""
1056 if self.sizeVirt >= size: 1056 ↛ 1058line 1056 didn't jump to line 1058, because the condition on line 1056 was never false
1057 return
1058 Util.log(" Expanding VHD virt size for VDI %s: %s -> %s" % \
1059 (self, Util.num2str(self.sizeVirt), Util.num2str(size)))
1061 msize = vhdutil.getMaxResizeSize(self.path) * 1024 * 1024
1062 if (size <= msize):
1063 vhdutil.setSizeVirtFast(self.path, size)
1064 else:
1065 if atomic:
1066 vdiList = self._getAllSubtree()
1067 self.sr.lock()
1068 try:
1069 self.sr.pauseVDIs(vdiList)
1070 try:
1071 self._setSizeVirt(size)
1072 finally:
1073 self.sr.unpauseVDIs(vdiList)
1074 finally:
1075 self.sr.unlock()
1076 else:
1077 self._setSizeVirt(size)
1079 self.sizeVirt = vhdutil.getSizeVirt(self.path)
1081 def _setSizeVirt(self, size) -> None:
1082 """WARNING: do not call this method directly unless all VDIs in the
1083 subtree are guaranteed to be unplugged (and remain so for the duration
1084 of the operation): this operation is only safe for offline VHDs"""
1085 jFile = os.path.join(self.sr.path, self.uuid)
1086 vhdutil.setSizeVirt(self.path, size, jFile)
1088 def _queryVHDBlocks(self) -> bytes:
1089 return vhdutil.getBlockBitmap(self.path)
1091 def _getCoalescedSizeData(self):
1092 """Get the data size of the resulting VHD if we coalesce self onto
1093 parent. We calculate the actual size by using the VHD block allocation
1094 information (as opposed to just adding up the two VHD sizes to get an
1095 upper bound)"""
1096 # make sure we don't use stale BAT info from vdi_rec since the child
1097 # was writable all this time
1098 self.delConfig(VDI.DB_VHD_BLOCKS)
1099 blocksChild = self.getVHDBlocks()
1100 blocksParent = self.parent.getVHDBlocks()
1101 numBlocks = Util.countBits(blocksChild, blocksParent)
1102 Util.log("Num combined blocks = %d" % numBlocks)
1103 sizeData = numBlocks * vhdutil.VHD_BLOCK_SIZE
1104 assert(sizeData <= self.sizeVirt)
1105 return sizeData
1107 def _calcExtraSpaceForCoalescing(self) -> int:
1108 sizeData = self._getCoalescedSizeData()
1109 sizeCoalesced = sizeData + vhdutil.calcOverheadBitmap(sizeData) + \
1110 vhdutil.calcOverheadEmpty(self.sizeVirt)
1111 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1112 return sizeCoalesced - self.parent.getSizeVHD()
1114 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1115 """How much extra space in the SR will be required to
1116 [live-]leaf-coalesce this VDI"""
1117 # the space requirements are the same as for inline coalesce
1118 return self._calcExtraSpaceForCoalescing()
1120 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1121 """How much extra space in the SR will be required to
1122 snapshot-coalesce this VDI"""
1123 return self._calcExtraSpaceForCoalescing() + \
1124 vhdutil.calcOverheadEmpty(self.sizeVirt) # extra snap leaf
1126 def _getAllSubtree(self):
1127 """Get self and all VDIs in the subtree of self as a flat list"""
1128 vdiList = [self]
1129 for child in self.children:
1130 vdiList.extend(child._getAllSubtree())
1131 return vdiList
1134class FileVDI(VDI):
1135 """Object representing a VDI in a file-based SR (EXT or NFS)"""
1137 @staticmethod
1138 def extractUuid(path):
1139 path = os.path.basename(path.strip())
1140 if not (path.endswith(vhdutil.FILE_EXTN_VHD) or \ 1140 ↛ 1142line 1140 didn't jump to line 1142, because the condition on line 1140 was never true
1141 path.endswith(vhdutil.FILE_EXTN_RAW)):
1142 return None
1143 uuid = path.replace(vhdutil.FILE_EXTN_VHD, "").replace( \
1144 vhdutil.FILE_EXTN_RAW, "")
1145 # TODO: validate UUID format
1146 return uuid
1148 def __init__(self, sr, uuid, raw):
1149 VDI.__init__(self, sr, uuid, raw)
1150 if self.raw: 1150 ↛ 1151line 1150 didn't jump to line 1151, because the condition on line 1150 was never true
1151 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_RAW)
1152 else:
1153 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_VHD)
1155 @override
1156 def load(self, info=None) -> None:
1157 if not info:
1158 if not util.pathexists(self.path):
1159 raise util.SMException("%s not found" % self.path)
1160 try:
1161 info = vhdutil.getVHDInfo(self.path, self.extractUuid)
1162 except util.SMException:
1163 Util.log(" [VDI %s: failed to read VHD metadata]" % self.uuid)
1164 return
1165 self.parent = None
1166 self.children = []
1167 self.parentUuid = info.parentUuid
1168 self.sizeVirt = info.sizeVirt
1169 self._sizeVHD = info.sizePhys
1170 self._sizeAllocated = info.sizeAllocated
1171 self.hidden = info.hidden
1172 self.scanError = False
1173 self.path = os.path.join(self.sr.path, "%s%s" % \
1174 (self.uuid, vhdutil.FILE_EXTN_VHD))
1176 @override
1177 def rename(self, uuid) -> None:
1178 oldPath = self.path
1179 VDI.rename(self, uuid)
1180 self.fileName = "%s%s" % (self.uuid, vhdutil.FILE_EXTN_VHD)
1181 self.path = os.path.join(self.sr.path, self.fileName)
1182 assert(not util.pathexists(self.path))
1183 Util.log("Renaming %s -> %s" % (oldPath, self.path))
1184 os.rename(oldPath, self.path)
1186 @override
1187 def delete(self) -> None:
1188 if len(self.children) > 0: 1188 ↛ 1189line 1188 didn't jump to line 1189, because the condition on line 1188 was never true
1189 raise util.SMException("VDI %s has children, can't delete" % \
1190 self.uuid)
1191 try:
1192 self.sr.lock()
1193 try:
1194 os.unlink(self.path)
1195 self.sr.forgetVDI(self.uuid)
1196 finally:
1197 self.sr.unlock()
1198 except OSError:
1199 raise util.SMException("os.unlink(%s) failed" % self.path)
1200 VDI.delete(self)
1202 @override
1203 def getAllocatedSize(self) -> int:
1204 if self._sizeAllocated == -1: 1204 ↛ 1205line 1204 didn't jump to line 1205, because the condition on line 1204 was never true
1205 self._sizeAllocated = vhdutil.getAllocatedSize(self.path)
1206 return self._sizeAllocated
1209class LVHDVDI(VDI):
1210 """Object representing a VDI in an LVHD SR"""
1212 JRN_ZERO = "zero" # journal entry type for zeroing out end of parent
1213 DRIVER_NAME_RAW = "aio"
1215 @override
1216 def load(self, info=None) -> None:
1217 # `info` is always set. `None` default value is only here to match parent method.
1218 assert info, "No info given to LVHDVDI.load"
1219 self.parent = None
1220 self.children = []
1221 self._sizeVHD = -1
1222 self._sizeAllocated = -1
1223 self.scanError = info.scanError
1224 self.sizeLV = info.sizeLV
1225 self.sizeVirt = info.sizeVirt
1226 self.fileName = info.lvName
1227 self.lvActive = info.lvActive
1228 self.lvOpen = info.lvOpen
1229 self.lvReadonly = info.lvReadonly
1230 self.hidden = info.hidden
1231 self.parentUuid = info.parentUuid
1232 self.path = os.path.join(self.sr.path, self.fileName)
1234 @staticmethod
1235 def extractUuid(path):
1236 return lvhdutil.extractUuid(path)
1238 @override
1239 def getDriverName(self) -> str:
1240 if self.raw:
1241 return self.DRIVER_NAME_RAW
1242 return self.DRIVER_NAME_VHD
1244 def inflate(self, size):
1245 """inflate the LV containing the VHD to 'size'"""
1246 if self.raw:
1247 return
1248 self._activate()
1249 self.sr.lock()
1250 try:
1251 lvhdutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, size)
1252 util.fistpoint.activate("LVHDRT_inflating_the_parent", self.sr.uuid)
1253 finally:
1254 self.sr.unlock()
1255 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1256 self._sizeVHD = -1
1257 self._sizeAllocated = -1
1259 def deflate(self):
1260 """deflate the LV containing the VHD to minimum"""
1261 if self.raw:
1262 return
1263 self._activate()
1264 self.sr.lock()
1265 try:
1266 lvhdutil.deflate(self.sr.lvmCache, self.fileName, self.getSizeVHD())
1267 finally:
1268 self.sr.unlock()
1269 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1270 self._sizeVHD = -1
1271 self._sizeAllocated = -1
1273 def inflateFully(self):
1274 self.inflate(lvhdutil.calcSizeVHDLV(self.sizeVirt))
1276 def inflateParentForCoalesce(self):
1277 """Inflate the parent only as much as needed for the purposes of
1278 coalescing"""
1279 if self.parent.raw:
1280 return
1281 inc = self._calcExtraSpaceForCoalescing()
1282 if inc > 0:
1283 util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent", self.sr.uuid)
1284 self.parent.inflate(self.parent.sizeLV + inc)
1286 @override
1287 def updateBlockInfo(self) -> Optional[str]:
1288 if not self.raw:
1289 return VDI.updateBlockInfo(self)
1290 return None
1292 @override
1293 def rename(self, uuid) -> None:
1294 oldUuid = self.uuid
1295 oldLVName = self.fileName
1296 VDI.rename(self, uuid)
1297 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + self.uuid
1298 if self.raw:
1299 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + self.uuid
1300 self.path = os.path.join(self.sr.path, self.fileName)
1301 assert(not self.sr.lvmCache.checkLV(self.fileName))
1303 self.sr.lvmCache.rename(oldLVName, self.fileName)
1304 if self.sr.lvActivator.get(oldUuid, False):
1305 self.sr.lvActivator.replace(oldUuid, self.uuid, self.fileName, False)
1307 ns = lvhdutil.NS_PREFIX_LVM + self.sr.uuid
1308 (cnt, bcnt) = RefCounter.check(oldUuid, ns)
1309 RefCounter.set(self.uuid, cnt, bcnt, ns)
1310 RefCounter.reset(oldUuid, ns)
1312 @override
1313 def delete(self) -> None:
1314 if len(self.children) > 0:
1315 raise util.SMException("VDI %s has children, can't delete" % \
1316 self.uuid)
1317 self.sr.lock()
1318 try:
1319 self.sr.lvmCache.remove(self.fileName)
1320 self.sr.forgetVDI(self.uuid)
1321 finally:
1322 self.sr.unlock()
1323 RefCounter.reset(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
1324 VDI.delete(self)
1326 @override
1327 def getSizeVHD(self) -> int:
1328 if self._sizeVHD == -1:
1329 self._loadInfoSizeVHD()
1330 return self._sizeVHD
1332 def _loadInfoSizeVHD(self):
1333 """Get the physical utilization of the VHD file. We do it individually
1334 (and not using the VHD batch scanner) as an optimization: this info is
1335 relatively expensive and we need it only for VDI's involved in
1336 coalescing."""
1337 if self.raw:
1338 return
1339 self._activate()
1340 self._sizeVHD = vhdutil.getSizePhys(self.path)
1341 if self._sizeVHD <= 0:
1342 raise util.SMException("phys size of %s = %d" % \
1343 (self, self._sizeVHD))
1345 @override
1346 def getAllocatedSize(self) -> int:
1347 if self._sizeAllocated == -1:
1348 self._loadInfoSizeAllocated()
1349 return self._sizeAllocated
1351 def _loadInfoSizeAllocated(self):
1352 """
1353 Get the allocated size of the VHD volume.
1354 """
1355 if self.raw:
1356 return
1357 self._activate()
1358 self._sizeAllocated = vhdutil.getAllocatedSize(self.path)
1360 @override
1361 def _loadInfoHidden(self) -> None:
1362 if self.raw:
1363 self.hidden = self.sr.lvmCache.getHidden(self.fileName)
1364 else:
1365 VDI._loadInfoHidden(self)
1367 @override
1368 def _setHidden(self, hidden=True) -> None:
1369 if self.raw:
1370 self.sr.lvmCache.setHidden(self.fileName, hidden)
1371 self.hidden = hidden
1372 else:
1373 VDI._setHidden(self, hidden)
1375 @override
1376 def __str__(self) -> str:
1377 strType = "VHD"
1378 if self.raw:
1379 strType = "RAW"
1380 strHidden = ""
1381 if self.hidden:
1382 strHidden = "*"
1383 strSizeVHD = ""
1384 if self._sizeVHD > 0:
1385 strSizeVHD = Util.num2str(self._sizeVHD)
1386 strSizeAllocated = ""
1387 if self._sizeAllocated >= 0:
1388 strSizeAllocated = Util.num2str(self._sizeAllocated)
1389 strActive = "n"
1390 if self.lvActive:
1391 strActive = "a"
1392 if self.lvOpen:
1393 strActive += "o"
1394 return "%s%s[%s](%s/%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType,
1395 Util.num2str(self.sizeVirt), strSizeVHD, strSizeAllocated,
1396 Util.num2str(self.sizeLV), strActive)
1398 @override
1399 def validate(self, fast=False) -> None:
1400 if not self.raw:
1401 VDI.validate(self, fast)
1403 @override
1404 def _doCoalesce(self) -> None:
1405 """LVHD parents must first be activated, inflated, and made writable"""
1406 try:
1407 self._activateChain()
1408 self.sr.lvmCache.setReadonly(self.parent.fileName, False)
1409 self.parent.validate()
1410 self.inflateParentForCoalesce()
1411 VDI._doCoalesce(self)
1412 finally:
1413 self.parent._loadInfoSizeVHD()
1414 self.parent.deflate()
1415 self.sr.lvmCache.setReadonly(self.parent.fileName, True)
1417 @override
1418 def _setParent(self, parent) -> None:
1419 self._activate()
1420 if self.lvReadonly:
1421 self.sr.lvmCache.setReadonly(self.fileName, False)
1423 try:
1424 vhdutil.setParent(self.path, parent.path, parent.raw)
1425 finally:
1426 if self.lvReadonly:
1427 self.sr.lvmCache.setReadonly(self.fileName, True)
1428 self._deactivate()
1429 self.parent = parent
1430 self.parentUuid = parent.uuid
1431 parent.children.append(self)
1432 try:
1433 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1434 Util.log("Updated the vhd-parent field for child %s with %s" % \
1435 (self.uuid, self.parentUuid))
1436 except:
1437 Util.log("Failed to update the vhd-parent with %s for child %s" % \
1438 (self.parentUuid, self.uuid))
1440 def _activate(self):
1441 self.sr.lvActivator.activate(self.uuid, self.fileName, False)
1443 def _activateChain(self):
1444 vdi = self
1445 while vdi:
1446 vdi._activate()
1447 vdi = vdi.parent
1449 def _deactivate(self):
1450 self.sr.lvActivator.deactivate(self.uuid, False)
1452 @override
1453 def _increaseSizeVirt(self, size, atomic=True) -> None:
1454 "ensure the virtual size of 'self' is at least 'size'"
1455 self._activate()
1456 if not self.raw:
1457 VDI._increaseSizeVirt(self, size, atomic)
1458 return
1460 # raw VDI case
1461 offset = self.sizeLV
1462 if self.sizeVirt < size:
1463 oldSize = self.sizeLV
1464 self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size)
1465 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV))
1466 self.sr.lvmCache.setSize(self.fileName, self.sizeLV)
1467 offset = oldSize
1468 unfinishedZero = False
1469 jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid)
1470 if jval:
1471 unfinishedZero = True
1472 offset = int(jval)
1473 length = self.sizeLV - offset
1474 if not length:
1475 return
1477 if unfinishedZero:
1478 Util.log(" ==> Redoing unfinished zeroing out")
1479 else:
1480 self.sr.journaler.create(self.JRN_ZERO, self.uuid, \
1481 str(offset))
1482 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
1483 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1484 func = lambda: util.zeroOut(self.path, offset, length)
1485 Util.runAbortable(func, True, self.sr.uuid, abortTest,
1486 VDI.POLL_INTERVAL, 0)
1487 self.sr.journaler.remove(self.JRN_ZERO, self.uuid)
1489 @override
1490 def _setSizeVirt(self, size) -> None:
1491 """WARNING: do not call this method directly unless all VDIs in the
1492 subtree are guaranteed to be unplugged (and remain so for the duration
1493 of the operation): this operation is only safe for offline VHDs"""
1494 self._activate()
1495 jFile = lvhdutil.createVHDJournalLV(self.sr.lvmCache, self.uuid,
1496 vhdutil.MAX_VHD_JOURNAL_SIZE)
1497 try:
1498 lvhdutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid,
1499 size, jFile)
1500 finally:
1501 lvhdutil.deleteVHDJournalLV(self.sr.lvmCache, self.uuid)
1503 @override
1504 def _queryVHDBlocks(self) -> bytes:
1505 self._activate()
1506 return VDI._queryVHDBlocks(self)
1508 @override
1509 def _calcExtraSpaceForCoalescing(self) -> int:
1510 if self.parent.raw:
1511 return 0 # raw parents are never deflated in the first place
1512 sizeCoalesced = lvhdutil.calcSizeVHDLV(self._getCoalescedSizeData())
1513 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1514 return sizeCoalesced - self.parent.sizeLV
1516 @override
1517 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1518 """How much extra space in the SR will be required to
1519 [live-]leaf-coalesce this VDI"""
1520 # we can deflate the leaf to minimize the space requirements
1521 deflateDiff = self.sizeLV - lvhdutil.calcSizeLV(self.getSizeVHD())
1522 return self._calcExtraSpaceForCoalescing() - deflateDiff
1524 @override
1525 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1526 return self._calcExtraSpaceForCoalescing() + \
1527 lvhdutil.calcSizeLV(self.getSizeVHD())
1530class LinstorVDI(VDI):
1531 """Object representing a VDI in a LINSTOR SR"""
1533 VOLUME_LOCK_TIMEOUT = 30
1535 @override
1536 def load(self, info=None) -> None:
1537 self.parentUuid = info.parentUuid
1538 self.scanError = True
1539 self.parent = None
1540 self.children = []
1542 self.fileName = self.sr._linstor.get_volume_name(self.uuid)
1543 self.path = self.sr._linstor.build_device_path(self.fileName)
1545 if not info:
1546 try:
1547 info = self.sr._vhdutil.get_vhd_info(self.uuid)
1548 except util.SMException:
1549 Util.log(
1550 ' [VDI {}: failed to read VHD metadata]'.format(self.uuid)
1551 )
1552 return
1554 self.parentUuid = info.parentUuid
1555 self.sizeVirt = info.sizeVirt
1556 self._sizeVHD = -1
1557 self._sizeAllocated = -1
1558 self.drbd_size = -1
1559 self.hidden = info.hidden
1560 self.scanError = False
1561 self.vdi_type = vhdutil.VDI_TYPE_VHD
1563 @override
1564 def getSizeVHD(self, fetch=False) -> int:
1565 if self._sizeVHD < 0 or fetch:
1566 self._sizeVHD = self.sr._vhdutil.get_size_phys(self.uuid)
1567 return self._sizeVHD
1569 def getDrbdSize(self, fetch=False):
1570 if self.drbd_size < 0 or fetch:
1571 self.drbd_size = self.sr._vhdutil.get_drbd_size(self.uuid)
1572 return self.drbd_size
1574 @override
1575 def getAllocatedSize(self) -> int:
1576 if self._sizeAllocated == -1:
1577 if not self.raw:
1578 self._sizeAllocated = self.sr._vhdutil.get_allocated_size(self.uuid)
1579 return self._sizeAllocated
1581 def inflate(self, size):
1582 if self.raw:
1583 return
1584 self.sr.lock()
1585 try:
1586 # Ensure we use the real DRBD size and not the cached one.
1587 # Why? Because this attribute can be changed if volume is resized by user.
1588 self.drbd_size = self.getDrbdSize(fetch=True)
1589 self.sr._vhdutil.inflate(self.sr.journaler, self.uuid, self.path, size, self.drbd_size)
1590 finally:
1591 self.sr.unlock()
1592 self.drbd_size = -1
1593 self._sizeVHD = -1
1594 self._sizeAllocated = -1
1596 def deflate(self):
1597 if self.raw:
1598 return
1599 self.sr.lock()
1600 try:
1601 # Ensure we use the real sizes and not the cached info.
1602 self.drbd_size = self.getDrbdSize(fetch=True)
1603 self._sizeVHD = self.getSizeVHD(fetch=True)
1604 self.sr._vhdutil.force_deflate(self.path, self._sizeVHD, self.drbd_size, zeroize=False)
1605 finally:
1606 self.sr.unlock()
1607 self.drbd_size = -1
1608 self._sizeVHD = -1
1609 self._sizeAllocated = -1
1611 def inflateFully(self):
1612 if not self.raw:
1613 self.inflate(LinstorVhdUtil.compute_volume_size(self.sizeVirt, self.vdi_type))
1615 @override
1616 def rename(self, uuid) -> None:
1617 Util.log('Renaming {} -> {} (path={})'.format(
1618 self.uuid, uuid, self.path
1619 ))
1620 self.sr._linstor.update_volume_uuid(self.uuid, uuid)
1621 VDI.rename(self, uuid)
1623 @override
1624 def delete(self) -> None:
1625 if len(self.children) > 0:
1626 raise util.SMException(
1627 'VDI {} has children, can\'t delete'.format(self.uuid)
1628 )
1629 self.sr.lock()
1630 try:
1631 self.sr._linstor.destroy_volume(self.uuid)
1632 self.sr.forgetVDI(self.uuid)
1633 finally:
1634 self.sr.unlock()
1635 VDI.delete(self)
1637 @override
1638 def validate(self, fast=False) -> None:
1639 if not self.raw and not self.sr._vhdutil.check(self.uuid, fast=fast):
1640 raise util.SMException('VHD {} corrupted'.format(self))
1642 @override
1643 def pause(self, failfast=False) -> None:
1644 self.sr._linstor.ensure_volume_is_not_locked(
1645 self.uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1646 )
1647 return super(LinstorVDI, self).pause(failfast)
1649 @override
1650 def coalesce(self) -> int:
1651 # Note: We raise `SMException` here to skip the current coalesce in case of failure.
1652 # Using another exception we can't execute the next coalesce calls.
1653 return self.sr._vhdutil.force_coalesce(self.path) * 512
1655 @override
1656 def getParent(self) -> str:
1657 return self.sr._vhdutil.get_parent(
1658 self.sr._linstor.get_volume_uuid_from_device_path(self.path)
1659 )
1661 @override
1662 def repair(self, parent_uuid) -> None:
1663 self.sr._vhdutil.force_repair(
1664 self.sr._linstor.get_device_path(parent_uuid)
1665 )
1667 @override
1668 def _relinkSkip(self) -> None:
1669 abortFlag = IPCFlag(self.sr.uuid)
1670 for child in self.children:
1671 if abortFlag.test(FLAG_TYPE_ABORT):
1672 raise AbortException('Aborting due to signal')
1673 Util.log(
1674 ' Relinking {} from {} to {}'.format(
1675 child, self, self.parent
1676 )
1677 )
1679 session = child.sr.xapi.session
1680 sr_uuid = child.sr.uuid
1681 vdi_uuid = child.uuid
1682 try:
1683 self.sr._linstor.ensure_volume_is_not_locked(
1684 vdi_uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1685 )
1686 blktap2.VDI.tap_pause(session, sr_uuid, vdi_uuid)
1687 child._setParent(self.parent)
1688 finally:
1689 blktap2.VDI.tap_unpause(session, sr_uuid, vdi_uuid)
1690 self.children = []
1692 @override
1693 def _setParent(self, parent) -> None:
1694 self.sr._linstor.get_device_path(self.uuid)
1695 self.sr._vhdutil.force_parent(self.path, parent.path)
1696 self.parent = parent
1697 self.parentUuid = parent.uuid
1698 parent.children.append(self)
1699 try:
1700 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1701 Util.log("Updated the vhd-parent field for child %s with %s" % \
1702 (self.uuid, self.parentUuid))
1703 except:
1704 Util.log("Failed to update %s with vhd-parent field %s" % \
1705 (self.uuid, self.parentUuid))
1707 @override
1708 def _doCoalesce(self) -> None:
1709 try:
1710 self._activateChain()
1711 self.parent.validate()
1712 self._inflateParentForCoalesce()
1713 VDI._doCoalesce(self)
1714 finally:
1715 self.parent.deflate()
1717 def _activateChain(self):
1718 vdi = self
1719 while vdi:
1720 try:
1721 p = self.sr._linstor.get_device_path(vdi.uuid)
1722 except Exception as e:
1723 # Use SMException to skip coalesce.
1724 # Otherwise the GC is stopped...
1725 raise util.SMException(str(e))
1726 vdi = vdi.parent
1728 @override
1729 def _setHidden(self, hidden=True) -> None:
1730 HIDDEN_TAG = 'hidden'
1732 if self.raw:
1733 self.sr._linstor.update_volume_metadata(self.uuid, {
1734 HIDDEN_TAG: hidden
1735 })
1736 self.hidden = hidden
1737 else:
1738 VDI._setHidden(self, hidden)
1740 @override
1741 def _setSizeVirt(self, size) -> None:
1742 jfile = self.uuid + '-jvhd'
1743 self.sr._linstor.create_volume(
1744 jfile, vhdutil.MAX_VHD_JOURNAL_SIZE, persistent=False, volume_name=jfile
1745 )
1746 try:
1747 self.inflate(LinstorVhdUtil.compute_volume_size(size, self.vdi_type))
1748 self.sr._vhdutil.set_size_virt(size, jfile)
1749 finally:
1750 try:
1751 self.sr._linstor.destroy_volume(jfile)
1752 except Exception:
1753 # We can ignore it, in any case this volume is not persistent.
1754 pass
1756 @override
1757 def _queryVHDBlocks(self) -> bytes:
1758 return self.sr._vhdutil.get_block_bitmap(self.uuid)
1760 def _inflateParentForCoalesce(self):
1761 if self.parent.raw:
1762 return
1763 inc = self._calcExtraSpaceForCoalescing()
1764 if inc > 0:
1765 self.parent.inflate(self.parent.getDrbdSize() + inc)
1767 @override
1768 def _calcExtraSpaceForCoalescing(self) -> int:
1769 if self.parent.raw:
1770 return 0
1771 size_coalesced = LinstorVhdUtil.compute_volume_size(
1772 self._getCoalescedSizeData(), self.vdi_type
1773 )
1774 Util.log("Coalesced size = %s" % Util.num2str(size_coalesced))
1775 return size_coalesced - self.parent.getDrbdSize()
1777 @override
1778 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1779 assert self.getDrbdSize() > 0
1780 assert self.getSizeVHD() > 0
1781 deflate_diff = self.getDrbdSize() - LinstorVolumeManager.round_up_volume_size(self.getSizeVHD())
1782 assert deflate_diff >= 0
1783 return self._calcExtraSpaceForCoalescing() - deflate_diff
1785 @override
1786 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1787 assert self.getSizeVHD() > 0
1788 return self._calcExtraSpaceForCoalescing() + \
1789 LinstorVolumeManager.round_up_volume_size(self.getSizeVHD())
1791################################################################################
1792#
1793# SR
1794#
1795class SR(object):
1796 class LogFilter:
1797 def __init__(self, sr):
1798 self.sr = sr
1799 self.stateLogged = False
1800 self.prevState = {}
1801 self.currState = {}
1803 def logState(self):
1804 changes = ""
1805 self.currState.clear()
1806 for vdi in self.sr.vdiTrees:
1807 self.currState[vdi.uuid] = self._getTreeStr(vdi)
1808 if not self.prevState.get(vdi.uuid) or \
1809 self.prevState[vdi.uuid] != self.currState[vdi.uuid]:
1810 changes += self.currState[vdi.uuid]
1812 for uuid in self.prevState:
1813 if not self.currState.get(uuid):
1814 changes += "Tree %s gone\n" % uuid
1816 result = "SR %s (%d VDIs in %d VHD trees): " % \
1817 (self.sr, len(self.sr.vdis), len(self.sr.vdiTrees))
1819 if len(changes) > 0:
1820 if self.stateLogged:
1821 result += "showing only VHD trees that changed:"
1822 result += "\n%s" % changes
1823 else:
1824 result += "no changes"
1826 for line in result.split("\n"):
1827 Util.log("%s" % line)
1828 self.prevState.clear()
1829 for key, val in self.currState.items():
1830 self.prevState[key] = val
1831 self.stateLogged = True
1833 def logNewVDI(self, uuid):
1834 if self.stateLogged:
1835 Util.log("Found new VDI when scanning: %s" % uuid)
1837 def _getTreeStr(self, vdi, indent=8):
1838 treeStr = "%s%s\n" % (" " * indent, vdi)
1839 for child in vdi.children:
1840 treeStr += self._getTreeStr(child, indent + VDI.STR_TREE_INDENT)
1841 return treeStr
1843 TYPE_FILE = "file"
1844 TYPE_LVHD = "lvhd"
1845 TYPE_LINSTOR = "linstor"
1846 TYPES = [TYPE_LVHD, TYPE_FILE, TYPE_LINSTOR]
1848 LOCK_RETRY_INTERVAL = 3
1849 LOCK_RETRY_ATTEMPTS = 20
1850 LOCK_RETRY_ATTEMPTS_LOCK = 100
1852 SCAN_RETRY_ATTEMPTS = 3
1854 JRN_CLONE = "clone" # journal entry type for the clone operation (from SM)
1855 TMP_RENAME_PREFIX = "OLD_"
1857 KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline"
1858 KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override"
1860 @staticmethod
1861 def getInstance(uuid, xapiSession, createLock=True, force=False):
1862 xapi = XAPI(xapiSession, uuid)
1863 type = normalizeType(xapi.srRecord["type"])
1864 if type == SR.TYPE_FILE:
1865 return FileSR(uuid, xapi, createLock, force)
1866 elif type == SR.TYPE_LVHD:
1867 return LVHDSR(uuid, xapi, createLock, force)
1868 elif type == SR.TYPE_LINSTOR:
1869 return LinstorSR(uuid, xapi, createLock, force)
1870 raise util.SMException("SR type %s not recognized" % type)
1872 def __init__(self, uuid, xapi, createLock, force):
1873 self.logFilter = self.LogFilter(self)
1874 self.uuid = uuid
1875 self.path = ""
1876 self.name = ""
1877 self.vdis = {}
1878 self.vdiTrees = []
1879 self.journaler = None
1880 self.xapi = xapi
1881 self._locked = 0
1882 self._srLock = None
1883 if createLock: 1883 ↛ 1884line 1883 didn't jump to line 1884, because the condition on line 1883 was never true
1884 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
1885 else:
1886 Util.log("Requested no SR locking")
1887 self.name = self.xapi.srRecord["name_label"]
1888 self._failedCoalesceTargets = []
1890 if not self.xapi.isPluggedHere():
1891 if force: 1891 ↛ 1892line 1891 didn't jump to line 1892, because the condition on line 1891 was never true
1892 Util.log("SR %s not attached on this host, ignoring" % uuid)
1893 else:
1894 if not self.wait_for_plug():
1895 raise util.SMException("SR %s not attached on this host" % uuid)
1897 if force: 1897 ↛ 1898line 1897 didn't jump to line 1898, because the condition on line 1897 was never true
1898 Util.log("Not checking if we are Master (SR %s)" % uuid)
1899 elif not self.xapi.isMaster(): 1899 ↛ 1900line 1899 didn't jump to line 1900, because the condition on line 1899 was never true
1900 raise util.SMException("This host is NOT master, will not run")
1902 def wait_for_plug(self):
1903 for _ in range(1, 10):
1904 time.sleep(2)
1905 if self.xapi.isPluggedHere():
1906 return True
1907 return False
1909 def gcEnabled(self, refresh=True):
1910 if refresh:
1911 self.xapi.srRecord = \
1912 self.xapi.session.xenapi.SR.get_record(self.xapi._srRef)
1913 if self.xapi.srRecord["other_config"].get(VDI.DB_GC) == "false":
1914 Util.log("GC is disabled for this SR, abort")
1915 return False
1916 return True
1918 def scan(self, force=False) -> None:
1919 """Scan the SR and load VDI info for each VDI. If called repeatedly,
1920 update VDI objects if they already exist"""
1921 pass
1923 def scanLocked(self, force=False):
1924 self.lock()
1925 try:
1926 self.scan(force)
1927 finally:
1928 self.unlock()
1930 def getVDI(self, uuid):
1931 return self.vdis.get(uuid)
1933 def hasWork(self):
1934 if len(self.findGarbage()) > 0:
1935 return True
1936 if self.findCoalesceable():
1937 return True
1938 if self.findLeafCoalesceable():
1939 return True
1940 if self.needUpdateBlockInfo():
1941 return True
1942 return False
1944 def findCoalesceable(self):
1945 """Find a coalesceable VDI. Return a vdi that should be coalesced
1946 (choosing one among all coalesceable candidates according to some
1947 criteria) or None if there is no VDI that could be coalesced"""
1949 candidates = []
1951 srSwitch = self.xapi.srRecord["other_config"].get(VDI.DB_COALESCE)
1952 if srSwitch == "false":
1953 Util.log("Coalesce disabled for this SR")
1954 return candidates
1956 # finish any VDI for which a relink journal entry exists first
1957 journals = self.journaler.getAll(VDI.JRN_RELINK)
1958 for uuid in journals:
1959 vdi = self.getVDI(uuid)
1960 if vdi and vdi not in self._failedCoalesceTargets:
1961 return vdi
1963 for vdi in self.vdis.values():
1964 if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets:
1965 candidates.append(vdi)
1966 Util.log("%s is coalescable" % vdi.uuid)
1968 self.xapi.update_task_progress("coalescable", len(candidates))
1970 # pick one in the tallest tree
1971 treeHeight = dict()
1972 for c in candidates:
1973 height = c.getTreeRoot().getTreeHeight()
1974 if treeHeight.get(height):
1975 treeHeight[height].append(c)
1976 else:
1977 treeHeight[height] = [c]
1979 freeSpace = self.getFreeSpace()
1980 heights = list(treeHeight.keys())
1981 heights.sort(reverse=True)
1982 for h in heights:
1983 for c in treeHeight[h]:
1984 spaceNeeded = c._calcExtraSpaceForCoalescing()
1985 if spaceNeeded <= freeSpace:
1986 Util.log("Coalesce candidate: %s (tree height %d)" % (c, h))
1987 return c
1988 else:
1989 Util.log("No space to coalesce %s (free space: %d)" % \
1990 (c, freeSpace))
1991 return None
1993 def getSwitch(self, key):
1994 return self.xapi.srRecord["other_config"].get(key)
1996 def forbiddenBySwitch(self, switch, condition, fail_msg):
1997 srSwitch = self.getSwitch(switch)
1998 ret = False
1999 if srSwitch:
2000 ret = srSwitch == condition
2002 if ret:
2003 Util.log(fail_msg)
2005 return ret
2007 def leafCoalesceForbidden(self):
2008 return (self.forbiddenBySwitch(VDI.DB_COALESCE,
2009 "false",
2010 "Coalesce disabled for this SR") or
2011 self.forbiddenBySwitch(VDI.DB_LEAFCLSC,
2012 VDI.LEAFCLSC_DISABLED,
2013 "Leaf-coalesce disabled for this SR"))
2015 def findLeafCoalesceable(self):
2016 """Find leaf-coalesceable VDIs in each VHD tree"""
2018 candidates = []
2019 if self.leafCoalesceForbidden():
2020 return candidates
2022 self.gatherLeafCoalesceable(candidates)
2024 self.xapi.update_task_progress("coalescable", len(candidates))
2026 freeSpace = self.getFreeSpace()
2027 for candidate in candidates:
2028 # check the space constraints to see if leaf-coalesce is actually
2029 # feasible for this candidate
2030 spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing()
2031 spaceNeededLive = spaceNeeded
2032 if spaceNeeded > freeSpace:
2033 spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing()
2034 if candidate.canLiveCoalesce(self.getStorageSpeed()):
2035 spaceNeeded = spaceNeededLive
2037 if spaceNeeded <= freeSpace:
2038 Util.log("Leaf-coalesce candidate: %s" % candidate)
2039 return candidate
2040 else:
2041 Util.log("No space to leaf-coalesce %s (free space: %d)" % \
2042 (candidate, freeSpace))
2043 if spaceNeededLive <= freeSpace:
2044 Util.log("...but enough space if skip snap-coalesce")
2045 candidate.setConfig(VDI.DB_LEAFCLSC,
2046 VDI.LEAFCLSC_OFFLINE)
2048 return None
2050 def gatherLeafCoalesceable(self, candidates):
2051 for vdi in self.vdis.values():
2052 if not vdi.isLeafCoalesceable():
2053 continue
2054 if vdi in self._failedCoalesceTargets:
2055 continue
2056 if vdi.getConfig(vdi.DB_ONBOOT) == vdi.ONBOOT_RESET:
2057 Util.log("Skipping reset-on-boot %s" % vdi)
2058 continue
2059 if vdi.getConfig(vdi.DB_ALLOW_CACHING):
2060 Util.log("Skipping allow_caching=true %s" % vdi)
2061 continue
2062 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED:
2063 Util.log("Leaf-coalesce disabled for %s" % vdi)
2064 continue
2065 if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or
2066 vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE):
2067 continue
2068 candidates.append(vdi)
2070 def coalesce(self, vdi, dryRun=False):
2071 """Coalesce vdi onto parent"""
2072 Util.log("Coalescing %s -> %s" % (vdi, vdi.parent))
2073 if dryRun: 2073 ↛ 2074line 2073 didn't jump to line 2074, because the condition on line 2073 was never true
2074 return
2076 try:
2077 self._coalesce(vdi)
2078 except util.SMException as e:
2079 if isinstance(e, AbortException): 2079 ↛ 2080line 2079 didn't jump to line 2080, because the condition on line 2079 was never true
2080 self.cleanup()
2081 raise
2082 else:
2083 self._failedCoalesceTargets.append(vdi)
2084 Util.logException("coalesce")
2085 Util.log("Coalesce failed, skipping")
2086 self.cleanup()
2088 def coalesceLeaf(self, vdi, dryRun=False):
2089 """Leaf-coalesce vdi onto parent"""
2090 Util.log("Leaf-coalescing %s -> %s" % (vdi, vdi.parent))
2091 if dryRun:
2092 return
2094 try:
2095 uuid = vdi.uuid
2096 try:
2097 # "vdi" object will no longer be valid after this call
2098 self._coalesceLeaf(vdi)
2099 finally:
2100 vdi = self.getVDI(uuid)
2101 if vdi:
2102 vdi.delConfig(vdi.DB_LEAFCLSC)
2103 except AbortException:
2104 self.cleanup()
2105 raise
2106 except (util.SMException, XenAPI.Failure) as e:
2107 self._failedCoalesceTargets.append(vdi)
2108 Util.logException("leaf-coalesce")
2109 Util.log("Leaf-coalesce failed on %s, skipping" % vdi)
2110 self.cleanup()
2112 def garbageCollect(self, dryRun=False):
2113 vdiList = self.findGarbage()
2114 Util.log("Found %d VDIs for deletion:" % len(vdiList))
2115 for vdi in vdiList:
2116 Util.log(" %s" % vdi)
2117 if not dryRun:
2118 self.deleteVDIs(vdiList)
2119 self.cleanupJournals(dryRun)
2121 def findGarbage(self):
2122 vdiList = []
2123 for vdi in self.vdiTrees:
2124 vdiList.extend(vdi.getAllPrunable())
2125 return vdiList
2127 def deleteVDIs(self, vdiList) -> None:
2128 for vdi in vdiList:
2129 if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT):
2130 raise AbortException("Aborting due to signal")
2131 Util.log("Deleting unlinked VDI %s" % vdi)
2132 self.deleteVDI(vdi)
2134 def deleteVDI(self, vdi) -> None:
2135 assert(len(vdi.children) == 0)
2136 del self.vdis[vdi.uuid]
2137 if vdi.parent: 2137 ↛ 2139line 2137 didn't jump to line 2139, because the condition on line 2137 was never false
2138 vdi.parent.children.remove(vdi)
2139 if vdi in self.vdiTrees: 2139 ↛ 2140line 2139 didn't jump to line 2140, because the condition on line 2139 was never true
2140 self.vdiTrees.remove(vdi)
2141 vdi.delete()
2143 def forgetVDI(self, vdiUuid) -> None:
2144 self.xapi.forgetVDI(self.uuid, vdiUuid)
2146 def pauseVDIs(self, vdiList) -> None:
2147 paused = []
2148 failed = False
2149 for vdi in vdiList:
2150 try:
2151 vdi.pause()
2152 paused.append(vdi)
2153 except:
2154 Util.logException("pauseVDIs")
2155 failed = True
2156 break
2158 if failed:
2159 self.unpauseVDIs(paused)
2160 raise util.SMException("Failed to pause VDIs")
2162 def unpauseVDIs(self, vdiList):
2163 failed = False
2164 for vdi in vdiList:
2165 try:
2166 vdi.unpause()
2167 except:
2168 Util.log("ERROR: Failed to unpause VDI %s" % vdi)
2169 failed = True
2170 if failed:
2171 raise util.SMException("Failed to unpause VDIs")
2173 def getFreeSpace(self) -> int:
2174 return 0
2176 def cleanup(self):
2177 Util.log("In cleanup")
2178 return
2180 @override
2181 def __str__(self) -> str:
2182 if self.name:
2183 ret = "%s ('%s')" % (self.uuid[0:4], self.name)
2184 else:
2185 ret = "%s" % self.uuid
2186 return ret
2188 def lock(self):
2189 """Acquire the SR lock. Nested acquire()'s are ok. Check for Abort
2190 signal to avoid deadlocking (trying to acquire the SR lock while the
2191 lock is held by a process that is trying to abort us)"""
2192 if not self._srLock:
2193 return
2195 if self._locked == 0:
2196 abortFlag = IPCFlag(self.uuid)
2197 for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK):
2198 if self._srLock.acquireNoblock():
2199 self._locked += 1
2200 return
2201 if abortFlag.test(FLAG_TYPE_ABORT):
2202 raise AbortException("Abort requested")
2203 time.sleep(SR.LOCK_RETRY_INTERVAL)
2204 raise util.SMException("Unable to acquire the SR lock")
2206 self._locked += 1
2208 def unlock(self):
2209 if not self._srLock: 2209 ↛ 2211line 2209 didn't jump to line 2211, because the condition on line 2209 was never false
2210 return
2211 assert(self._locked > 0)
2212 self._locked -= 1
2213 if self._locked == 0:
2214 self._srLock.release()
2216 def needUpdateBlockInfo(self) -> bool:
2217 for vdi in self.vdis.values():
2218 if vdi.scanError or len(vdi.children) == 0:
2219 continue
2220 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2221 return True
2222 return False
2224 def updateBlockInfo(self) -> None:
2225 for vdi in self.vdis.values():
2226 if vdi.scanError or len(vdi.children) == 0:
2227 continue
2228 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2229 vdi.updateBlockInfo()
2231 def cleanupCoalesceJournals(self):
2232 """Remove stale coalesce VDI indicators"""
2233 entries = self.journaler.getAll(VDI.JRN_COALESCE)
2234 for uuid, jval in entries.items():
2235 self.journaler.remove(VDI.JRN_COALESCE, uuid)
2237 def cleanupJournals(self, dryRun=False):
2238 """delete journal entries for non-existing VDIs"""
2239 for t in [LVHDVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]:
2240 entries = self.journaler.getAll(t)
2241 for uuid, jval in entries.items():
2242 if self.getVDI(uuid):
2243 continue
2244 if t == SR.JRN_CLONE:
2245 baseUuid, clonUuid = jval.split("_")
2246 if self.getVDI(baseUuid):
2247 continue
2248 Util.log(" Deleting stale '%s' journal entry for %s "
2249 "(%s)" % (t, uuid, jval))
2250 if not dryRun:
2251 self.journaler.remove(t, uuid)
2253 def cleanupCache(self, maxAge=-1) -> int:
2254 return 0
2256 def _coalesce(self, vdi):
2257 if self.journaler.get(vdi.JRN_RELINK, vdi.uuid): 2257 ↛ 2260line 2257 didn't jump to line 2260, because the condition on line 2257 was never true
2258 # this means we had done the actual coalescing already and just
2259 # need to finish relinking and/or refreshing the children
2260 Util.log("==> Coalesce apparently already done: skipping")
2261 else:
2262 # JRN_COALESCE is used to check which VDI is being coalesced in
2263 # order to decide whether to abort the coalesce. We remove the
2264 # journal as soon as the VHD coalesce step is done, because we
2265 # don't expect the rest of the process to take long
2266 self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1")
2267 vdi._doCoalesce()
2268 self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid)
2270 util.fistpoint.activate("LVHDRT_before_create_relink_journal", self.uuid)
2272 # we now need to relink the children: lock the SR to prevent ops
2273 # like SM.clone from manipulating the VDIs we'll be relinking and
2274 # rescan the SR first in case the children changed since the last
2275 # scan
2276 self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1")
2278 self.lock()
2279 try:
2280 vdi.parent._tagChildrenForRelink()
2281 self.scan()
2282 vdi._relinkSkip()
2283 finally:
2284 self.unlock()
2285 # Reload the children to leave things consistent
2286 vdi.parent._reloadChildren(vdi)
2288 self.journaler.remove(vdi.JRN_RELINK, vdi.uuid)
2289 self.deleteVDI(vdi)
2291 class CoalesceTracker:
2292 GRACE_ITERATIONS = 1
2293 MAX_ITERATIONS_NO_PROGRESS = 3
2294 MAX_ITERATIONS = 10
2295 MAX_INCREASE_FROM_MINIMUM = 1.2
2296 HISTORY_STRING = "Iteration: {its} -- Initial size {initSize}" \
2297 " --> Final size {finSize}"
2299 def __init__(self, sr):
2300 self.itsNoProgress = 0
2301 self.its = 0
2302 self.minSize = float("inf")
2303 self.history = []
2304 self.reason = ""
2305 self.startSize = None
2306 self.finishSize = None
2307 self.sr = sr
2309 def abortCoalesce(self, prevSize, curSize):
2310 res = False
2312 self.its += 1
2313 self.history.append(self.HISTORY_STRING.format(its=self.its,
2314 initSize=prevSize,
2315 finSize=curSize))
2317 self.finishSize = curSize
2319 if self.startSize is None:
2320 self.startSize = prevSize
2322 if curSize < self.minSize:
2323 self.minSize = curSize
2325 if prevSize < self.minSize:
2326 self.minSize = prevSize
2328 if prevSize < curSize:
2329 self.itsNoProgress += 1
2330 Util.log("No progress, attempt:"
2331 " {attempt}".format(attempt=self.itsNoProgress))
2332 util.fistpoint.activate("cleanup_tracker_no_progress", self.sr.uuid)
2334 if (not res) and (self.its > self.MAX_ITERATIONS):
2335 max = self.MAX_ITERATIONS
2336 self.reason = \
2337 "Max iterations ({max}) exceeded".format(max=max)
2338 res = True
2340 if (not res) and (self.itsNoProgress >
2341 self.MAX_ITERATIONS_NO_PROGRESS):
2342 max = self.MAX_ITERATIONS_NO_PROGRESS
2343 self.reason = \
2344 "No progress made for {max} iterations".format(max=max)
2345 res = True
2347 maxSizeFromMin = self.MAX_INCREASE_FROM_MINIMUM * self.minSize
2348 if (self.its > self.GRACE_ITERATIONS and
2349 (not res) and (curSize > maxSizeFromMin)):
2350 self.reason = "Unexpected bump in size," \
2351 " compared to minimum acheived"
2352 res = True
2354 return res
2356 def printReasoning(self):
2357 Util.log("Aborted coalesce")
2358 for hist in self.history:
2359 Util.log(hist)
2360 Util.log(self.reason)
2361 Util.log("Starting size was {size}"
2362 .format(size=self.startSize))
2363 Util.log("Final size was {size}"
2364 .format(size=self.finishSize))
2365 Util.log("Minimum size acheived was {size}"
2366 .format(size=self.minSize))
2368 def _coalesceLeaf(self, vdi):
2369 """Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot
2370 complete due to external changes, namely vdi_delete and vdi_snapshot
2371 that alter leaf-coalescibility of vdi"""
2372 tracker = self.CoalesceTracker(self)
2373 while not vdi.canLiveCoalesce(self.getStorageSpeed()):
2374 prevSizeVHD = vdi.getSizeVHD()
2375 if not self._snapshotCoalesce(vdi): 2375 ↛ 2376line 2375 didn't jump to line 2376, because the condition on line 2375 was never true
2376 return False
2377 if tracker.abortCoalesce(prevSizeVHD, vdi.getSizeVHD()):
2378 tracker.printReasoning()
2379 raise util.SMException("VDI {uuid} could not be coalesced"
2380 .format(uuid=vdi.uuid))
2381 return self._liveLeafCoalesce(vdi)
2383 def calcStorageSpeed(self, startTime, endTime, vhdSize):
2384 speed = None
2385 total_time = endTime - startTime
2386 if total_time > 0:
2387 speed = float(vhdSize) / float(total_time)
2388 return speed
2390 def writeSpeedToFile(self, speed):
2391 content = []
2392 speedFile = None
2393 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2394 self.lock()
2395 try:
2396 Util.log("Writing to file: {myfile}".format(myfile=path))
2397 lines = ""
2398 if not os.path.isfile(path):
2399 lines = str(speed) + "\n"
2400 else:
2401 speedFile = open(path, "r+")
2402 content = speedFile.readlines()
2403 content.append(str(speed) + "\n")
2404 if len(content) > N_RUNNING_AVERAGE:
2405 del content[0]
2406 lines = "".join(content)
2408 util.atomicFileWrite(path, VAR_RUN, lines)
2409 finally:
2410 if speedFile is not None:
2411 speedFile.close()
2412 Util.log("Closing file: {myfile}".format(myfile=path))
2413 self.unlock()
2415 def recordStorageSpeed(self, startTime, endTime, vhdSize):
2416 speed = self.calcStorageSpeed(startTime, endTime, vhdSize)
2417 if speed is None:
2418 return
2420 self.writeSpeedToFile(speed)
2422 def getStorageSpeed(self):
2423 speedFile = None
2424 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2425 self.lock()
2426 try:
2427 speed = None
2428 if os.path.isfile(path):
2429 speedFile = open(path)
2430 content = speedFile.readlines()
2431 try:
2432 content = [float(i) for i in content]
2433 except ValueError:
2434 Util.log("Something bad in the speed log:{log}".
2435 format(log=speedFile.readlines()))
2436 return speed
2438 if len(content):
2439 speed = sum(content) / float(len(content))
2440 if speed <= 0: 2440 ↛ 2442line 2440 didn't jump to line 2442, because the condition on line 2440 was never true
2441 # Defensive, should be impossible.
2442 Util.log("Bad speed: {speed} calculated for SR: {uuid}".
2443 format(speed=speed, uuid=self.uuid))
2444 speed = None
2445 else:
2446 Util.log("Speed file empty for SR: {uuid}".
2447 format(uuid=self.uuid))
2448 else:
2449 Util.log("Speed log missing for SR: {uuid}".
2450 format(uuid=self.uuid))
2451 return speed
2452 finally:
2453 if not (speedFile is None):
2454 speedFile.close()
2455 self.unlock()
2457 def _snapshotCoalesce(self, vdi):
2458 # Note that because we are not holding any locks here, concurrent SM
2459 # operations may change this tree under our feet. In particular, vdi
2460 # can be deleted, or it can be snapshotted.
2461 assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED)
2462 Util.log("Single-snapshotting %s" % vdi)
2463 util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid)
2464 try:
2465 ret = self.xapi.singleSnapshotVDI(vdi)
2466 Util.log("Single-snapshot returned: %s" % ret)
2467 except XenAPI.Failure as e:
2468 if util.isInvalidVDI(e):
2469 Util.log("The VDI appears to have been concurrently deleted")
2470 return False
2471 raise
2472 self.scanLocked()
2473 tempSnap = vdi.parent
2474 if not tempSnap.isCoalesceable():
2475 Util.log("The VDI appears to have been concurrently snapshotted")
2476 return False
2477 Util.log("Coalescing parent %s" % tempSnap)
2478 util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid)
2479 vhdSize = vdi.getSizeVHD()
2480 self._coalesce(tempSnap)
2481 if not vdi.isLeafCoalesceable():
2482 Util.log("The VDI tree appears to have been altered since")
2483 return False
2484 return True
2486 def _liveLeafCoalesce(self, vdi) -> bool:
2487 util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid)
2488 self.lock()
2489 try:
2490 self.scan()
2491 if not self.getVDI(vdi.uuid):
2492 Util.log("The VDI appears to have been deleted meanwhile")
2493 return False
2494 if not vdi.isLeafCoalesceable():
2495 Util.log("The VDI is no longer leaf-coalesceable")
2496 return False
2498 uuid = vdi.uuid
2499 vdi.pause(failfast=True)
2500 try:
2501 try:
2502 # "vdi" object will no longer be valid after this call
2503 self._doCoalesceLeaf(vdi)
2504 except:
2505 Util.logException("_doCoalesceLeaf")
2506 self._handleInterruptedCoalesceLeaf()
2507 raise
2508 finally:
2509 vdi = self.getVDI(uuid)
2510 if vdi:
2511 vdi.ensureUnpaused()
2512 vdiOld = self.getVDI(self.TMP_RENAME_PREFIX + uuid)
2513 if vdiOld:
2514 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2515 self.deleteVDI(vdiOld)
2516 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2517 finally:
2518 self.cleanup()
2519 self.unlock()
2520 self.logFilter.logState()
2521 return True
2523 def _doCoalesceLeaf(self, vdi):
2524 """Actual coalescing of a leaf VDI onto parent. Must be called in an
2525 offline/atomic context"""
2526 self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid)
2527 self._prepareCoalesceLeaf(vdi)
2528 vdi.parent._setHidden(False)
2529 vdi.parent._increaseSizeVirt(vdi.sizeVirt, False)
2530 vdi.validate(True)
2531 vdi.parent.validate(True)
2532 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
2533 timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT
2534 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE:
2535 Util.log("Leaf-coalesce forced, will not use timeout")
2536 timeout = 0
2537 vdi._coalesceVHD(timeout)
2538 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
2539 vdi.parent.validate(True)
2540 #vdi._verifyContents(timeout / 2)
2542 # rename
2543 vdiUuid = vdi.uuid
2544 oldName = vdi.fileName
2545 origParentUuid = vdi.parent.uuid
2546 vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid)
2547 util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid)
2548 vdi.parent.rename(vdiUuid)
2549 util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid)
2550 self._updateSlavesOnRename(vdi.parent, oldName, origParentUuid)
2552 # Note that "vdi.parent" is now the single remaining leaf and "vdi" is
2553 # garbage
2555 # update the VDI record
2556 vdi.parent.delConfig(VDI.DB_VHD_PARENT)
2557 if vdi.parent.raw:
2558 vdi.parent.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_RAW)
2559 vdi.parent.delConfig(VDI.DB_VHD_BLOCKS)
2560 util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid)
2562 self._updateNode(vdi)
2564 # delete the obsolete leaf & inflate the parent (in that order, to
2565 # minimize free space requirements)
2566 parent = vdi.parent
2567 vdi._setHidden(True)
2568 vdi.parent.children = []
2569 vdi.parent = None
2571 extraSpace = self._calcExtraSpaceNeeded(vdi, parent)
2572 freeSpace = self.getFreeSpace()
2573 if freeSpace < extraSpace:
2574 # don't delete unless we need the space: deletion is time-consuming
2575 # because it requires contacting the slaves, and we're paused here
2576 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2577 self.deleteVDI(vdi)
2578 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2580 util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid)
2581 self.journaler.remove(VDI.JRN_LEAF, vdiUuid)
2583 self.forgetVDI(origParentUuid)
2584 self._finishCoalesceLeaf(parent)
2585 self._updateSlavesOnResize(parent)
2587 def _calcExtraSpaceNeeded(self, child, parent) -> int:
2588 assert(not parent.raw) # raw parents not supported
2589 extra = child.getSizeVHD() - parent.getSizeVHD()
2590 if extra < 0:
2591 extra = 0
2592 return extra
2594 def _prepareCoalesceLeaf(self, vdi) -> None:
2595 pass
2597 def _updateNode(self, vdi) -> None:
2598 pass
2600 def _finishCoalesceLeaf(self, parent) -> None:
2601 pass
2603 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
2604 pass
2606 def _updateSlavesOnRename(self, vdi, oldName, origParentUuid) -> None:
2607 pass
2609 def _updateSlavesOnResize(self, vdi) -> None:
2610 pass
2612 def _removeStaleVDIs(self, uuidsPresent) -> None:
2613 for uuid in list(self.vdis.keys()):
2614 if not uuid in uuidsPresent:
2615 Util.log("VDI %s disappeared since last scan" % \
2616 self.vdis[uuid])
2617 del self.vdis[uuid]
2619 def _handleInterruptedCoalesceLeaf(self) -> None:
2620 """An interrupted leaf-coalesce operation may leave the VHD tree in an
2621 inconsistent state. If the old-leaf VDI is still present, we revert the
2622 operation (in case the original error is persistent); otherwise we must
2623 finish the operation"""
2624 pass
2626 def _buildTree(self, force):
2627 self.vdiTrees = []
2628 for vdi in self.vdis.values():
2629 if vdi.parentUuid:
2630 parent = self.getVDI(vdi.parentUuid)
2631 if not parent:
2632 if vdi.uuid.startswith(self.TMP_RENAME_PREFIX):
2633 self.vdiTrees.append(vdi)
2634 continue
2635 if force:
2636 Util.log("ERROR: Parent VDI %s not found! (for %s)" % \
2637 (vdi.parentUuid, vdi.uuid))
2638 self.vdiTrees.append(vdi)
2639 continue
2640 else:
2641 raise util.SMException("Parent VDI %s of %s not " \
2642 "found" % (vdi.parentUuid, vdi.uuid))
2643 vdi.parent = parent
2644 parent.children.append(vdi)
2645 else:
2646 self.vdiTrees.append(vdi)
2649class FileSR(SR):
2650 TYPE = SR.TYPE_FILE
2651 CACHE_FILE_EXT = ".vhdcache"
2652 # cache cleanup actions
2653 CACHE_ACTION_KEEP = 0
2654 CACHE_ACTION_REMOVE = 1
2655 CACHE_ACTION_REMOVE_IF_INACTIVE = 2
2657 def __init__(self, uuid, xapi, createLock, force):
2658 SR.__init__(self, uuid, xapi, createLock, force)
2659 self.path = "/var/run/sr-mount/%s" % self.uuid
2660 self.journaler = fjournaler.Journaler(self.path)
2662 @override
2663 def scan(self, force=False) -> None:
2664 if not util.pathexists(self.path):
2665 raise util.SMException("directory %s not found!" % self.uuid)
2666 vhds = self._scan(force)
2667 for uuid, vhdInfo in vhds.items():
2668 vdi = self.getVDI(uuid)
2669 if not vdi:
2670 self.logFilter.logNewVDI(uuid)
2671 vdi = FileVDI(self, uuid, False)
2672 self.vdis[uuid] = vdi
2673 vdi.load(vhdInfo)
2674 uuidsPresent = list(vhds.keys())
2675 rawList = [x for x in os.listdir(self.path) if x.endswith(vhdutil.FILE_EXTN_RAW)]
2676 for rawName in rawList:
2677 uuid = FileVDI.extractUuid(rawName)
2678 uuidsPresent.append(uuid)
2679 vdi = self.getVDI(uuid)
2680 if not vdi:
2681 self.logFilter.logNewVDI(uuid)
2682 vdi = FileVDI(self, uuid, True)
2683 self.vdis[uuid] = vdi
2684 self._removeStaleVDIs(uuidsPresent)
2685 self._buildTree(force)
2686 self.logFilter.logState()
2687 self._handleInterruptedCoalesceLeaf()
2689 @override
2690 def getFreeSpace(self) -> int:
2691 return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path)
2693 @override
2694 def deleteVDIs(self, vdiList) -> None:
2695 rootDeleted = False
2696 for vdi in vdiList:
2697 if not vdi.parent:
2698 rootDeleted = True
2699 break
2700 SR.deleteVDIs(self, vdiList)
2701 if self.xapi.srRecord["type"] == "nfs" and rootDeleted:
2702 self.xapi.markCacheSRsDirty()
2704 @override
2705 def cleanupCache(self, maxAge=-1) -> int:
2706 """Clean up IntelliCache cache files. Caches for leaf nodes are
2707 removed when the leaf node no longer exists or its allow-caching
2708 attribute is not set. Caches for parent nodes are removed when the
2709 parent node no longer exists or it hasn't been used in more than
2710 <maxAge> hours.
2711 Return number of caches removed.
2712 """
2713 numRemoved = 0
2714 cacheFiles = [x for x in os.listdir(self.path) if self._isCacheFileName(x)]
2715 Util.log("Found %d cache files" % len(cacheFiles))
2716 cutoff = datetime.datetime.now() - datetime.timedelta(hours=maxAge)
2717 for cacheFile in cacheFiles:
2718 uuid = cacheFile[:-len(self.CACHE_FILE_EXT)]
2719 action = self.CACHE_ACTION_KEEP
2720 rec = self.xapi.getRecordVDI(uuid)
2721 if not rec:
2722 Util.log("Cache %s: VDI doesn't exist" % uuid)
2723 action = self.CACHE_ACTION_REMOVE
2724 elif rec["managed"] and not rec["allow_caching"]:
2725 Util.log("Cache %s: caching disabled" % uuid)
2726 action = self.CACHE_ACTION_REMOVE
2727 elif not rec["managed"] and maxAge >= 0:
2728 lastAccess = datetime.datetime.fromtimestamp( \
2729 os.path.getatime(os.path.join(self.path, cacheFile)))
2730 if lastAccess < cutoff:
2731 Util.log("Cache %s: older than %d hrs" % (uuid, maxAge))
2732 action = self.CACHE_ACTION_REMOVE_IF_INACTIVE
2734 if action == self.CACHE_ACTION_KEEP:
2735 Util.log("Keeping cache %s" % uuid)
2736 continue
2738 lockId = uuid
2739 parentUuid = None
2740 if rec and rec["managed"]:
2741 parentUuid = rec["sm_config"].get("vhd-parent")
2742 if parentUuid:
2743 lockId = parentUuid
2745 cacheLock = lock.Lock(blktap2.VDI.LOCK_CACHE_SETUP, lockId)
2746 cacheLock.acquire()
2747 try:
2748 if self._cleanupCache(uuid, action):
2749 numRemoved += 1
2750 finally:
2751 cacheLock.release()
2752 return numRemoved
2754 def _cleanupCache(self, uuid, action):
2755 assert(action != self.CACHE_ACTION_KEEP)
2756 rec = self.xapi.getRecordVDI(uuid)
2757 if rec and rec["allow_caching"]:
2758 Util.log("Cache %s appears to have become valid" % uuid)
2759 return False
2761 fullPath = os.path.join(self.path, uuid + self.CACHE_FILE_EXT)
2762 tapdisk = blktap2.Tapdisk.find_by_path(fullPath)
2763 if tapdisk:
2764 if action == self.CACHE_ACTION_REMOVE_IF_INACTIVE:
2765 Util.log("Cache %s still in use" % uuid)
2766 return False
2767 Util.log("Shutting down tapdisk for %s" % fullPath)
2768 tapdisk.shutdown()
2770 Util.log("Deleting file %s" % fullPath)
2771 os.unlink(fullPath)
2772 return True
2774 def _isCacheFileName(self, name):
2775 return (len(name) == Util.UUID_LEN + len(self.CACHE_FILE_EXT)) and \
2776 name.endswith(self.CACHE_FILE_EXT)
2778 def _scan(self, force):
2779 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2780 error = False
2781 pattern = os.path.join(self.path, "*%s" % vhdutil.FILE_EXTN_VHD)
2782 vhds = vhdutil.getAllVHDs(pattern, FileVDI.extractUuid)
2783 for uuid, vhdInfo in vhds.items():
2784 if vhdInfo.error:
2785 error = True
2786 break
2787 if not error:
2788 return vhds
2789 Util.log("Scan error on attempt %d" % i)
2790 if force:
2791 return vhds
2792 raise util.SMException("Scan error")
2794 @override
2795 def deleteVDI(self, vdi) -> None:
2796 self._checkSlaves(vdi)
2797 SR.deleteVDI(self, vdi)
2799 def _checkSlaves(self, vdi):
2800 onlineHosts = self.xapi.getOnlineHosts()
2801 abortFlag = IPCFlag(self.uuid)
2802 for pbdRecord in self.xapi.getAttachedPBDs():
2803 hostRef = pbdRecord["host"]
2804 if hostRef == self.xapi._hostRef:
2805 continue
2806 if abortFlag.test(FLAG_TYPE_ABORT):
2807 raise AbortException("Aborting due to signal")
2808 try:
2809 self._checkSlave(hostRef, vdi)
2810 except util.CommandException:
2811 if hostRef in onlineHosts:
2812 raise
2814 def _checkSlave(self, hostRef, vdi):
2815 call = (hostRef, "nfs-on-slave", "check", {'path': vdi.path})
2816 Util.log("Checking with slave: %s" % repr(call))
2817 _host = self.xapi.session.xenapi.host
2818 text = _host.call_plugin( * call)
2820 @override
2821 def _handleInterruptedCoalesceLeaf(self) -> None:
2822 entries = self.journaler.getAll(VDI.JRN_LEAF)
2823 for uuid, parentUuid in entries.items():
2824 fileList = os.listdir(self.path)
2825 childName = uuid + vhdutil.FILE_EXTN_VHD
2826 tmpChildName = self.TMP_RENAME_PREFIX + uuid + vhdutil.FILE_EXTN_VHD
2827 parentName1 = parentUuid + vhdutil.FILE_EXTN_VHD
2828 parentName2 = parentUuid + vhdutil.FILE_EXTN_RAW
2829 parentPresent = (parentName1 in fileList or parentName2 in fileList)
2830 if parentPresent or tmpChildName in fileList:
2831 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
2832 else:
2833 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
2834 self.journaler.remove(VDI.JRN_LEAF, uuid)
2835 vdi = self.getVDI(uuid)
2836 if vdi:
2837 vdi.ensureUnpaused()
2839 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2840 Util.log("*** UNDO LEAF-COALESCE")
2841 parent = self.getVDI(parentUuid)
2842 if not parent:
2843 parent = self.getVDI(childUuid)
2844 if not parent:
2845 raise util.SMException("Neither %s nor %s found" % \
2846 (parentUuid, childUuid))
2847 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
2848 parent.rename(parentUuid)
2849 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
2851 child = self.getVDI(childUuid)
2852 if not child:
2853 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
2854 if not child:
2855 raise util.SMException("Neither %s nor %s found" % \
2856 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
2857 Util.log("Renaming child back to %s" % childUuid)
2858 child.rename(childUuid)
2859 Util.log("Updating the VDI record")
2860 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
2861 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
2862 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
2864 if child.hidden:
2865 child._setHidden(False)
2866 if not parent.hidden:
2867 parent._setHidden(True)
2868 self._updateSlavesOnUndoLeafCoalesce(parent, child)
2869 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
2870 Util.log("*** leaf-coalesce undo successful")
2871 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
2872 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
2874 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2875 Util.log("*** FINISH LEAF-COALESCE")
2876 vdi = self.getVDI(childUuid)
2877 if not vdi:
2878 raise util.SMException("VDI %s not found" % childUuid)
2879 try:
2880 self.forgetVDI(parentUuid)
2881 except XenAPI.Failure:
2882 pass
2883 self._updateSlavesOnResize(vdi)
2884 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
2885 Util.log("*** finished leaf-coalesce successfully")
2888class LVHDSR(SR):
2889 TYPE = SR.TYPE_LVHD
2890 SUBTYPES = ["lvhdoiscsi", "lvhdohba"]
2892 def __init__(self, uuid, xapi, createLock, force):
2893 SR.__init__(self, uuid, xapi, createLock, force)
2894 self.vgName = "%s%s" % (lvhdutil.VG_PREFIX, self.uuid)
2895 self.path = os.path.join(lvhdutil.VG_LOCATION, self.vgName)
2897 sr_ref = self.xapi.session.xenapi.SR.get_by_uuid(self.uuid)
2898 other_conf = self.xapi.session.xenapi.SR.get_other_config(sr_ref)
2899 lvm_conf = other_conf.get('lvm-conf') if other_conf else None
2900 self.lvmCache = lvmcache.LVMCache(self.vgName, lvm_conf)
2902 self.lvActivator = LVActivator(self.uuid, self.lvmCache)
2903 self.journaler = journaler.Journaler(self.lvmCache)
2905 @override
2906 def deleteVDI(self, vdi) -> None:
2907 if self.lvActivator.get(vdi.uuid, False):
2908 self.lvActivator.deactivate(vdi.uuid, False)
2909 self._checkSlaves(vdi)
2910 SR.deleteVDI(self, vdi)
2912 @override
2913 def forgetVDI(self, vdiUuid) -> None:
2914 SR.forgetVDI(self, vdiUuid)
2915 mdpath = os.path.join(self.path, lvutil.MDVOLUME_NAME)
2916 LVMMetadataHandler(mdpath).deleteVdiFromMetadata(vdiUuid)
2918 @override
2919 def getFreeSpace(self) -> int:
2920 stats = lvutil._getVGstats(self.vgName)
2921 return stats['physical_size'] - stats['physical_utilisation']
2923 @override
2924 def cleanup(self):
2925 if not self.lvActivator.deactivateAll():
2926 Util.log("ERROR deactivating LVs while cleaning up")
2928 @override
2929 def needUpdateBlockInfo(self) -> bool:
2930 for vdi in self.vdis.values():
2931 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2932 continue
2933 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2934 return True
2935 return False
2937 @override
2938 def updateBlockInfo(self) -> None:
2939 numUpdated = 0
2940 for vdi in self.vdis.values():
2941 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2942 continue
2943 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2944 vdi.updateBlockInfo()
2945 numUpdated += 1
2946 if numUpdated:
2947 # deactivate the LVs back sooner rather than later. If we don't
2948 # now, by the time this thread gets to deactivations, another one
2949 # might have leaf-coalesced a node and deleted it, making the child
2950 # inherit the refcount value and preventing the correct decrement
2951 self.cleanup()
2953 @override
2954 def scan(self, force=False) -> None:
2955 vdis = self._scan(force)
2956 for uuid, vdiInfo in vdis.items():
2957 vdi = self.getVDI(uuid)
2958 if not vdi:
2959 self.logFilter.logNewVDI(uuid)
2960 vdi = LVHDVDI(self, uuid,
2961 vdiInfo.vdiType == vhdutil.VDI_TYPE_RAW)
2962 self.vdis[uuid] = vdi
2963 vdi.load(vdiInfo)
2964 self._removeStaleVDIs(vdis.keys())
2965 self._buildTree(force)
2966 self.logFilter.logState()
2967 self._handleInterruptedCoalesceLeaf()
2969 def _scan(self, force):
2970 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2971 error = False
2972 self.lvmCache.refresh()
2973 vdis = lvhdutil.getVDIInfo(self.lvmCache)
2974 for uuid, vdiInfo in vdis.items():
2975 if vdiInfo.scanError:
2976 error = True
2977 break
2978 if not error:
2979 return vdis
2980 Util.log("Scan error, retrying (%d)" % i)
2981 if force:
2982 return vdis
2983 raise util.SMException("Scan error")
2985 @override
2986 def _removeStaleVDIs(self, uuidsPresent) -> None:
2987 for uuid in list(self.vdis.keys()):
2988 if not uuid in uuidsPresent:
2989 Util.log("VDI %s disappeared since last scan" % \
2990 self.vdis[uuid])
2991 del self.vdis[uuid]
2992 if self.lvActivator.get(uuid, False):
2993 self.lvActivator.remove(uuid, False)
2995 @override
2996 def _liveLeafCoalesce(self, vdi) -> bool:
2997 """If the parent is raw and the child was resized (virt. size), then
2998 we'll need to resize the parent, which can take a while due to zeroing
2999 out of the extended portion of the LV. Do it before pausing the child
3000 to avoid a protracted downtime"""
3001 if vdi.parent.raw and vdi.sizeVirt > vdi.parent.sizeVirt:
3002 self.lvmCache.setReadonly(vdi.parent.fileName, False)
3003 vdi.parent._increaseSizeVirt(vdi.sizeVirt)
3005 return SR._liveLeafCoalesce(self, vdi)
3007 @override
3008 def _prepareCoalesceLeaf(self, vdi) -> None:
3009 vdi._activateChain()
3010 self.lvmCache.setReadonly(vdi.parent.fileName, False)
3011 vdi.deflate()
3012 vdi.inflateParentForCoalesce()
3014 @override
3015 def _updateNode(self, vdi) -> None:
3016 # fix the refcounts: the remaining node should inherit the binary
3017 # refcount from the leaf (because if it was online, it should remain
3018 # refcounted as such), but the normal refcount from the parent (because
3019 # this node is really the parent node) - minus 1 if it is online (since
3020 # non-leaf nodes increment their normal counts when they are online and
3021 # we are now a leaf, storing that 1 in the binary refcount).
3022 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
3023 cCnt, cBcnt = RefCounter.check(vdi.uuid, ns)
3024 pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns)
3025 pCnt = pCnt - cBcnt
3026 assert(pCnt >= 0)
3027 RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns)
3029 @override
3030 def _finishCoalesceLeaf(self, parent) -> None:
3031 if not parent.isSnapshot() or parent.isAttachedRW():
3032 parent.inflateFully()
3033 else:
3034 parent.deflate()
3036 @override
3037 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3038 return lvhdutil.calcSizeVHDLV(parent.sizeVirt) - parent.sizeLV
3040 @override
3041 def _handleInterruptedCoalesceLeaf(self) -> None:
3042 entries = self.journaler.getAll(VDI.JRN_LEAF)
3043 for uuid, parentUuid in entries.items():
3044 childLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + uuid
3045 tmpChildLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
3046 self.TMP_RENAME_PREFIX + uuid
3047 parentLV1 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + parentUuid
3048 parentLV2 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + parentUuid
3049 parentPresent = (self.lvmCache.checkLV(parentLV1) or \
3050 self.lvmCache.checkLV(parentLV2))
3051 if parentPresent or self.lvmCache.checkLV(tmpChildLV):
3052 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3053 else:
3054 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3055 self.journaler.remove(VDI.JRN_LEAF, uuid)
3056 vdi = self.getVDI(uuid)
3057 if vdi:
3058 vdi.ensureUnpaused()
3060 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3061 Util.log("*** UNDO LEAF-COALESCE")
3062 parent = self.getVDI(parentUuid)
3063 if not parent:
3064 parent = self.getVDI(childUuid)
3065 if not parent:
3066 raise util.SMException("Neither %s nor %s found" % \
3067 (parentUuid, childUuid))
3068 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
3069 parent.rename(parentUuid)
3070 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
3072 child = self.getVDI(childUuid)
3073 if not child:
3074 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3075 if not child:
3076 raise util.SMException("Neither %s nor %s found" % \
3077 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
3078 Util.log("Renaming child back to %s" % childUuid)
3079 child.rename(childUuid)
3080 Util.log("Updating the VDI record")
3081 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
3082 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
3083 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
3085 # refcount (best effort - assume that it had succeeded if the
3086 # second rename succeeded; if not, this adjustment will be wrong,
3087 # leading to a non-deactivation of the LV)
3088 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
3089 cCnt, cBcnt = RefCounter.check(child.uuid, ns)
3090 pCnt, pBcnt = RefCounter.check(parent.uuid, ns)
3091 pCnt = pCnt + cBcnt
3092 RefCounter.set(parent.uuid, pCnt, 0, ns)
3093 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid)
3095 parent.deflate()
3096 child.inflateFully()
3097 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid)
3098 if child.hidden:
3099 child._setHidden(False)
3100 if not parent.hidden:
3101 parent._setHidden(True)
3102 if not parent.lvReadonly:
3103 self.lvmCache.setReadonly(parent.fileName, True)
3104 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3105 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
3106 Util.log("*** leaf-coalesce undo successful")
3107 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
3108 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
3110 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3111 Util.log("*** FINISH LEAF-COALESCE")
3112 vdi = self.getVDI(childUuid)
3113 if not vdi:
3114 raise util.SMException("VDI %s not found" % childUuid)
3115 vdi.inflateFully()
3116 util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid)
3117 try:
3118 self.forgetVDI(parentUuid)
3119 except XenAPI.Failure:
3120 pass
3121 self._updateSlavesOnResize(vdi)
3122 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
3123 Util.log("*** finished leaf-coalesce successfully")
3125 def _checkSlaves(self, vdi):
3126 """Confirm with all slaves in the pool that 'vdi' is not in use. We
3127 try to check all slaves, including those that the Agent believes are
3128 offline, but ignore failures for offline hosts. This is to avoid cases
3129 where the Agent thinks a host is offline but the host is up."""
3130 args = {"vgName": self.vgName,
3131 "action1": "deactivateNoRefcount",
3132 "lvName1": vdi.fileName,
3133 "action2": "cleanupLockAndRefcount",
3134 "uuid2": vdi.uuid,
3135 "ns2": lvhdutil.NS_PREFIX_LVM + self.uuid}
3136 onlineHosts = self.xapi.getOnlineHosts()
3137 abortFlag = IPCFlag(self.uuid)
3138 for pbdRecord in self.xapi.getAttachedPBDs():
3139 hostRef = pbdRecord["host"]
3140 if hostRef == self.xapi._hostRef:
3141 continue
3142 if abortFlag.test(FLAG_TYPE_ABORT):
3143 raise AbortException("Aborting due to signal")
3144 Util.log("Checking with slave %s (path %s)" % (
3145 self.xapi.getRecordHost(hostRef)['hostname'], vdi.path))
3146 try:
3147 self.xapi.ensureInactive(hostRef, args)
3148 except XenAPI.Failure:
3149 if hostRef in onlineHosts:
3150 raise
3152 @override
3153 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
3154 slaves = util.get_slaves_attached_on(self.xapi.session, [child.uuid])
3155 if not slaves:
3156 Util.log("Update-on-leaf-undo: VDI %s not attached on any slave" % \
3157 child)
3158 return
3160 tmpName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
3161 self.TMP_RENAME_PREFIX + child.uuid
3162 args = {"vgName": self.vgName,
3163 "action1": "deactivateNoRefcount",
3164 "lvName1": tmpName,
3165 "action2": "deactivateNoRefcount",
3166 "lvName2": child.fileName,
3167 "action3": "refresh",
3168 "lvName3": child.fileName,
3169 "action4": "refresh",
3170 "lvName4": parent.fileName}
3171 for slave in slaves:
3172 Util.log("Updating %s, %s, %s on slave %s" % \
3173 (tmpName, child.fileName, parent.fileName,
3174 self.xapi.getRecordHost(slave)['hostname']))
3175 text = self.xapi.session.xenapi.host.call_plugin( \
3176 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3177 Util.log("call-plugin returned: '%s'" % text)
3179 @override
3180 def _updateSlavesOnRename(self, vdi, oldNameLV, origParentUuid) -> None:
3181 slaves = util.get_slaves_attached_on(self.xapi.session, [vdi.uuid])
3182 if not slaves:
3183 Util.log("Update-on-rename: VDI %s not attached on any slave" % vdi)
3184 return
3186 args = {"vgName": self.vgName,
3187 "action1": "deactivateNoRefcount",
3188 "lvName1": oldNameLV,
3189 "action2": "refresh",
3190 "lvName2": vdi.fileName,
3191 "action3": "cleanupLockAndRefcount",
3192 "uuid3": origParentUuid,
3193 "ns3": lvhdutil.NS_PREFIX_LVM + self.uuid}
3194 for slave in slaves:
3195 Util.log("Updating %s to %s on slave %s" % \
3196 (oldNameLV, vdi.fileName,
3197 self.xapi.getRecordHost(slave)['hostname']))
3198 text = self.xapi.session.xenapi.host.call_plugin( \
3199 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3200 Util.log("call-plugin returned: '%s'" % text)
3202 @override
3203 def _updateSlavesOnResize(self, vdi) -> None:
3204 uuids = [x.uuid for x in vdi.getAllLeaves()]
3205 slaves = util.get_slaves_attached_on(self.xapi.session, uuids)
3206 if not slaves:
3207 util.SMlog("Update-on-resize: %s not attached on any slave" % vdi)
3208 return
3209 lvhdutil.lvRefreshOnSlaves(self.xapi.session, self.uuid, self.vgName,
3210 vdi.fileName, vdi.uuid, slaves)
3213class LinstorSR(SR):
3214 TYPE = SR.TYPE_LINSTOR
3216 def __init__(self, uuid, xapi, createLock, force):
3217 if not LINSTOR_AVAILABLE:
3218 raise util.SMException(
3219 'Can\'t load cleanup LinstorSR: LINSTOR libraries are missing'
3220 )
3222 SR.__init__(self, uuid, xapi, createLock, force)
3223 self.path = LinstorVolumeManager.DEV_ROOT_PATH
3224 self._reloadLinstor()
3226 @override
3227 def deleteVDI(self, vdi) -> None:
3228 self._checkSlaves(vdi)
3229 SR.deleteVDI(self, vdi)
3231 @override
3232 def getFreeSpace(self) -> int:
3233 return self._linstor.max_volume_size_allowed
3235 @override
3236 def scan(self, force=False) -> None:
3237 all_vdi_info = self._scan(force)
3238 for uuid, vdiInfo in all_vdi_info.items():
3239 # When vdiInfo is None, the VDI is RAW.
3240 vdi = self.getVDI(uuid)
3241 if not vdi:
3242 self.logFilter.logNewVDI(uuid)
3243 vdi = LinstorVDI(self, uuid, not vdiInfo)
3244 self.vdis[uuid] = vdi
3245 if vdiInfo:
3246 vdi.load(vdiInfo)
3247 self._removeStaleVDIs(all_vdi_info.keys())
3248 self._buildTree(force)
3249 self.logFilter.logState()
3250 self._handleInterruptedCoalesceLeaf()
3252 @override
3253 def pauseVDIs(self, vdiList) -> None:
3254 self._linstor.ensure_volume_list_is_not_locked(
3255 vdiList, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3256 )
3257 return super(LinstorSR, self).pauseVDIs(vdiList)
3259 def _reloadLinstor(self):
3260 session = self.xapi.session
3261 host_ref = util.get_this_host_ref(session)
3262 sr_ref = session.xenapi.SR.get_by_uuid(self.uuid)
3264 pbd = util.find_my_pbd(session, host_ref, sr_ref)
3265 if pbd is None:
3266 raise util.SMException('Failed to find PBD')
3268 dconf = session.xenapi.PBD.get_device_config(pbd)
3269 group_name = dconf['group-name']
3271 controller_uri = get_controller_uri()
3272 self.journaler = LinstorJournaler(
3273 controller_uri, group_name, logger=util.SMlog
3274 )
3276 self._linstor = LinstorVolumeManager(
3277 controller_uri,
3278 group_name,
3279 repair=True,
3280 logger=util.SMlog
3281 )
3282 self._vhdutil = LinstorVhdUtil(session, self._linstor)
3284 def _scan(self, force):
3285 for i in range(SR.SCAN_RETRY_ATTEMPTS):
3286 self._reloadLinstor()
3287 error = False
3288 try:
3289 all_vdi_info = self._load_vdi_info()
3290 for uuid, vdiInfo in all_vdi_info.items():
3291 if vdiInfo and vdiInfo.error:
3292 error = True
3293 break
3294 if not error:
3295 return all_vdi_info
3296 Util.log('Scan error, retrying ({})'.format(i))
3297 except Exception as e:
3298 Util.log('Scan exception, retrying ({}): {}'.format(i, e))
3299 Util.log(traceback.format_exc())
3301 if force:
3302 return all_vdi_info
3303 raise util.SMException('Scan error')
3305 def _load_vdi_info(self):
3306 all_vdi_info = {}
3308 # TODO: Ensure metadata contains the right info.
3310 all_volume_info = self._linstor.get_volumes_with_info()
3311 volumes_metadata = self._linstor.get_volumes_with_metadata()
3312 for vdi_uuid, volume_info in all_volume_info.items():
3313 try:
3314 volume_metadata = volumes_metadata[vdi_uuid]
3315 if not volume_info.name and not list(volume_metadata.items()):
3316 continue # Ignore it, probably deleted.
3318 if vdi_uuid.startswith('DELETED_'):
3319 # Assume it's really a RAW volume of a failed snap without VHD header/footer.
3320 # We must remove this VDI now without adding it in the VDI list.
3321 # Otherwise `Relinking` calls and other actions can be launched on it.
3322 # We don't want that...
3323 Util.log('Deleting bad VDI {}'.format(vdi_uuid))
3325 self.lock()
3326 try:
3327 self._linstor.destroy_volume(vdi_uuid)
3328 try:
3329 self.forgetVDI(vdi_uuid)
3330 except:
3331 pass
3332 except Exception as e:
3333 Util.log('Cannot delete bad VDI: {}'.format(e))
3334 finally:
3335 self.unlock()
3336 continue
3338 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
3339 volume_name = self._linstor.get_volume_name(vdi_uuid)
3340 if volume_name.startswith(LINSTOR_PERSISTENT_PREFIX):
3341 # Always RAW!
3342 info = None
3343 elif vdi_type == vhdutil.VDI_TYPE_VHD:
3344 info = self._vhdutil.get_vhd_info(vdi_uuid)
3345 else:
3346 # Ensure it's not a VHD...
3347 try:
3348 info = self._vhdutil.get_vhd_info(vdi_uuid)
3349 except:
3350 try:
3351 self._vhdutil.force_repair(
3352 self._linstor.get_device_path(vdi_uuid)
3353 )
3354 info = self._vhdutil.get_vhd_info(vdi_uuid)
3355 except:
3356 info = None
3358 except Exception as e:
3359 Util.log(
3360 ' [VDI {}: failed to load VDI info]: {}'
3361 .format(vdi_uuid, e)
3362 )
3363 info = vhdutil.VHDInfo(vdi_uuid)
3364 info.error = 1
3366 all_vdi_info[vdi_uuid] = info
3368 return all_vdi_info
3370 @override
3371 def _prepareCoalesceLeaf(self, vdi) -> None:
3372 vdi._activateChain()
3373 vdi.deflate()
3374 vdi._inflateParentForCoalesce()
3376 @override
3377 def _finishCoalesceLeaf(self, parent) -> None:
3378 if not parent.isSnapshot() or parent.isAttachedRW():
3379 parent.inflateFully()
3380 else:
3381 parent.deflate()
3383 @override
3384 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3385 return LinstorVhdUtil.compute_volume_size(parent.sizeVirt, parent.vdi_type) - parent.getDrbdSize()
3387 def _hasValidDevicePath(self, uuid):
3388 try:
3389 self._linstor.get_device_path(uuid)
3390 except Exception:
3391 # TODO: Maybe log exception.
3392 return False
3393 return True
3395 @override
3396 def _liveLeafCoalesce(self, vdi) -> bool:
3397 self.lock()
3398 try:
3399 self._linstor.ensure_volume_is_not_locked(
3400 vdi.uuid, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3401 )
3402 return super(LinstorSR, self)._liveLeafCoalesce(vdi)
3403 finally:
3404 self.unlock()
3406 @override
3407 def _handleInterruptedCoalesceLeaf(self) -> None:
3408 entries = self.journaler.get_all(VDI.JRN_LEAF)
3409 for uuid, parentUuid in entries.items():
3410 if self._hasValidDevicePath(parentUuid) or \
3411 self._hasValidDevicePath(self.TMP_RENAME_PREFIX + uuid):
3412 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3413 else:
3414 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3415 self.journaler.remove(VDI.JRN_LEAF, uuid)
3416 vdi = self.getVDI(uuid)
3417 if vdi:
3418 vdi.ensureUnpaused()
3420 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3421 Util.log('*** UNDO LEAF-COALESCE')
3422 parent = self.getVDI(parentUuid)
3423 if not parent:
3424 parent = self.getVDI(childUuid)
3425 if not parent:
3426 raise util.SMException(
3427 'Neither {} nor {} found'.format(parentUuid, childUuid)
3428 )
3429 Util.log(
3430 'Renaming parent back: {} -> {}'.format(childUuid, parentUuid)
3431 )
3432 parent.rename(parentUuid)
3434 child = self.getVDI(childUuid)
3435 if not child:
3436 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3437 if not child:
3438 raise util.SMException(
3439 'Neither {} nor {} found'.format(
3440 childUuid, self.TMP_RENAME_PREFIX + childUuid
3441 )
3442 )
3443 Util.log('Renaming child back to {}'.format(childUuid))
3444 child.rename(childUuid)
3445 Util.log('Updating the VDI record')
3446 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
3447 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
3449 # TODO: Maybe deflate here.
3451 if child.hidden:
3452 child._setHidden(False)
3453 if not parent.hidden:
3454 parent._setHidden(True)
3455 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3456 Util.log('*** leaf-coalesce undo successful')
3458 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3459 Util.log('*** FINISH LEAF-COALESCE')
3460 vdi = self.getVDI(childUuid)
3461 if not vdi:
3462 raise util.SMException('VDI {} not found'.format(childUuid))
3463 # TODO: Maybe inflate.
3464 try:
3465 self.forgetVDI(parentUuid)
3466 except XenAPI.Failure:
3467 pass
3468 self._updateSlavesOnResize(vdi)
3469 Util.log('*** finished leaf-coalesce successfully')
3471 def _checkSlaves(self, vdi):
3472 try:
3473 all_openers = self._linstor.get_volume_openers(vdi.uuid)
3474 for openers in all_openers.values():
3475 for opener in openers.values():
3476 if opener['process-name'] != 'tapdisk':
3477 raise util.SMException(
3478 'VDI {} is in use: {}'.format(vdi.uuid, all_openers)
3479 )
3480 except LinstorVolumeManagerError as e:
3481 if e.code != LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
3482 raise
3485################################################################################
3486#
3487# Helpers
3488#
3489def daemonize():
3490 pid = os.fork()
3491 if pid:
3492 os.waitpid(pid, 0)
3493 Util.log("New PID [%d]" % pid)
3494 return False
3495 os.chdir("/")
3496 os.setsid()
3497 pid = os.fork()
3498 if pid:
3499 Util.log("Will finish as PID [%d]" % pid)
3500 os._exit(0)
3501 for fd in [0, 1, 2]:
3502 try:
3503 os.close(fd)
3504 except OSError:
3505 pass
3506 # we need to fill those special fd numbers or pread won't work
3507 sys.stdin = open("/dev/null", 'r')
3508 sys.stderr = open("/dev/null", 'w')
3509 sys.stdout = open("/dev/null", 'w')
3510 # As we're a new process we need to clear the lock objects
3511 lock.Lock.clearAll()
3512 return True
3515def normalizeType(type):
3516 if type in LVHDSR.SUBTYPES:
3517 type = SR.TYPE_LVHD
3518 if type in ["lvm", "lvmoiscsi", "lvmohba", "lvmofcoe"]:
3519 # temporary while LVHD is symlinked as LVM
3520 type = SR.TYPE_LVHD
3521 if type in [
3522 "ext", "nfs", "ocfsoiscsi", "ocfsohba", "smb", "cephfs", "glusterfs",
3523 "moosefs", "xfs", "zfs", "largeblock"
3524 ]:
3525 type = SR.TYPE_FILE
3526 if type in ["linstor"]:
3527 type = SR.TYPE_LINSTOR
3528 if type not in SR.TYPES:
3529 raise util.SMException("Unsupported SR type: %s" % type)
3530 return type
3532GCPAUSE_DEFAULT_SLEEP = 5 * 60
3535def _gc_init_file(sr_uuid):
3536 return os.path.join(NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init')
3539def _create_init_file(sr_uuid):
3540 util.makedirs(os.path.join(NON_PERSISTENT_DIR, str(sr_uuid)))
3541 with open(os.path.join(
3542 NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init'), 'w+') as f:
3543 f.write('1')
3546def _gcLoopPause(sr, dryRun=False, immediate=False):
3547 if immediate:
3548 return
3550 # Check to see if the GCPAUSE_FISTPOINT is present. If so the fist
3551 # point will just return. Otherwise, fall back on an abortable sleep.
3553 if util.fistpoint.is_active(util.GCPAUSE_FISTPOINT):
3555 util.fistpoint.activate_custom_fn(util.GCPAUSE_FISTPOINT, 3555 ↛ exitline 3555 didn't jump to the function exit
3556 lambda *args: None)
3557 elif os.path.exists(_gc_init_file(sr.uuid)):
3558 def abortTest():
3559 return IPCFlag(sr.uuid).test(FLAG_TYPE_ABORT)
3561 # If time.sleep hangs we are in deep trouble, however for
3562 # completeness we set the timeout of the abort thread to
3563 # 110% of GCPAUSE_DEFAULT_SLEEP.
3564 Util.log("GC active, about to go quiet")
3565 Util.runAbortable(lambda: time.sleep(GCPAUSE_DEFAULT_SLEEP), 3565 ↛ exitline 3565 didn't run the lambda on line 3565
3566 None, sr.uuid, abortTest, VDI.POLL_INTERVAL,
3567 GCPAUSE_DEFAULT_SLEEP * 1.1)
3568 Util.log("GC active, quiet period ended")
3571def _gcLoop(sr, dryRun=False, immediate=False):
3572 if not lockGCActive.acquireNoblock(): 3572 ↛ 3573line 3572 didn't jump to line 3573, because the condition on line 3572 was never true
3573 Util.log("Another GC instance already active, exiting")
3574 return
3576 # Check we're still attached after acquiring locks
3577 if not sr.xapi.isPluggedHere():
3578 Util.log("SR no longer attached, exiting")
3579 return
3581 # Clean up Intellicache files
3582 sr.cleanupCache()
3584 # Track how many we do
3585 coalesced = 0
3586 task_status = "success"
3587 try:
3588 # Check if any work needs to be done
3589 if not sr.xapi.isPluggedHere(): 3589 ↛ 3590line 3589 didn't jump to line 3590, because the condition on line 3589 was never true
3590 Util.log("SR no longer attached, exiting")
3591 return
3592 sr.scanLocked()
3593 if not sr.hasWork():
3594 Util.log("No work, exiting")
3595 return
3596 sr.xapi.create_task(
3597 "Garbage Collection",
3598 "Garbage collection for SR %s" % sr.uuid)
3599 _gcLoopPause(sr, dryRun, immediate=immediate)
3600 while True:
3601 if not sr.xapi.isPluggedHere(): 3601 ↛ 3602line 3601 didn't jump to line 3602, because the condition on line 3601 was never true
3602 Util.log("SR no longer attached, exiting")
3603 break
3604 sr.scanLocked()
3605 if not sr.hasWork():
3606 Util.log("No work, exiting")
3607 break
3609 if not lockGCRunning.acquireNoblock(): 3609 ↛ 3610line 3609 didn't jump to line 3610, because the condition on line 3609 was never true
3610 Util.log("Unable to acquire GC running lock.")
3611 return
3612 try:
3613 if not sr.gcEnabled(): 3613 ↛ 3614line 3613 didn't jump to line 3614, because the condition on line 3613 was never true
3614 break
3616 sr.xapi.update_task_progress("done", coalesced)
3618 sr.cleanupCoalesceJournals()
3619 # Create the init file here in case startup is waiting on it
3620 _create_init_file(sr.uuid)
3621 sr.scanLocked()
3622 sr.updateBlockInfo()
3624 howmany = len(sr.findGarbage())
3625 if howmany > 0:
3626 Util.log("Found %d orphaned vdis" % howmany)
3627 sr.lock()
3628 try:
3629 sr.garbageCollect(dryRun)
3630 finally:
3631 sr.unlock()
3632 sr.xapi.srUpdate()
3634 candidate = sr.findCoalesceable()
3635 if candidate:
3636 util.fistpoint.activate(
3637 "LVHDRT_finding_a_suitable_pair", sr.uuid)
3638 sr.coalesce(candidate, dryRun)
3639 sr.xapi.srUpdate()
3640 coalesced += 1
3641 continue
3643 candidate = sr.findLeafCoalesceable()
3644 if candidate: 3644 ↛ 3651line 3644 didn't jump to line 3651, because the condition on line 3644 was never false
3645 sr.coalesceLeaf(candidate, dryRun)
3646 sr.xapi.srUpdate()
3647 coalesced += 1
3648 continue
3650 finally:
3651 lockGCRunning.release() 3651 ↛ 3656line 3651 didn't jump to line 3656, because the break on line 3614 wasn't executed
3652 except:
3653 task_status = "failure"
3654 raise
3655 finally:
3656 sr.xapi.set_task_status(task_status)
3657 Util.log("GC process exiting, no work left")
3658 _create_init_file(sr.uuid)
3659 lockGCActive.release()
3662def _xapi_enabled(session, hostref):
3663 host = session.xenapi.host.get_record(hostref)
3664 return host['enabled']
3667def _ensure_xapi_initialised(session):
3668 """
3669 Don't want to start GC until Xapi is fully initialised
3670 """
3671 local_session = None
3672 if session is None:
3673 local_session = util.get_localAPI_session()
3674 session = local_session
3676 try:
3677 hostref = session.xenapi.host.get_by_uuid(util.get_this_host())
3678 while not _xapi_enabled(session, hostref):
3679 util.SMlog("Xapi not ready, GC waiting")
3680 time.sleep(15)
3681 finally:
3682 if local_session is not None:
3683 local_session.xenapi.session.logout()
3685def _gc(session, srUuid, dryRun=False, immediate=False):
3686 init(srUuid)
3687 _ensure_xapi_initialised(session)
3688 sr = SR.getInstance(srUuid, session)
3689 if not sr.gcEnabled(False): 3689 ↛ 3690line 3689 didn't jump to line 3690, because the condition on line 3689 was never true
3690 return
3692 try:
3693 _gcLoop(sr, dryRun, immediate=immediate)
3694 finally:
3695 sr.cleanup()
3696 sr.logFilter.logState()
3697 del sr.xapi
3700def _abort(srUuid, soft=False):
3701 """Aborts an GC/coalesce.
3703 srUuid: the UUID of the SR whose GC/coalesce must be aborted
3704 soft: If set to True and there is a pending abort signal, the function
3705 doesn't do anything. If set to False, a new abort signal is issued.
3707 returns: If soft is set to False, we return True holding lockGCActive. If
3708 soft is set to False and an abort signal is pending, we return False
3709 without holding lockGCActive. An exception is raised in case of error."""
3710 Util.log("=== SR %s: abort ===" % (srUuid))
3711 init(srUuid)
3712 if not lockGCActive.acquireNoblock():
3713 gotLock = False
3714 Util.log("Aborting currently-running instance (SR %s)" % srUuid)
3715 abortFlag = IPCFlag(srUuid)
3716 if not abortFlag.set(FLAG_TYPE_ABORT, soft):
3717 return False
3718 for i in range(SR.LOCK_RETRY_ATTEMPTS):
3719 gotLock = lockGCActive.acquireNoblock()
3720 if gotLock:
3721 break
3722 time.sleep(SR.LOCK_RETRY_INTERVAL)
3723 abortFlag.clear(FLAG_TYPE_ABORT)
3724 if not gotLock:
3725 raise util.CommandException(code=errno.ETIMEDOUT,
3726 reason="SR %s: error aborting existing process" % srUuid)
3727 return True
3730def init(srUuid):
3731 global lockGCRunning
3732 if not lockGCRunning: 3732 ↛ 3733line 3732 didn't jump to line 3733, because the condition on line 3732 was never true
3733 lockGCRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, srUuid)
3734 global lockGCActive
3735 if not lockGCActive: 3735 ↛ 3736line 3735 didn't jump to line 3736, because the condition on line 3735 was never true
3736 lockGCActive = LockActive(srUuid)
3739class LockActive:
3740 """
3741 Wraps the use of LOCK_TYPE_GC_ACTIVE such that the lock cannot be acquired
3742 if another process holds the SR lock.
3743 """
3744 def __init__(self, srUuid):
3745 self._lock = lock.Lock(LOCK_TYPE_GC_ACTIVE, srUuid)
3746 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, srUuid)
3748 def acquireNoblock(self):
3749 self._srLock.acquire()
3751 try:
3752 return self._lock.acquireNoblock()
3753 finally:
3754 self._srLock.release()
3756 def release(self):
3757 self._lock.release()
3760def usage():
3761 output = """Garbage collect and/or coalesce VHDs in a VHD-based SR
3763Parameters:
3764 -u --uuid UUID SR UUID
3765 and one of:
3766 -g --gc garbage collect, coalesce, and repeat while there is work
3767 -G --gc_force garbage collect once, aborting any current operations
3768 -c --cache-clean <max_age> clean up IntelliCache cache files older than
3769 max_age hours
3770 -a --abort abort any currently running operation (GC or coalesce)
3771 -q --query query the current state (GC'ing, coalescing or not running)
3772 -x --disable disable GC/coalesce (will be in effect until you exit)
3773 -t --debug see Debug below
3775Options:
3776 -b --background run in background (return immediately) (valid for -g only)
3777 -f --force continue in the presence of VHDs with errors (when doing
3778 GC, this might cause removal of any such VHDs) (only valid
3779 for -G) (DANGEROUS)
3781Debug:
3782 The --debug parameter enables manipulation of LVHD VDIs for debugging
3783 purposes. ** NEVER USE IT ON A LIVE VM **
3784 The following parameters are required:
3785 -t --debug <cmd> <cmd> is one of "activate", "deactivate", "inflate",
3786 "deflate".
3787 -v --vdi_uuid VDI UUID
3788 """
3789 #-d --dry-run don't actually perform any SR-modifying operations
3790 print(output)
3791 Util.log("(Invalid usage)")
3792 sys.exit(1)
3795##############################################################################
3796#
3797# API
3798#
3799def abort(srUuid, soft=False):
3800 """Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair.
3801 """
3802 if _abort(srUuid, soft):
3803 Util.log("abort: releasing the process lock")
3804 lockGCActive.release()
3805 return True
3806 else:
3807 return False
3810def gc(session, srUuid, inBackground, dryRun=False):
3811 """Garbage collect all deleted VDIs in SR "srUuid". Fork & return
3812 immediately if inBackground=True.
3814 The following algorithm is used:
3815 1. If we are already GC'ing in this SR, return
3816 2. If we are already coalescing a VDI pair:
3817 a. Scan the SR and determine if the VDI pair is GC'able
3818 b. If the pair is not GC'able, return
3819 c. If the pair is GC'able, abort coalesce
3820 3. Scan the SR
3821 4. If there is nothing to collect, nor to coalesce, return
3822 5. If there is something to collect, GC all, then goto 3
3823 6. If there is something to coalesce, coalesce one pair, then goto 3
3824 """
3825 Util.log("=== SR %s: gc ===" % srUuid)
3826 if inBackground:
3827 if daemonize(): 3827 ↛ exitline 3827 didn't return from function 'gc', because the condition on line 3827 was never false
3828 # we are now running in the background. Catch & log any errors
3829 # because there is no other way to propagate them back at this
3830 # point
3832 try:
3833 _gc(None, srUuid, dryRun)
3834 except AbortException:
3835 Util.log("Aborted")
3836 except Exception:
3837 Util.logException("gc")
3838 Util.log("* * * * * SR %s: ERROR\n" % srUuid)
3839 os._exit(0)
3840 else:
3841 _gc(session, srUuid, dryRun, immediate=True)
3844def start_gc(session, sr_uuid):
3845 """
3846 This function is used to try to start a backgrounded GC session by forking
3847 the current process. If using the systemd version, call start_gc_service() instead.
3848 """
3849 # don't bother if an instance already running (this is just an
3850 # optimization to reduce the overhead of forking a new process if we
3851 # don't have to, but the process will check the lock anyways)
3852 lockRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, sr_uuid)
3853 if not lockRunning.acquireNoblock():
3854 if should_preempt(session, sr_uuid):
3855 util.SMlog("Aborting currently-running coalesce of garbage VDI")
3856 try:
3857 if not abort(sr_uuid, soft=True):
3858 util.SMlog("The GC has already been scheduled to re-start")
3859 except util.CommandException as e:
3860 if e.code != errno.ETIMEDOUT:
3861 raise
3862 util.SMlog('failed to abort the GC')
3863 else:
3864 util.SMlog("A GC instance already running, not kicking")
3865 return
3866 else:
3867 lockRunning.release()
3869 util.SMlog(f"Starting GC file is {__file__}")
3870 subprocess.run([__file__, '-b', '-u', sr_uuid, '-g'],
3871 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3873def start_gc_service(sr_uuid, wait=False):
3874 """
3875 This starts the templated systemd service which runs GC on the given SR UUID.
3876 If the service was already started, this is a no-op.
3878 Because the service is a one-shot with RemainAfterExit=no, when called with
3879 wait=True this will run the service synchronously and will not return until the
3880 run has finished. This is used to force a run of the GC instead of just kicking it
3881 in the background.
3882 """
3883 sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
3884 util.SMlog(f"Kicking SMGC@{sr_uuid}...")
3885 cmd=[ "/usr/bin/systemctl", "--quiet" ]
3886 if not wait: 3886 ↛ 3888line 3886 didn't jump to line 3888, because the condition on line 3886 was never false
3887 cmd.append("--no-block")
3888 cmd += ["start", f"SMGC@{sr_uuid_esc}"]
3889 subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3892def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False):
3893 """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure
3894 the SR lock is held.
3895 The following algorithm is used:
3896 1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce
3897 2. Scan the SR
3898 3. GC
3899 4. return
3900 """
3901 Util.log("=== SR %s: gc_force ===" % srUuid)
3902 init(srUuid)
3903 sr = SR.getInstance(srUuid, session, lockSR, True)
3904 if not lockGCActive.acquireNoblock():
3905 abort(srUuid)
3906 else:
3907 Util.log("Nothing was running, clear to proceed")
3909 if force:
3910 Util.log("FORCED: will continue even if there are VHD errors")
3911 sr.scanLocked(force)
3912 sr.cleanupCoalesceJournals()
3914 try:
3915 sr.cleanupCache()
3916 sr.garbageCollect(dryRun)
3917 finally:
3918 sr.cleanup()
3919 sr.logFilter.logState()
3920 lockGCActive.release()
3923def get_state(srUuid):
3924 """Return whether GC/coalesce is currently running or not. This asks systemd for
3925 the state of the templated SMGC service and will return True if it is "activating"
3926 or "running" (for completeness, as in practice it will never achieve the latter state)
3927 """
3928 sr_uuid_esc = srUuid.replace("-", "\\x2d")
3929 cmd=[ "/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"]
3930 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3931 state = result.stdout.decode('utf-8').rstrip()
3932 if state == "activating" or state == "running":
3933 return True
3934 return False
3937def should_preempt(session, srUuid):
3938 sr = SR.getInstance(srUuid, session)
3939 entries = sr.journaler.getAll(VDI.JRN_COALESCE)
3940 if len(entries) == 0:
3941 return False
3942 elif len(entries) > 1:
3943 raise util.SMException("More than one coalesce entry: " + str(entries))
3944 sr.scanLocked()
3945 coalescedUuid = entries.popitem()[0]
3946 garbage = sr.findGarbage()
3947 for vdi in garbage:
3948 if vdi.uuid == coalescedUuid:
3949 return True
3950 return False
3953def get_coalesceable_leaves(session, srUuid, vdiUuids):
3954 coalesceable = []
3955 sr = SR.getInstance(srUuid, session)
3956 sr.scanLocked()
3957 for uuid in vdiUuids:
3958 vdi = sr.getVDI(uuid)
3959 if not vdi:
3960 raise util.SMException("VDI %s not found" % uuid)
3961 if vdi.isLeafCoalesceable():
3962 coalesceable.append(uuid)
3963 return coalesceable
3966def cache_cleanup(session, srUuid, maxAge):
3967 sr = SR.getInstance(srUuid, session)
3968 return sr.cleanupCache(maxAge)
3971def debug(sr_uuid, cmd, vdi_uuid):
3972 Util.log("Debug command: %s" % cmd)
3973 sr = SR.getInstance(sr_uuid, None)
3974 if not isinstance(sr, LVHDSR):
3975 print("Error: not an LVHD SR")
3976 return
3977 sr.scanLocked()
3978 vdi = sr.getVDI(vdi_uuid)
3979 if not vdi:
3980 print("Error: VDI %s not found")
3981 return
3982 print("Running %s on SR %s" % (cmd, sr))
3983 print("VDI before: %s" % vdi)
3984 if cmd == "activate":
3985 vdi._activate()
3986 print("VDI file: %s" % vdi.path)
3987 if cmd == "deactivate":
3988 ns = lvhdutil.NS_PREFIX_LVM + sr.uuid
3989 sr.lvmCache.deactivate(ns, vdi.uuid, vdi.fileName, False)
3990 if cmd == "inflate":
3991 vdi.inflateFully()
3992 sr.cleanup()
3993 if cmd == "deflate":
3994 vdi.deflate()
3995 sr.cleanup()
3996 sr.scanLocked()
3997 print("VDI after: %s" % vdi)
4000def abort_optional_reenable(uuid):
4001 print("Disabling GC/coalesce for %s" % uuid)
4002 ret = _abort(uuid)
4003 input("Press enter to re-enable...")
4004 print("GC/coalesce re-enabled")
4005 lockGCRunning.release()
4006 if ret:
4007 lockGCActive.release()
4010##############################################################################
4011#
4012# CLI
4013#
4014def main():
4015 action = ""
4016 uuid = ""
4017 background = False
4018 force = False
4019 dryRun = False
4020 debug_cmd = ""
4021 vdi_uuid = ""
4022 shortArgs = "gGc:aqxu:bfdt:v:"
4023 longArgs = ["gc", "gc_force", "clean_cache", "abort", "query", "disable",
4024 "uuid=", "background", "force", "dry-run", "debug=", "vdi_uuid="]
4026 try:
4027 opts, args = getopt.getopt(sys.argv[1:], shortArgs, longArgs)
4028 except getopt.GetoptError:
4029 usage()
4030 for o, a in opts:
4031 if o in ("-g", "--gc"):
4032 action = "gc"
4033 if o in ("-G", "--gc_force"):
4034 action = "gc_force"
4035 if o in ("-c", "--clean_cache"):
4036 action = "clean_cache"
4037 maxAge = int(a)
4038 if o in ("-a", "--abort"):
4039 action = "abort"
4040 if o in ("-q", "--query"):
4041 action = "query"
4042 if o in ("-x", "--disable"):
4043 action = "disable"
4044 if o in ("-u", "--uuid"):
4045 uuid = a
4046 if o in ("-b", "--background"):
4047 background = True
4048 if o in ("-f", "--force"):
4049 force = True
4050 if o in ("-d", "--dry-run"):
4051 Util.log("Dry run mode")
4052 dryRun = True
4053 if o in ("-t", "--debug"):
4054 action = "debug"
4055 debug_cmd = a
4056 if o in ("-v", "--vdi_uuid"):
4057 vdi_uuid = a
4059 if not action or not uuid:
4060 usage()
4061 if action == "debug" and not (debug_cmd and vdi_uuid) or \
4062 action != "debug" and (debug_cmd or vdi_uuid):
4063 usage()
4065 if action != "query" and action != "debug":
4066 print("All output goes to log")
4068 if action == "gc":
4069 gc(None, uuid, background, dryRun)
4070 elif action == "gc_force":
4071 gc_force(None, uuid, force, dryRun, True)
4072 elif action == "clean_cache":
4073 cache_cleanup(None, uuid, maxAge)
4074 elif action == "abort":
4075 abort(uuid)
4076 elif action == "query":
4077 print("Currently running: %s" % get_state(uuid))
4078 elif action == "disable":
4079 abort_optional_reenable(uuid)
4080 elif action == "debug":
4081 debug(uuid, debug_cmd, vdi_uuid)
4084if __name__ == '__main__': 4084 ↛ 4085line 4084 didn't jump to line 4085, because the condition on line 4084 was never true
4085 main()