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"):
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: 790 ↛ 791line 790 didn't jump to line 791, because the condition on line 790 was never true
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)
1203class LVHDVDI(VDI):
1204 """Object representing a VDI in an LVHD SR"""
1206 JRN_ZERO = "zero" # journal entry type for zeroing out end of parent
1207 DRIVER_NAME_RAW = "aio"
1209 @override
1210 def load(self, info=None) -> None:
1211 # `info` is always set. `None` default value is only here to match parent method.
1212 assert info, "No info given to LVHDVDI.load"
1213 self.parent = None
1214 self.children = []
1215 self._sizeVHD = -1
1216 self._sizeAllocated = -1
1217 self.scanError = info.scanError
1218 self.sizeLV = info.sizeLV
1219 self.sizeVirt = info.sizeVirt
1220 self.fileName = info.lvName
1221 self.lvActive = info.lvActive
1222 self.lvOpen = info.lvOpen
1223 self.lvReadonly = info.lvReadonly
1224 self.hidden = info.hidden
1225 self.parentUuid = info.parentUuid
1226 self.path = os.path.join(self.sr.path, self.fileName)
1228 @staticmethod
1229 def extractUuid(path):
1230 return lvhdutil.extractUuid(path)
1232 @override
1233 def getDriverName(self) -> str:
1234 if self.raw:
1235 return self.DRIVER_NAME_RAW
1236 return self.DRIVER_NAME_VHD
1238 def inflate(self, size):
1239 """inflate the LV containing the VHD to 'size'"""
1240 if self.raw:
1241 return
1242 self._activate()
1243 self.sr.lock()
1244 try:
1245 lvhdutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, size)
1246 util.fistpoint.activate("LVHDRT_inflating_the_parent", self.sr.uuid)
1247 finally:
1248 self.sr.unlock()
1249 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1250 self._sizeVHD = -1
1251 self._sizeAllocated = -1
1253 def deflate(self):
1254 """deflate the LV containing the VHD to minimum"""
1255 if self.raw:
1256 return
1257 self._activate()
1258 self.sr.lock()
1259 try:
1260 lvhdutil.deflate(self.sr.lvmCache, self.fileName, self.getSizeVHD())
1261 finally:
1262 self.sr.unlock()
1263 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1264 self._sizeVHD = -1
1265 self._sizeAllocated = -1
1267 def inflateFully(self):
1268 self.inflate(lvhdutil.calcSizeVHDLV(self.sizeVirt))
1270 def inflateParentForCoalesce(self):
1271 """Inflate the parent only as much as needed for the purposes of
1272 coalescing"""
1273 if self.parent.raw:
1274 return
1275 inc = self._calcExtraSpaceForCoalescing()
1276 if inc > 0:
1277 util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent", self.sr.uuid)
1278 self.parent.inflate(self.parent.sizeLV + inc)
1280 @override
1281 def updateBlockInfo(self) -> Optional[str]:
1282 if not self.raw:
1283 return VDI.updateBlockInfo(self)
1284 return None
1286 @override
1287 def rename(self, uuid) -> None:
1288 oldUuid = self.uuid
1289 oldLVName = self.fileName
1290 VDI.rename(self, uuid)
1291 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + self.uuid
1292 if self.raw:
1293 self.fileName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + self.uuid
1294 self.path = os.path.join(self.sr.path, self.fileName)
1295 assert(not self.sr.lvmCache.checkLV(self.fileName))
1297 self.sr.lvmCache.rename(oldLVName, self.fileName)
1298 if self.sr.lvActivator.get(oldUuid, False):
1299 self.sr.lvActivator.replace(oldUuid, self.uuid, self.fileName, False)
1301 ns = lvhdutil.NS_PREFIX_LVM + self.sr.uuid
1302 (cnt, bcnt) = RefCounter.check(oldUuid, ns)
1303 RefCounter.set(self.uuid, cnt, bcnt, ns)
1304 RefCounter.reset(oldUuid, ns)
1306 @override
1307 def delete(self) -> None:
1308 if len(self.children) > 0:
1309 raise util.SMException("VDI %s has children, can't delete" % \
1310 self.uuid)
1311 self.sr.lock()
1312 try:
1313 self.sr.lvmCache.remove(self.fileName)
1314 self.sr.forgetVDI(self.uuid)
1315 finally:
1316 self.sr.unlock()
1317 RefCounter.reset(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
1318 VDI.delete(self)
1320 @override
1321 def getSizeVHD(self) -> int:
1322 if self._sizeVHD == -1:
1323 self._loadInfoSizeVHD()
1324 return self._sizeVHD
1326 def _loadInfoSizeVHD(self):
1327 """Get the physical utilization of the VHD file. We do it individually
1328 (and not using the VHD batch scanner) as an optimization: this info is
1329 relatively expensive and we need it only for VDI's involved in
1330 coalescing."""
1331 if self.raw:
1332 return
1333 self._activate()
1334 self._sizeVHD = vhdutil.getSizePhys(self.path)
1335 if self._sizeVHD <= 0:
1336 raise util.SMException("phys size of %s = %d" % \
1337 (self, self._sizeVHD))
1339 @override
1340 def getAllocatedSize(self) -> int:
1341 if self._sizeAllocated == -1:
1342 self._loadInfoSizeAllocated()
1343 return self._sizeAllocated
1345 def _loadInfoSizeAllocated(self):
1346 """
1347 Get the allocated size of the VHD volume.
1348 """
1349 if self.raw:
1350 return
1351 self._activate()
1352 self._sizeAllocated = vhdutil.getAllocatedSize(self.path)
1354 @override
1355 def _loadInfoHidden(self) -> None:
1356 if self.raw:
1357 self.hidden = self.sr.lvmCache.getHidden(self.fileName)
1358 else:
1359 VDI._loadInfoHidden(self)
1361 @override
1362 def _setHidden(self, hidden=True) -> None:
1363 if self.raw:
1364 self.sr.lvmCache.setHidden(self.fileName, hidden)
1365 self.hidden = hidden
1366 else:
1367 VDI._setHidden(self, hidden)
1369 @override
1370 def __str__(self) -> str:
1371 strType = "VHD"
1372 if self.raw:
1373 strType = "RAW"
1374 strHidden = ""
1375 if self.hidden:
1376 strHidden = "*"
1377 strSizeVHD = ""
1378 if self._sizeVHD > 0:
1379 strSizeVHD = Util.num2str(self._sizeVHD)
1380 strSizeAllocated = ""
1381 if self._sizeAllocated >= 0:
1382 strSizeAllocated = Util.num2str(self._sizeAllocated)
1383 strActive = "n"
1384 if self.lvActive:
1385 strActive = "a"
1386 if self.lvOpen:
1387 strActive += "o"
1388 return "%s%s[%s](%s/%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType,
1389 Util.num2str(self.sizeVirt), strSizeVHD, strSizeAllocated,
1390 Util.num2str(self.sizeLV), strActive)
1392 @override
1393 def validate(self, fast=False) -> None:
1394 if not self.raw:
1395 VDI.validate(self, fast)
1397 @override
1398 def _doCoalesce(self) -> None:
1399 """LVHD parents must first be activated, inflated, and made writable"""
1400 try:
1401 self._activateChain()
1402 self.sr.lvmCache.setReadonly(self.parent.fileName, False)
1403 self.parent.validate()
1404 self.inflateParentForCoalesce()
1405 VDI._doCoalesce(self)
1406 finally:
1407 self.parent._loadInfoSizeVHD()
1408 self.parent.deflate()
1409 self.sr.lvmCache.setReadonly(self.parent.fileName, True)
1411 @override
1412 def _setParent(self, parent) -> None:
1413 self._activate()
1414 if self.lvReadonly:
1415 self.sr.lvmCache.setReadonly(self.fileName, False)
1417 try:
1418 vhdutil.setParent(self.path, parent.path, parent.raw)
1419 finally:
1420 if self.lvReadonly:
1421 self.sr.lvmCache.setReadonly(self.fileName, True)
1422 self._deactivate()
1423 self.parent = parent
1424 self.parentUuid = parent.uuid
1425 parent.children.append(self)
1426 try:
1427 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1428 Util.log("Updated the vhd-parent field for child %s with %s" % \
1429 (self.uuid, self.parentUuid))
1430 except:
1431 Util.log("Failed to update the vhd-parent with %s for child %s" % \
1432 (self.parentUuid, self.uuid))
1434 def _activate(self):
1435 self.sr.lvActivator.activate(self.uuid, self.fileName, False)
1437 def _activateChain(self):
1438 vdi = self
1439 while vdi:
1440 vdi._activate()
1441 vdi = vdi.parent
1443 def _deactivate(self):
1444 self.sr.lvActivator.deactivate(self.uuid, False)
1446 @override
1447 def _increaseSizeVirt(self, size, atomic=True) -> None:
1448 "ensure the virtual size of 'self' is at least 'size'"
1449 self._activate()
1450 if not self.raw:
1451 VDI._increaseSizeVirt(self, size, atomic)
1452 return
1454 # raw VDI case
1455 offset = self.sizeLV
1456 if self.sizeVirt < size:
1457 oldSize = self.sizeLV
1458 self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size)
1459 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV))
1460 self.sr.lvmCache.setSize(self.fileName, self.sizeLV)
1461 offset = oldSize
1462 unfinishedZero = False
1463 jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid)
1464 if jval:
1465 unfinishedZero = True
1466 offset = int(jval)
1467 length = self.sizeLV - offset
1468 if not length:
1469 return
1471 if unfinishedZero:
1472 Util.log(" ==> Redoing unfinished zeroing out")
1473 else:
1474 self.sr.journaler.create(self.JRN_ZERO, self.uuid, \
1475 str(offset))
1476 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
1477 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1478 func = lambda: util.zeroOut(self.path, offset, length)
1479 Util.runAbortable(func, True, self.sr.uuid, abortTest,
1480 VDI.POLL_INTERVAL, 0)
1481 self.sr.journaler.remove(self.JRN_ZERO, self.uuid)
1483 @override
1484 def _setSizeVirt(self, size) -> None:
1485 """WARNING: do not call this method directly unless all VDIs in the
1486 subtree are guaranteed to be unplugged (and remain so for the duration
1487 of the operation): this operation is only safe for offline VHDs"""
1488 self._activate()
1489 jFile = lvhdutil.createVHDJournalLV(self.sr.lvmCache, self.uuid,
1490 vhdutil.MAX_VHD_JOURNAL_SIZE)
1491 try:
1492 lvhdutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid,
1493 size, jFile)
1494 finally:
1495 lvhdutil.deleteVHDJournalLV(self.sr.lvmCache, self.uuid)
1497 @override
1498 def _queryVHDBlocks(self) -> bytes:
1499 self._activate()
1500 return VDI._queryVHDBlocks(self)
1502 @override
1503 def _calcExtraSpaceForCoalescing(self) -> int:
1504 if self.parent.raw:
1505 return 0 # raw parents are never deflated in the first place
1506 sizeCoalesced = lvhdutil.calcSizeVHDLV(self._getCoalescedSizeData())
1507 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1508 return sizeCoalesced - self.parent.sizeLV
1510 @override
1511 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1512 """How much extra space in the SR will be required to
1513 [live-]leaf-coalesce this VDI"""
1514 # we can deflate the leaf to minimize the space requirements
1515 deflateDiff = self.sizeLV - lvhdutil.calcSizeLV(self.getSizeVHD())
1516 return self._calcExtraSpaceForCoalescing() - deflateDiff
1518 @override
1519 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1520 return self._calcExtraSpaceForCoalescing() + \
1521 lvhdutil.calcSizeLV(self.getSizeVHD())
1524class LinstorVDI(VDI):
1525 """Object representing a VDI in a LINSTOR SR"""
1527 VOLUME_LOCK_TIMEOUT = 30
1529 @override
1530 def load(self, info=None) -> None:
1531 self.parentUuid = info.parentUuid
1532 self.scanError = True
1533 self.parent = None
1534 self.children = []
1536 self.fileName = self.sr._linstor.get_volume_name(self.uuid)
1537 self.path = self.sr._linstor.build_device_path(self.fileName)
1539 if not info:
1540 try:
1541 info = self.sr._vhdutil.get_vhd_info(self.uuid)
1542 except util.SMException:
1543 Util.log(
1544 ' [VDI {}: failed to read VHD metadata]'.format(self.uuid)
1545 )
1546 return
1548 self.parentUuid = info.parentUuid
1549 self.sizeVirt = info.sizeVirt
1550 self._sizeVHD = -1
1551 self._sizeAllocated = -1
1552 self.drbd_size = -1
1553 self.hidden = info.hidden
1554 self.scanError = False
1555 self.vdi_type = vhdutil.VDI_TYPE_VHD
1557 @override
1558 def getSizeVHD(self, fetch=False) -> int:
1559 if self._sizeVHD < 0 or fetch:
1560 self._sizeVHD = self.sr._vhdutil.get_size_phys(self.uuid)
1561 return self._sizeVHD
1563 def getDrbdSize(self, fetch=False):
1564 if self.drbd_size < 0 or fetch:
1565 self.drbd_size = self.sr._vhdutil.get_drbd_size(self.uuid)
1566 return self.drbd_size
1568 @override
1569 def getAllocatedSize(self) -> int:
1570 if self._sizeAllocated == -1:
1571 if not self.raw:
1572 self._sizeAllocated = self.sr._vhdutil.get_allocated_size(self.uuid)
1573 return self._sizeAllocated
1575 def inflate(self, size):
1576 if self.raw:
1577 return
1578 self.sr.lock()
1579 try:
1580 # Ensure we use the real DRBD size and not the cached one.
1581 # Why? Because this attribute can be changed if volume is resized by user.
1582 self.drbd_size = self.getDrbdSize(fetch=True)
1583 self.sr._vhdutil.inflate(self.sr.journaler, self.uuid, self.path, size, self.drbd_size)
1584 finally:
1585 self.sr.unlock()
1586 self.drbd_size = -1
1587 self._sizeVHD = -1
1588 self._sizeAllocated = -1
1590 def deflate(self):
1591 if self.raw:
1592 return
1593 self.sr.lock()
1594 try:
1595 # Ensure we use the real sizes and not the cached info.
1596 self.drbd_size = self.getDrbdSize(fetch=True)
1597 self._sizeVHD = self.getSizeVHD(fetch=True)
1598 self.sr._vhdutil.force_deflate(self.path, self._sizeVHD, self.drbd_size, zeroize=False)
1599 finally:
1600 self.sr.unlock()
1601 self.drbd_size = -1
1602 self._sizeVHD = -1
1603 self._sizeAllocated = -1
1605 def inflateFully(self):
1606 if not self.raw:
1607 self.inflate(LinstorVhdUtil.compute_volume_size(self.sizeVirt, self.vdi_type))
1609 @override
1610 def rename(self, uuid) -> None:
1611 Util.log('Renaming {} -> {} (path={})'.format(
1612 self.uuid, uuid, self.path
1613 ))
1614 self.sr._linstor.update_volume_uuid(self.uuid, uuid)
1615 VDI.rename(self, uuid)
1617 @override
1618 def delete(self) -> None:
1619 if len(self.children) > 0:
1620 raise util.SMException(
1621 'VDI {} has children, can\'t delete'.format(self.uuid)
1622 )
1623 self.sr.lock()
1624 try:
1625 self.sr._linstor.destroy_volume(self.uuid)
1626 self.sr.forgetVDI(self.uuid)
1627 finally:
1628 self.sr.unlock()
1629 VDI.delete(self)
1631 @override
1632 def validate(self, fast=False) -> None:
1633 if not self.raw and not self.sr._vhdutil.check(self.uuid, fast=fast):
1634 raise util.SMException('VHD {} corrupted'.format(self))
1636 @override
1637 def pause(self, failfast=False) -> None:
1638 self.sr._linstor.ensure_volume_is_not_locked(
1639 self.uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1640 )
1641 return super(LinstorVDI, self).pause(failfast)
1643 @override
1644 def coalesce(self) -> int:
1645 # Note: We raise `SMException` here to skip the current coalesce in case of failure.
1646 # Using another exception we can't execute the next coalesce calls.
1647 return self.sr._vhdutil.force_coalesce(self.path) * 512
1649 @override
1650 def getParent(self) -> str:
1651 return self.sr._vhdutil.get_parent(
1652 self.sr._linstor.get_volume_uuid_from_device_path(self.path)
1653 )
1655 @override
1656 def repair(self, parent_uuid) -> None:
1657 self.sr._vhdutil.force_repair(
1658 self.sr._linstor.get_device_path(parent_uuid)
1659 )
1661 @override
1662 def _relinkSkip(self) -> None:
1663 abortFlag = IPCFlag(self.sr.uuid)
1664 for child in self.children:
1665 if abortFlag.test(FLAG_TYPE_ABORT):
1666 raise AbortException('Aborting due to signal')
1667 Util.log(
1668 ' Relinking {} from {} to {}'.format(
1669 child, self, self.parent
1670 )
1671 )
1673 session = child.sr.xapi.session
1674 sr_uuid = child.sr.uuid
1675 vdi_uuid = child.uuid
1676 try:
1677 self.sr._linstor.ensure_volume_is_not_locked(
1678 vdi_uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1679 )
1680 blktap2.VDI.tap_pause(session, sr_uuid, vdi_uuid)
1681 child._setParent(self.parent)
1682 finally:
1683 blktap2.VDI.tap_unpause(session, sr_uuid, vdi_uuid)
1684 self.children = []
1686 @override
1687 def _setParent(self, parent) -> None:
1688 self.sr._linstor.get_device_path(self.uuid)
1689 self.sr._vhdutil.force_parent(self.path, parent.path)
1690 self.parent = parent
1691 self.parentUuid = parent.uuid
1692 parent.children.append(self)
1693 try:
1694 self.setConfig(self.DB_VHD_PARENT, self.parentUuid)
1695 Util.log("Updated the vhd-parent field for child %s with %s" % \
1696 (self.uuid, self.parentUuid))
1697 except:
1698 Util.log("Failed to update %s with vhd-parent field %s" % \
1699 (self.uuid, self.parentUuid))
1701 @override
1702 def _doCoalesce(self) -> None:
1703 try:
1704 self._activateChain()
1705 self.parent.validate()
1706 self._inflateParentForCoalesce()
1707 VDI._doCoalesce(self)
1708 finally:
1709 self.parent.deflate()
1711 def _activateChain(self):
1712 vdi = self
1713 while vdi:
1714 try:
1715 p = self.sr._linstor.get_device_path(vdi.uuid)
1716 except Exception as e:
1717 # Use SMException to skip coalesce.
1718 # Otherwise the GC is stopped...
1719 raise util.SMException(str(e))
1720 vdi = vdi.parent
1722 @override
1723 def _setHidden(self, hidden=True) -> None:
1724 HIDDEN_TAG = 'hidden'
1726 if self.raw:
1727 self.sr._linstor.update_volume_metadata(self.uuid, {
1728 HIDDEN_TAG: hidden
1729 })
1730 self.hidden = hidden
1731 else:
1732 VDI._setHidden(self, hidden)
1734 @override
1735 def _setSizeVirt(self, size) -> None:
1736 jfile = self.uuid + '-jvhd'
1737 self.sr._linstor.create_volume(
1738 jfile, vhdutil.MAX_VHD_JOURNAL_SIZE, persistent=False, volume_name=jfile
1739 )
1740 try:
1741 self.inflate(LinstorVhdUtil.compute_volume_size(size, self.vdi_type))
1742 self.sr._vhdutil.set_size_virt(size, jfile)
1743 finally:
1744 try:
1745 self.sr._linstor.destroy_volume(jfile)
1746 except Exception:
1747 # We can ignore it, in any case this volume is not persistent.
1748 pass
1750 @override
1751 def _queryVHDBlocks(self) -> bytes:
1752 return self.sr._vhdutil.get_block_bitmap(self.uuid)
1754 def _inflateParentForCoalesce(self):
1755 if self.parent.raw:
1756 return
1757 inc = self._calcExtraSpaceForCoalescing()
1758 if inc > 0:
1759 self.parent.inflate(self.parent.getDrbdSize() + inc)
1761 @override
1762 def _calcExtraSpaceForCoalescing(self) -> int:
1763 if self.parent.raw:
1764 return 0
1765 size_coalesced = LinstorVhdUtil.compute_volume_size(
1766 self._getCoalescedSizeData(), self.vdi_type
1767 )
1768 Util.log("Coalesced size = %s" % Util.num2str(size_coalesced))
1769 return size_coalesced - self.parent.getDrbdSize()
1771 @override
1772 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1773 assert self.getDrbdSize() > 0
1774 assert self.getSizeVHD() > 0
1775 deflate_diff = self.getDrbdSize() - LinstorVolumeManager.round_up_volume_size(self.getSizeVHD())
1776 assert deflate_diff >= 0
1777 return self._calcExtraSpaceForCoalescing() - deflate_diff
1779 @override
1780 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1781 assert self.getSizeVHD() > 0
1782 return self._calcExtraSpaceForCoalescing() + \
1783 LinstorVolumeManager.round_up_volume_size(self.getSizeVHD())
1785################################################################################
1786#
1787# SR
1788#
1789class SR(object):
1790 class LogFilter:
1791 def __init__(self, sr):
1792 self.sr = sr
1793 self.stateLogged = False
1794 self.prevState = {}
1795 self.currState = {}
1797 def logState(self):
1798 changes = ""
1799 self.currState.clear()
1800 for vdi in self.sr.vdiTrees:
1801 self.currState[vdi.uuid] = self._getTreeStr(vdi)
1802 if not self.prevState.get(vdi.uuid) or \
1803 self.prevState[vdi.uuid] != self.currState[vdi.uuid]:
1804 changes += self.currState[vdi.uuid]
1806 for uuid in self.prevState:
1807 if not self.currState.get(uuid):
1808 changes += "Tree %s gone\n" % uuid
1810 result = "SR %s (%d VDIs in %d VHD trees): " % \
1811 (self.sr, len(self.sr.vdis), len(self.sr.vdiTrees))
1813 if len(changes) > 0:
1814 if self.stateLogged:
1815 result += "showing only VHD trees that changed:"
1816 result += "\n%s" % changes
1817 else:
1818 result += "no changes"
1820 for line in result.split("\n"):
1821 Util.log("%s" % line)
1822 self.prevState.clear()
1823 for key, val in self.currState.items():
1824 self.prevState[key] = val
1825 self.stateLogged = True
1827 def logNewVDI(self, uuid):
1828 if self.stateLogged:
1829 Util.log("Found new VDI when scanning: %s" % uuid)
1831 def _getTreeStr(self, vdi, indent=8):
1832 treeStr = "%s%s\n" % (" " * indent, vdi)
1833 for child in vdi.children:
1834 treeStr += self._getTreeStr(child, indent + VDI.STR_TREE_INDENT)
1835 return treeStr
1837 TYPE_FILE = "file"
1838 TYPE_LVHD = "lvhd"
1839 TYPE_LINSTOR = "linstor"
1840 TYPES = [TYPE_LVHD, TYPE_FILE, TYPE_LINSTOR]
1842 LOCK_RETRY_INTERVAL = 3
1843 LOCK_RETRY_ATTEMPTS = 20
1844 LOCK_RETRY_ATTEMPTS_LOCK = 100
1846 SCAN_RETRY_ATTEMPTS = 3
1848 JRN_CLONE = "clone" # journal entry type for the clone operation (from SM)
1849 TMP_RENAME_PREFIX = "OLD_"
1851 KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline"
1852 KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override"
1854 @staticmethod
1855 def getInstance(uuid, xapiSession, createLock=True, force=False):
1856 xapi = XAPI(xapiSession, uuid)
1857 type = normalizeType(xapi.srRecord["type"])
1858 if type == SR.TYPE_FILE:
1859 return FileSR(uuid, xapi, createLock, force)
1860 elif type == SR.TYPE_LVHD:
1861 return LVHDSR(uuid, xapi, createLock, force)
1862 elif type == SR.TYPE_LINSTOR:
1863 return LinstorSR(uuid, xapi, createLock, force)
1864 raise util.SMException("SR type %s not recognized" % type)
1866 def __init__(self, uuid, xapi, createLock, force):
1867 self.logFilter = self.LogFilter(self)
1868 self.uuid = uuid
1869 self.path = ""
1870 self.name = ""
1871 self.vdis = {}
1872 self.vdiTrees = []
1873 self.journaler = None
1874 self.xapi = xapi
1875 self._locked = 0
1876 self._srLock = None
1877 if createLock: 1877 ↛ 1878line 1877 didn't jump to line 1878, because the condition on line 1877 was never true
1878 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
1879 else:
1880 Util.log("Requested no SR locking")
1881 self.name = self.xapi.srRecord["name_label"]
1882 self._failedCoalesceTargets = []
1884 if not self.xapi.isPluggedHere():
1885 if force: 1885 ↛ 1886line 1885 didn't jump to line 1886, because the condition on line 1885 was never true
1886 Util.log("SR %s not attached on this host, ignoring" % uuid)
1887 else:
1888 if not self.wait_for_plug():
1889 raise util.SMException("SR %s not attached on this host" % uuid)
1891 if force: 1891 ↛ 1892line 1891 didn't jump to line 1892, because the condition on line 1891 was never true
1892 Util.log("Not checking if we are Master (SR %s)" % uuid)
1893 elif not self.xapi.isMaster(): 1893 ↛ 1894line 1893 didn't jump to line 1894, because the condition on line 1893 was never true
1894 raise util.SMException("This host is NOT master, will not run")
1896 def wait_for_plug(self):
1897 for _ in range(1, 10):
1898 time.sleep(2)
1899 if self.xapi.isPluggedHere():
1900 return True
1901 return False
1903 def gcEnabled(self, refresh=True):
1904 if refresh:
1905 self.xapi.srRecord = \
1906 self.xapi.session.xenapi.SR.get_record(self.xapi._srRef)
1907 if self.xapi.srRecord["other_config"].get(VDI.DB_GC) == "false":
1908 Util.log("GC is disabled for this SR, abort")
1909 return False
1910 return True
1912 def scan(self, force=False) -> None:
1913 """Scan the SR and load VDI info for each VDI. If called repeatedly,
1914 update VDI objects if they already exist"""
1915 pass
1917 def scanLocked(self, force=False):
1918 self.lock()
1919 try:
1920 self.scan(force)
1921 finally:
1922 self.unlock()
1924 def getVDI(self, uuid):
1925 return self.vdis.get(uuid)
1927 def hasWork(self):
1928 if len(self.findGarbage()) > 0:
1929 return True
1930 if self.findCoalesceable():
1931 return True
1932 if self.findLeafCoalesceable():
1933 return True
1934 if self.needUpdateBlockInfo():
1935 return True
1936 return False
1938 def findCoalesceable(self):
1939 """Find a coalesceable VDI. Return a vdi that should be coalesced
1940 (choosing one among all coalesceable candidates according to some
1941 criteria) or None if there is no VDI that could be coalesced"""
1943 candidates = []
1945 srSwitch = self.xapi.srRecord["other_config"].get(VDI.DB_COALESCE)
1946 if srSwitch == "false":
1947 Util.log("Coalesce disabled for this SR")
1948 return candidates
1950 # finish any VDI for which a relink journal entry exists first
1951 journals = self.journaler.getAll(VDI.JRN_RELINK)
1952 for uuid in journals:
1953 vdi = self.getVDI(uuid)
1954 if vdi and vdi not in self._failedCoalesceTargets:
1955 return vdi
1957 for vdi in self.vdis.values():
1958 if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets:
1959 candidates.append(vdi)
1960 Util.log("%s is coalescable" % vdi.uuid)
1962 self.xapi.update_task_progress("coalescable", len(candidates))
1964 # pick one in the tallest tree
1965 treeHeight = dict()
1966 for c in candidates:
1967 height = c.getTreeRoot().getTreeHeight()
1968 if treeHeight.get(height):
1969 treeHeight[height].append(c)
1970 else:
1971 treeHeight[height] = [c]
1973 freeSpace = self.getFreeSpace()
1974 heights = list(treeHeight.keys())
1975 heights.sort(reverse=True)
1976 for h in heights:
1977 for c in treeHeight[h]:
1978 spaceNeeded = c._calcExtraSpaceForCoalescing()
1979 if spaceNeeded <= freeSpace:
1980 Util.log("Coalesce candidate: %s (tree height %d)" % (c, h))
1981 return c
1982 else:
1983 Util.log("No space to coalesce %s (free space: %d)" % \
1984 (c, freeSpace))
1985 return None
1987 def getSwitch(self, key):
1988 return self.xapi.srRecord["other_config"].get(key)
1990 def forbiddenBySwitch(self, switch, condition, fail_msg):
1991 srSwitch = self.getSwitch(switch)
1992 ret = False
1993 if srSwitch:
1994 ret = srSwitch == condition
1996 if ret:
1997 Util.log(fail_msg)
1999 return ret
2001 def leafCoalesceForbidden(self):
2002 return (self.forbiddenBySwitch(VDI.DB_COALESCE,
2003 "false",
2004 "Coalesce disabled for this SR") or
2005 self.forbiddenBySwitch(VDI.DB_LEAFCLSC,
2006 VDI.LEAFCLSC_DISABLED,
2007 "Leaf-coalesce disabled for this SR"))
2009 def findLeafCoalesceable(self):
2010 """Find leaf-coalesceable VDIs in each VHD tree"""
2012 candidates = []
2013 if self.leafCoalesceForbidden():
2014 return candidates
2016 self.gatherLeafCoalesceable(candidates)
2018 self.xapi.update_task_progress("coalescable", len(candidates))
2020 freeSpace = self.getFreeSpace()
2021 for candidate in candidates:
2022 # check the space constraints to see if leaf-coalesce is actually
2023 # feasible for this candidate
2024 spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing()
2025 spaceNeededLive = spaceNeeded
2026 if spaceNeeded > freeSpace:
2027 spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing()
2028 if candidate.canLiveCoalesce(self.getStorageSpeed()):
2029 spaceNeeded = spaceNeededLive
2031 if spaceNeeded <= freeSpace:
2032 Util.log("Leaf-coalesce candidate: %s" % candidate)
2033 return candidate
2034 else:
2035 Util.log("No space to leaf-coalesce %s (free space: %d)" % \
2036 (candidate, freeSpace))
2037 if spaceNeededLive <= freeSpace:
2038 Util.log("...but enough space if skip snap-coalesce")
2039 candidate.setConfig(VDI.DB_LEAFCLSC,
2040 VDI.LEAFCLSC_OFFLINE)
2042 return None
2044 def gatherLeafCoalesceable(self, candidates):
2045 for vdi in self.vdis.values():
2046 if not vdi.isLeafCoalesceable():
2047 continue
2048 if vdi in self._failedCoalesceTargets:
2049 continue
2050 if vdi.getConfig(vdi.DB_ONBOOT) == vdi.ONBOOT_RESET:
2051 Util.log("Skipping reset-on-boot %s" % vdi)
2052 continue
2053 if vdi.getConfig(vdi.DB_ALLOW_CACHING):
2054 Util.log("Skipping allow_caching=true %s" % vdi)
2055 continue
2056 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED:
2057 Util.log("Leaf-coalesce disabled for %s" % vdi)
2058 continue
2059 if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or
2060 vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE):
2061 continue
2062 candidates.append(vdi)
2064 def coalesce(self, vdi, dryRun=False):
2065 """Coalesce vdi onto parent"""
2066 Util.log("Coalescing %s -> %s" % (vdi, vdi.parent))
2067 if dryRun: 2067 ↛ 2068line 2067 didn't jump to line 2068, because the condition on line 2067 was never true
2068 return
2070 try:
2071 self._coalesce(vdi)
2072 except util.SMException as e:
2073 if isinstance(e, AbortException): 2073 ↛ 2074line 2073 didn't jump to line 2074, because the condition on line 2073 was never true
2074 self.cleanup()
2075 raise
2076 else:
2077 self._failedCoalesceTargets.append(vdi)
2078 Util.logException("coalesce")
2079 Util.log("Coalesce failed, skipping")
2080 self.cleanup()
2082 def coalesceLeaf(self, vdi, dryRun=False):
2083 """Leaf-coalesce vdi onto parent"""
2084 Util.log("Leaf-coalescing %s -> %s" % (vdi, vdi.parent))
2085 if dryRun:
2086 return
2088 try:
2089 uuid = vdi.uuid
2090 try:
2091 # "vdi" object will no longer be valid after this call
2092 self._coalesceLeaf(vdi)
2093 finally:
2094 vdi = self.getVDI(uuid)
2095 if vdi:
2096 vdi.delConfig(vdi.DB_LEAFCLSC)
2097 except AbortException:
2098 self.cleanup()
2099 raise
2100 except (util.SMException, XenAPI.Failure) as e:
2101 self._failedCoalesceTargets.append(vdi)
2102 Util.logException("leaf-coalesce")
2103 Util.log("Leaf-coalesce failed on %s, skipping" % vdi)
2104 self.cleanup()
2106 def garbageCollect(self, dryRun=False):
2107 vdiList = self.findGarbage()
2108 Util.log("Found %d VDIs for deletion:" % len(vdiList))
2109 for vdi in vdiList:
2110 Util.log(" %s" % vdi)
2111 if not dryRun:
2112 self.deleteVDIs(vdiList)
2113 self.cleanupJournals(dryRun)
2115 def findGarbage(self):
2116 vdiList = []
2117 for vdi in self.vdiTrees:
2118 vdiList.extend(vdi.getAllPrunable())
2119 return vdiList
2121 def deleteVDIs(self, vdiList) -> None:
2122 for vdi in vdiList:
2123 if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT):
2124 raise AbortException("Aborting due to signal")
2125 Util.log("Deleting unlinked VDI %s" % vdi)
2126 self.deleteVDI(vdi)
2128 def deleteVDI(self, vdi) -> None:
2129 assert(len(vdi.children) == 0)
2130 del self.vdis[vdi.uuid]
2131 if vdi.parent: 2131 ↛ 2133line 2131 didn't jump to line 2133, because the condition on line 2131 was never false
2132 vdi.parent.children.remove(vdi)
2133 if vdi in self.vdiTrees: 2133 ↛ 2134line 2133 didn't jump to line 2134, because the condition on line 2133 was never true
2134 self.vdiTrees.remove(vdi)
2135 vdi.delete()
2137 def forgetVDI(self, vdiUuid) -> None:
2138 self.xapi.forgetVDI(self.uuid, vdiUuid)
2140 def pauseVDIs(self, vdiList) -> None:
2141 paused = []
2142 failed = False
2143 for vdi in vdiList:
2144 try:
2145 vdi.pause()
2146 paused.append(vdi)
2147 except:
2148 Util.logException("pauseVDIs")
2149 failed = True
2150 break
2152 if failed:
2153 self.unpauseVDIs(paused)
2154 raise util.SMException("Failed to pause VDIs")
2156 def unpauseVDIs(self, vdiList):
2157 failed = False
2158 for vdi in vdiList:
2159 try:
2160 vdi.unpause()
2161 except:
2162 Util.log("ERROR: Failed to unpause VDI %s" % vdi)
2163 failed = True
2164 if failed:
2165 raise util.SMException("Failed to unpause VDIs")
2167 def getFreeSpace(self) -> int:
2168 return 0
2170 def cleanup(self):
2171 Util.log("In cleanup")
2172 return
2174 @override
2175 def __str__(self) -> str:
2176 if self.name:
2177 ret = "%s ('%s')" % (self.uuid[0:4], self.name)
2178 else:
2179 ret = "%s" % self.uuid
2180 return ret
2182 def lock(self):
2183 """Acquire the SR lock. Nested acquire()'s are ok. Check for Abort
2184 signal to avoid deadlocking (trying to acquire the SR lock while the
2185 lock is held by a process that is trying to abort us)"""
2186 if not self._srLock:
2187 return
2189 if self._locked == 0:
2190 abortFlag = IPCFlag(self.uuid)
2191 for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK):
2192 if self._srLock.acquireNoblock():
2193 self._locked += 1
2194 return
2195 if abortFlag.test(FLAG_TYPE_ABORT):
2196 raise AbortException("Abort requested")
2197 time.sleep(SR.LOCK_RETRY_INTERVAL)
2198 raise util.SMException("Unable to acquire the SR lock")
2200 self._locked += 1
2202 def unlock(self):
2203 if not self._srLock: 2203 ↛ 2205line 2203 didn't jump to line 2205, because the condition on line 2203 was never false
2204 return
2205 assert(self._locked > 0)
2206 self._locked -= 1
2207 if self._locked == 0:
2208 self._srLock.release()
2210 def needUpdateBlockInfo(self) -> bool:
2211 for vdi in self.vdis.values():
2212 if vdi.scanError or len(vdi.children) == 0:
2213 continue
2214 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2215 return True
2216 return False
2218 def updateBlockInfo(self) -> None:
2219 for vdi in self.vdis.values():
2220 if vdi.scanError or len(vdi.children) == 0:
2221 continue
2222 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2223 vdi.updateBlockInfo()
2225 def cleanupCoalesceJournals(self):
2226 """Remove stale coalesce VDI indicators"""
2227 entries = self.journaler.getAll(VDI.JRN_COALESCE)
2228 for uuid, jval in entries.items():
2229 self.journaler.remove(VDI.JRN_COALESCE, uuid)
2231 def cleanupJournals(self, dryRun=False):
2232 """delete journal entries for non-existing VDIs"""
2233 for t in [LVHDVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]:
2234 entries = self.journaler.getAll(t)
2235 for uuid, jval in entries.items():
2236 if self.getVDI(uuid):
2237 continue
2238 if t == SR.JRN_CLONE:
2239 baseUuid, clonUuid = jval.split("_")
2240 if self.getVDI(baseUuid):
2241 continue
2242 Util.log(" Deleting stale '%s' journal entry for %s "
2243 "(%s)" % (t, uuid, jval))
2244 if not dryRun:
2245 self.journaler.remove(t, uuid)
2247 def cleanupCache(self, maxAge=-1) -> int:
2248 return 0
2250 def _coalesce(self, vdi):
2251 if self.journaler.get(vdi.JRN_RELINK, vdi.uuid): 2251 ↛ 2254line 2251 didn't jump to line 2254, because the condition on line 2251 was never true
2252 # this means we had done the actual coalescing already and just
2253 # need to finish relinking and/or refreshing the children
2254 Util.log("==> Coalesce apparently already done: skipping")
2255 else:
2256 # JRN_COALESCE is used to check which VDI is being coalesced in
2257 # order to decide whether to abort the coalesce. We remove the
2258 # journal as soon as the VHD coalesce step is done, because we
2259 # don't expect the rest of the process to take long
2260 self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1")
2261 vdi._doCoalesce()
2262 self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid)
2264 util.fistpoint.activate("LVHDRT_before_create_relink_journal", self.uuid)
2266 # we now need to relink the children: lock the SR to prevent ops
2267 # like SM.clone from manipulating the VDIs we'll be relinking and
2268 # rescan the SR first in case the children changed since the last
2269 # scan
2270 self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1")
2272 self.lock()
2273 try:
2274 vdi.parent._tagChildrenForRelink()
2275 self.scan()
2276 vdi._relinkSkip()
2277 finally:
2278 self.unlock()
2279 # Reload the children to leave things consistent
2280 vdi.parent._reloadChildren(vdi)
2282 self.journaler.remove(vdi.JRN_RELINK, vdi.uuid)
2283 self.deleteVDI(vdi)
2285 class CoalesceTracker:
2286 GRACE_ITERATIONS = 1
2287 MAX_ITERATIONS_NO_PROGRESS = 3
2288 MAX_ITERATIONS = 10
2289 MAX_INCREASE_FROM_MINIMUM = 1.2
2290 HISTORY_STRING = "Iteration: {its} -- Initial size {initSize}" \
2291 " --> Final size {finSize}"
2293 def __init__(self, sr):
2294 self.itsNoProgress = 0
2295 self.its = 0
2296 self.minSize = float("inf")
2297 self.history = []
2298 self.reason = ""
2299 self.startSize = None
2300 self.finishSize = None
2301 self.sr = sr
2303 def abortCoalesce(self, prevSize, curSize):
2304 res = False
2306 self.its += 1
2307 self.history.append(self.HISTORY_STRING.format(its=self.its,
2308 initSize=prevSize,
2309 finSize=curSize))
2311 self.finishSize = curSize
2313 if self.startSize is None:
2314 self.startSize = prevSize
2316 if curSize < self.minSize:
2317 self.minSize = curSize
2319 if prevSize < self.minSize:
2320 self.minSize = prevSize
2322 if prevSize < curSize:
2323 self.itsNoProgress += 1
2324 Util.log("No progress, attempt:"
2325 " {attempt}".format(attempt=self.itsNoProgress))
2326 util.fistpoint.activate("cleanup_tracker_no_progress", self.sr.uuid)
2328 if (not res) and (self.its > self.MAX_ITERATIONS):
2329 max = self.MAX_ITERATIONS
2330 self.reason = \
2331 "Max iterations ({max}) exceeded".format(max=max)
2332 res = True
2334 if (not res) and (self.itsNoProgress >
2335 self.MAX_ITERATIONS_NO_PROGRESS):
2336 max = self.MAX_ITERATIONS_NO_PROGRESS
2337 self.reason = \
2338 "No progress made for {max} iterations".format(max=max)
2339 res = True
2341 maxSizeFromMin = self.MAX_INCREASE_FROM_MINIMUM * self.minSize
2342 if (self.its > self.GRACE_ITERATIONS and
2343 (not res) and (curSize > maxSizeFromMin)):
2344 self.reason = "Unexpected bump in size," \
2345 " compared to minimum acheived"
2346 res = True
2348 return res
2350 def printReasoning(self):
2351 Util.log("Aborted coalesce")
2352 for hist in self.history:
2353 Util.log(hist)
2354 Util.log(self.reason)
2355 Util.log("Starting size was {size}"
2356 .format(size=self.startSize))
2357 Util.log("Final size was {size}"
2358 .format(size=self.finishSize))
2359 Util.log("Minimum size acheived was {size}"
2360 .format(size=self.minSize))
2362 def _coalesceLeaf(self, vdi):
2363 """Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot
2364 complete due to external changes, namely vdi_delete and vdi_snapshot
2365 that alter leaf-coalescibility of vdi"""
2366 tracker = self.CoalesceTracker(self)
2367 while not vdi.canLiveCoalesce(self.getStorageSpeed()):
2368 prevSizeVHD = vdi.getSizeVHD()
2369 if not self._snapshotCoalesce(vdi): 2369 ↛ 2370line 2369 didn't jump to line 2370, because the condition on line 2369 was never true
2370 return False
2371 if tracker.abortCoalesce(prevSizeVHD, vdi.getSizeVHD()):
2372 tracker.printReasoning()
2373 raise util.SMException("VDI {uuid} could not be coalesced"
2374 .format(uuid=vdi.uuid))
2375 return self._liveLeafCoalesce(vdi)
2377 def calcStorageSpeed(self, startTime, endTime, vhdSize):
2378 speed = None
2379 total_time = endTime - startTime
2380 if total_time > 0:
2381 speed = float(vhdSize) / float(total_time)
2382 return speed
2384 def writeSpeedToFile(self, speed):
2385 content = []
2386 speedFile = None
2387 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2388 self.lock()
2389 try:
2390 Util.log("Writing to file: {myfile}".format(myfile=path))
2391 lines = ""
2392 if not os.path.isfile(path):
2393 lines = str(speed) + "\n"
2394 else:
2395 speedFile = open(path, "r+")
2396 content = speedFile.readlines()
2397 content.append(str(speed) + "\n")
2398 if len(content) > N_RUNNING_AVERAGE:
2399 del content[0]
2400 lines = "".join(content)
2402 util.atomicFileWrite(path, VAR_RUN, lines)
2403 finally:
2404 if speedFile is not None:
2405 speedFile.close()
2406 Util.log("Closing file: {myfile}".format(myfile=path))
2407 self.unlock()
2409 def recordStorageSpeed(self, startTime, endTime, vhdSize):
2410 speed = self.calcStorageSpeed(startTime, endTime, vhdSize)
2411 if speed is None:
2412 return
2414 self.writeSpeedToFile(speed)
2416 def getStorageSpeed(self):
2417 speedFile = None
2418 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2419 self.lock()
2420 try:
2421 speed = None
2422 if os.path.isfile(path):
2423 speedFile = open(path)
2424 content = speedFile.readlines()
2425 try:
2426 content = [float(i) for i in content]
2427 except ValueError:
2428 Util.log("Something bad in the speed log:{log}".
2429 format(log=speedFile.readlines()))
2430 return speed
2432 if len(content):
2433 speed = sum(content) / float(len(content))
2434 if speed <= 0: 2434 ↛ 2436line 2434 didn't jump to line 2436, because the condition on line 2434 was never true
2435 # Defensive, should be impossible.
2436 Util.log("Bad speed: {speed} calculated for SR: {uuid}".
2437 format(speed=speed, uuid=self.uuid))
2438 speed = None
2439 else:
2440 Util.log("Speed file empty for SR: {uuid}".
2441 format(uuid=self.uuid))
2442 else:
2443 Util.log("Speed log missing for SR: {uuid}".
2444 format(uuid=self.uuid))
2445 return speed
2446 finally:
2447 if not (speedFile is None):
2448 speedFile.close()
2449 self.unlock()
2451 def _snapshotCoalesce(self, vdi):
2452 # Note that because we are not holding any locks here, concurrent SM
2453 # operations may change this tree under our feet. In particular, vdi
2454 # can be deleted, or it can be snapshotted.
2455 assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED)
2456 Util.log("Single-snapshotting %s" % vdi)
2457 util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid)
2458 try:
2459 ret = self.xapi.singleSnapshotVDI(vdi)
2460 Util.log("Single-snapshot returned: %s" % ret)
2461 except XenAPI.Failure as e:
2462 if util.isInvalidVDI(e):
2463 Util.log("The VDI appears to have been concurrently deleted")
2464 return False
2465 raise
2466 self.scanLocked()
2467 tempSnap = vdi.parent
2468 if not tempSnap.isCoalesceable():
2469 Util.log("The VDI appears to have been concurrently snapshotted")
2470 return False
2471 Util.log("Coalescing parent %s" % tempSnap)
2472 util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid)
2473 vhdSize = vdi.getSizeVHD()
2474 self._coalesce(tempSnap)
2475 if not vdi.isLeafCoalesceable():
2476 Util.log("The VDI tree appears to have been altered since")
2477 return False
2478 return True
2480 def _liveLeafCoalesce(self, vdi) -> bool:
2481 util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid)
2482 self.lock()
2483 try:
2484 self.scan()
2485 if not self.getVDI(vdi.uuid):
2486 Util.log("The VDI appears to have been deleted meanwhile")
2487 return False
2488 if not vdi.isLeafCoalesceable():
2489 Util.log("The VDI is no longer leaf-coalesceable")
2490 return False
2492 uuid = vdi.uuid
2493 vdi.pause(failfast=True)
2494 try:
2495 try:
2496 # "vdi" object will no longer be valid after this call
2497 self._doCoalesceLeaf(vdi)
2498 except:
2499 Util.logException("_doCoalesceLeaf")
2500 self._handleInterruptedCoalesceLeaf()
2501 raise
2502 finally:
2503 vdi = self.getVDI(uuid)
2504 if vdi:
2505 vdi.ensureUnpaused()
2506 vdiOld = self.getVDI(self.TMP_RENAME_PREFIX + uuid)
2507 if vdiOld:
2508 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2509 self.deleteVDI(vdiOld)
2510 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2511 finally:
2512 self.cleanup()
2513 self.unlock()
2514 self.logFilter.logState()
2515 return True
2517 def _doCoalesceLeaf(self, vdi):
2518 """Actual coalescing of a leaf VDI onto parent. Must be called in an
2519 offline/atomic context"""
2520 self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid)
2521 self._prepareCoalesceLeaf(vdi)
2522 vdi.parent._setHidden(False)
2523 vdi.parent._increaseSizeVirt(vdi.sizeVirt, False)
2524 vdi.validate(True)
2525 vdi.parent.validate(True)
2526 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
2527 timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT
2528 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE:
2529 Util.log("Leaf-coalesce forced, will not use timeout")
2530 timeout = 0
2531 vdi._coalesceVHD(timeout)
2532 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
2533 vdi.parent.validate(True)
2534 #vdi._verifyContents(timeout / 2)
2536 # rename
2537 vdiUuid = vdi.uuid
2538 oldName = vdi.fileName
2539 origParentUuid = vdi.parent.uuid
2540 vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid)
2541 util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid)
2542 vdi.parent.rename(vdiUuid)
2543 util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid)
2544 self._updateSlavesOnRename(vdi.parent, oldName, origParentUuid)
2546 # Note that "vdi.parent" is now the single remaining leaf and "vdi" is
2547 # garbage
2549 # update the VDI record
2550 vdi.parent.delConfig(VDI.DB_VHD_PARENT)
2551 if vdi.parent.raw:
2552 vdi.parent.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_RAW)
2553 vdi.parent.delConfig(VDI.DB_VHD_BLOCKS)
2554 util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid)
2556 self._updateNode(vdi)
2558 # delete the obsolete leaf & inflate the parent (in that order, to
2559 # minimize free space requirements)
2560 parent = vdi.parent
2561 vdi._setHidden(True)
2562 vdi.parent.children = []
2563 vdi.parent = None
2565 extraSpace = self._calcExtraSpaceNeeded(vdi, parent)
2566 freeSpace = self.getFreeSpace()
2567 if freeSpace < extraSpace:
2568 # don't delete unless we need the space: deletion is time-consuming
2569 # because it requires contacting the slaves, and we're paused here
2570 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2571 self.deleteVDI(vdi)
2572 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2574 util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid)
2575 self.journaler.remove(VDI.JRN_LEAF, vdiUuid)
2577 self.forgetVDI(origParentUuid)
2578 self._finishCoalesceLeaf(parent)
2579 self._updateSlavesOnResize(parent)
2581 def _calcExtraSpaceNeeded(self, child, parent) -> int:
2582 assert(not parent.raw) # raw parents not supported
2583 extra = child.getSizeVHD() - parent.getSizeVHD()
2584 if extra < 0:
2585 extra = 0
2586 return extra
2588 def _prepareCoalesceLeaf(self, vdi) -> None:
2589 pass
2591 def _updateNode(self, vdi) -> None:
2592 pass
2594 def _finishCoalesceLeaf(self, parent) -> None:
2595 pass
2597 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
2598 pass
2600 def _updateSlavesOnRename(self, vdi, oldName, origParentUuid) -> None:
2601 pass
2603 def _updateSlavesOnResize(self, vdi) -> None:
2604 pass
2606 def _removeStaleVDIs(self, uuidsPresent) -> None:
2607 for uuid in list(self.vdis.keys()):
2608 if not uuid in uuidsPresent:
2609 Util.log("VDI %s disappeared since last scan" % \
2610 self.vdis[uuid])
2611 del self.vdis[uuid]
2613 def _handleInterruptedCoalesceLeaf(self) -> None:
2614 """An interrupted leaf-coalesce operation may leave the VHD tree in an
2615 inconsistent state. If the old-leaf VDI is still present, we revert the
2616 operation (in case the original error is persistent); otherwise we must
2617 finish the operation"""
2618 pass
2620 def _buildTree(self, force):
2621 self.vdiTrees = []
2622 for vdi in self.vdis.values():
2623 if vdi.parentUuid:
2624 parent = self.getVDI(vdi.parentUuid)
2625 if not parent:
2626 if vdi.uuid.startswith(self.TMP_RENAME_PREFIX):
2627 self.vdiTrees.append(vdi)
2628 continue
2629 if force:
2630 Util.log("ERROR: Parent VDI %s not found! (for %s)" % \
2631 (vdi.parentUuid, vdi.uuid))
2632 self.vdiTrees.append(vdi)
2633 continue
2634 else:
2635 raise util.SMException("Parent VDI %s of %s not " \
2636 "found" % (vdi.parentUuid, vdi.uuid))
2637 vdi.parent = parent
2638 parent.children.append(vdi)
2639 else:
2640 self.vdiTrees.append(vdi)
2643class FileSR(SR):
2644 TYPE = SR.TYPE_FILE
2645 CACHE_FILE_EXT = ".vhdcache"
2646 # cache cleanup actions
2647 CACHE_ACTION_KEEP = 0
2648 CACHE_ACTION_REMOVE = 1
2649 CACHE_ACTION_REMOVE_IF_INACTIVE = 2
2651 def __init__(self, uuid, xapi, createLock, force):
2652 SR.__init__(self, uuid, xapi, createLock, force)
2653 self.path = "/var/run/sr-mount/%s" % self.uuid
2654 self.journaler = fjournaler.Journaler(self.path)
2656 @override
2657 def scan(self, force=False) -> None:
2658 if not util.pathexists(self.path):
2659 raise util.SMException("directory %s not found!" % self.uuid)
2660 vhds = self._scan(force)
2661 for uuid, vhdInfo in vhds.items():
2662 vdi = self.getVDI(uuid)
2663 if not vdi:
2664 self.logFilter.logNewVDI(uuid)
2665 vdi = FileVDI(self, uuid, False)
2666 self.vdis[uuid] = vdi
2667 vdi.load(vhdInfo)
2668 uuidsPresent = list(vhds.keys())
2669 rawList = [x for x in os.listdir(self.path) if x.endswith(vhdutil.FILE_EXTN_RAW)]
2670 for rawName in rawList:
2671 uuid = FileVDI.extractUuid(rawName)
2672 uuidsPresent.append(uuid)
2673 vdi = self.getVDI(uuid)
2674 if not vdi:
2675 self.logFilter.logNewVDI(uuid)
2676 vdi = FileVDI(self, uuid, True)
2677 self.vdis[uuid] = vdi
2678 self._removeStaleVDIs(uuidsPresent)
2679 self._buildTree(force)
2680 self.logFilter.logState()
2681 self._handleInterruptedCoalesceLeaf()
2683 @override
2684 def getFreeSpace(self) -> int:
2685 return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path)
2687 @override
2688 def deleteVDIs(self, vdiList) -> None:
2689 rootDeleted = False
2690 for vdi in vdiList:
2691 if not vdi.parent:
2692 rootDeleted = True
2693 break
2694 SR.deleteVDIs(self, vdiList)
2695 if self.xapi.srRecord["type"] == "nfs" and rootDeleted:
2696 self.xapi.markCacheSRsDirty()
2698 @override
2699 def cleanupCache(self, maxAge=-1) -> int:
2700 """Clean up IntelliCache cache files. Caches for leaf nodes are
2701 removed when the leaf node no longer exists or its allow-caching
2702 attribute is not set. Caches for parent nodes are removed when the
2703 parent node no longer exists or it hasn't been used in more than
2704 <maxAge> hours.
2705 Return number of caches removed.
2706 """
2707 numRemoved = 0
2708 cacheFiles = [x for x in os.listdir(self.path) if self._isCacheFileName(x)]
2709 Util.log("Found %d cache files" % len(cacheFiles))
2710 cutoff = datetime.datetime.now() - datetime.timedelta(hours=maxAge)
2711 for cacheFile in cacheFiles:
2712 uuid = cacheFile[:-len(self.CACHE_FILE_EXT)]
2713 action = self.CACHE_ACTION_KEEP
2714 rec = self.xapi.getRecordVDI(uuid)
2715 if not rec:
2716 Util.log("Cache %s: VDI doesn't exist" % uuid)
2717 action = self.CACHE_ACTION_REMOVE
2718 elif rec["managed"] and not rec["allow_caching"]:
2719 Util.log("Cache %s: caching disabled" % uuid)
2720 action = self.CACHE_ACTION_REMOVE
2721 elif not rec["managed"] and maxAge >= 0:
2722 lastAccess = datetime.datetime.fromtimestamp( \
2723 os.path.getatime(os.path.join(self.path, cacheFile)))
2724 if lastAccess < cutoff:
2725 Util.log("Cache %s: older than %d hrs" % (uuid, maxAge))
2726 action = self.CACHE_ACTION_REMOVE_IF_INACTIVE
2728 if action == self.CACHE_ACTION_KEEP:
2729 Util.log("Keeping cache %s" % uuid)
2730 continue
2732 lockId = uuid
2733 parentUuid = None
2734 if rec and rec["managed"]:
2735 parentUuid = rec["sm_config"].get("vhd-parent")
2736 if parentUuid:
2737 lockId = parentUuid
2739 cacheLock = lock.Lock(blktap2.VDI.LOCK_CACHE_SETUP, lockId)
2740 cacheLock.acquire()
2741 try:
2742 if self._cleanupCache(uuid, action):
2743 numRemoved += 1
2744 finally:
2745 cacheLock.release()
2746 return numRemoved
2748 def _cleanupCache(self, uuid, action):
2749 assert(action != self.CACHE_ACTION_KEEP)
2750 rec = self.xapi.getRecordVDI(uuid)
2751 if rec and rec["allow_caching"]:
2752 Util.log("Cache %s appears to have become valid" % uuid)
2753 return False
2755 fullPath = os.path.join(self.path, uuid + self.CACHE_FILE_EXT)
2756 tapdisk = blktap2.Tapdisk.find_by_path(fullPath)
2757 if tapdisk:
2758 if action == self.CACHE_ACTION_REMOVE_IF_INACTIVE:
2759 Util.log("Cache %s still in use" % uuid)
2760 return False
2761 Util.log("Shutting down tapdisk for %s" % fullPath)
2762 tapdisk.shutdown()
2764 Util.log("Deleting file %s" % fullPath)
2765 os.unlink(fullPath)
2766 return True
2768 def _isCacheFileName(self, name):
2769 return (len(name) == Util.UUID_LEN + len(self.CACHE_FILE_EXT)) and \
2770 name.endswith(self.CACHE_FILE_EXT)
2772 def _scan(self, force):
2773 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2774 error = False
2775 pattern = os.path.join(self.path, "*%s" % vhdutil.FILE_EXTN_VHD)
2776 vhds = vhdutil.getAllVHDs(pattern, FileVDI.extractUuid)
2777 for uuid, vhdInfo in vhds.items():
2778 if vhdInfo.error:
2779 error = True
2780 break
2781 if not error:
2782 return vhds
2783 Util.log("Scan error on attempt %d" % i)
2784 if force:
2785 return vhds
2786 raise util.SMException("Scan error")
2788 @override
2789 def deleteVDI(self, vdi) -> None:
2790 self._checkSlaves(vdi)
2791 SR.deleteVDI(self, vdi)
2793 def _checkSlaves(self, vdi):
2794 onlineHosts = self.xapi.getOnlineHosts()
2795 abortFlag = IPCFlag(self.uuid)
2796 for pbdRecord in self.xapi.getAttachedPBDs():
2797 hostRef = pbdRecord["host"]
2798 if hostRef == self.xapi._hostRef:
2799 continue
2800 if abortFlag.test(FLAG_TYPE_ABORT):
2801 raise AbortException("Aborting due to signal")
2802 try:
2803 self._checkSlave(hostRef, vdi)
2804 except util.CommandException:
2805 if hostRef in onlineHosts:
2806 raise
2808 def _checkSlave(self, hostRef, vdi):
2809 call = (hostRef, "nfs-on-slave", "check", {'path': vdi.path})
2810 Util.log("Checking with slave: %s" % repr(call))
2811 _host = self.xapi.session.xenapi.host
2812 text = _host.call_plugin( * call)
2814 @override
2815 def _handleInterruptedCoalesceLeaf(self) -> None:
2816 entries = self.journaler.getAll(VDI.JRN_LEAF)
2817 for uuid, parentUuid in entries.items():
2818 fileList = os.listdir(self.path)
2819 childName = uuid + vhdutil.FILE_EXTN_VHD
2820 tmpChildName = self.TMP_RENAME_PREFIX + uuid + vhdutil.FILE_EXTN_VHD
2821 parentName1 = parentUuid + vhdutil.FILE_EXTN_VHD
2822 parentName2 = parentUuid + vhdutil.FILE_EXTN_RAW
2823 parentPresent = (parentName1 in fileList or parentName2 in fileList)
2824 if parentPresent or tmpChildName in fileList:
2825 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
2826 else:
2827 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
2828 self.journaler.remove(VDI.JRN_LEAF, uuid)
2829 vdi = self.getVDI(uuid)
2830 if vdi:
2831 vdi.ensureUnpaused()
2833 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2834 Util.log("*** UNDO LEAF-COALESCE")
2835 parent = self.getVDI(parentUuid)
2836 if not parent:
2837 parent = self.getVDI(childUuid)
2838 if not parent:
2839 raise util.SMException("Neither %s nor %s found" % \
2840 (parentUuid, childUuid))
2841 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
2842 parent.rename(parentUuid)
2843 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
2845 child = self.getVDI(childUuid)
2846 if not child:
2847 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
2848 if not child:
2849 raise util.SMException("Neither %s nor %s found" % \
2850 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
2851 Util.log("Renaming child back to %s" % childUuid)
2852 child.rename(childUuid)
2853 Util.log("Updating the VDI record")
2854 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
2855 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
2856 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
2858 if child.hidden:
2859 child._setHidden(False)
2860 if not parent.hidden:
2861 parent._setHidden(True)
2862 self._updateSlavesOnUndoLeafCoalesce(parent, child)
2863 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
2864 Util.log("*** leaf-coalesce undo successful")
2865 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
2866 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
2868 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
2869 Util.log("*** FINISH LEAF-COALESCE")
2870 vdi = self.getVDI(childUuid)
2871 if not vdi:
2872 raise util.SMException("VDI %s not found" % childUuid)
2873 try:
2874 self.forgetVDI(parentUuid)
2875 except XenAPI.Failure:
2876 pass
2877 self._updateSlavesOnResize(vdi)
2878 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
2879 Util.log("*** finished leaf-coalesce successfully")
2882class LVHDSR(SR):
2883 TYPE = SR.TYPE_LVHD
2884 SUBTYPES = ["lvhdoiscsi", "lvhdohba"]
2886 def __init__(self, uuid, xapi, createLock, force):
2887 SR.__init__(self, uuid, xapi, createLock, force)
2888 self.vgName = "%s%s" % (lvhdutil.VG_PREFIX, self.uuid)
2889 self.path = os.path.join(lvhdutil.VG_LOCATION, self.vgName)
2891 sr_ref = self.xapi.session.xenapi.SR.get_by_uuid(self.uuid)
2892 other_conf = self.xapi.session.xenapi.SR.get_other_config(sr_ref)
2893 lvm_conf = other_conf.get('lvm-conf') if other_conf else None
2894 self.lvmCache = lvmcache.LVMCache(self.vgName, lvm_conf)
2896 self.lvActivator = LVActivator(self.uuid, self.lvmCache)
2897 self.journaler = journaler.Journaler(self.lvmCache)
2899 @override
2900 def deleteVDI(self, vdi) -> None:
2901 if self.lvActivator.get(vdi.uuid, False):
2902 self.lvActivator.deactivate(vdi.uuid, False)
2903 self._checkSlaves(vdi)
2904 SR.deleteVDI(self, vdi)
2906 @override
2907 def forgetVDI(self, vdiUuid) -> None:
2908 SR.forgetVDI(self, vdiUuid)
2909 mdpath = os.path.join(self.path, lvutil.MDVOLUME_NAME)
2910 LVMMetadataHandler(mdpath).deleteVdiFromMetadata(vdiUuid)
2912 @override
2913 def getFreeSpace(self) -> int:
2914 stats = lvutil._getVGstats(self.vgName)
2915 return stats['physical_size'] - stats['physical_utilisation']
2917 @override
2918 def cleanup(self):
2919 if not self.lvActivator.deactivateAll():
2920 Util.log("ERROR deactivating LVs while cleaning up")
2922 @override
2923 def needUpdateBlockInfo(self) -> bool:
2924 for vdi in self.vdis.values():
2925 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2926 continue
2927 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2928 return True
2929 return False
2931 @override
2932 def updateBlockInfo(self) -> None:
2933 numUpdated = 0
2934 for vdi in self.vdis.values():
2935 if vdi.scanError or vdi.raw or len(vdi.children) == 0:
2936 continue
2937 if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
2938 vdi.updateBlockInfo()
2939 numUpdated += 1
2940 if numUpdated:
2941 # deactivate the LVs back sooner rather than later. If we don't
2942 # now, by the time this thread gets to deactivations, another one
2943 # might have leaf-coalesced a node and deleted it, making the child
2944 # inherit the refcount value and preventing the correct decrement
2945 self.cleanup()
2947 @override
2948 def scan(self, force=False) -> None:
2949 vdis = self._scan(force)
2950 for uuid, vdiInfo in vdis.items():
2951 vdi = self.getVDI(uuid)
2952 if not vdi:
2953 self.logFilter.logNewVDI(uuid)
2954 vdi = LVHDVDI(self, uuid,
2955 vdiInfo.vdiType == vhdutil.VDI_TYPE_RAW)
2956 self.vdis[uuid] = vdi
2957 vdi.load(vdiInfo)
2958 self._removeStaleVDIs(vdis.keys())
2959 self._buildTree(force)
2960 self.logFilter.logState()
2961 self._handleInterruptedCoalesceLeaf()
2963 def _scan(self, force):
2964 for i in range(SR.SCAN_RETRY_ATTEMPTS):
2965 error = False
2966 self.lvmCache.refresh()
2967 vdis = lvhdutil.getVDIInfo(self.lvmCache)
2968 for uuid, vdiInfo in vdis.items():
2969 if vdiInfo.scanError:
2970 error = True
2971 break
2972 if not error:
2973 return vdis
2974 Util.log("Scan error, retrying (%d)" % i)
2975 if force:
2976 return vdis
2977 raise util.SMException("Scan error")
2979 @override
2980 def _removeStaleVDIs(self, uuidsPresent) -> None:
2981 for uuid in list(self.vdis.keys()):
2982 if not uuid in uuidsPresent:
2983 Util.log("VDI %s disappeared since last scan" % \
2984 self.vdis[uuid])
2985 del self.vdis[uuid]
2986 if self.lvActivator.get(uuid, False):
2987 self.lvActivator.remove(uuid, False)
2989 @override
2990 def _liveLeafCoalesce(self, vdi) -> bool:
2991 """If the parent is raw and the child was resized (virt. size), then
2992 we'll need to resize the parent, which can take a while due to zeroing
2993 out of the extended portion of the LV. Do it before pausing the child
2994 to avoid a protracted downtime"""
2995 if vdi.parent.raw and vdi.sizeVirt > vdi.parent.sizeVirt:
2996 self.lvmCache.setReadonly(vdi.parent.fileName, False)
2997 vdi.parent._increaseSizeVirt(vdi.sizeVirt)
2999 return SR._liveLeafCoalesce(self, vdi)
3001 @override
3002 def _prepareCoalesceLeaf(self, vdi) -> None:
3003 vdi._activateChain()
3004 self.lvmCache.setReadonly(vdi.parent.fileName, False)
3005 vdi.deflate()
3006 vdi.inflateParentForCoalesce()
3008 @override
3009 def _updateNode(self, vdi) -> None:
3010 # fix the refcounts: the remaining node should inherit the binary
3011 # refcount from the leaf (because if it was online, it should remain
3012 # refcounted as such), but the normal refcount from the parent (because
3013 # this node is really the parent node) - minus 1 if it is online (since
3014 # non-leaf nodes increment their normal counts when they are online and
3015 # we are now a leaf, storing that 1 in the binary refcount).
3016 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
3017 cCnt, cBcnt = RefCounter.check(vdi.uuid, ns)
3018 pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns)
3019 pCnt = pCnt - cBcnt
3020 assert(pCnt >= 0)
3021 RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns)
3023 @override
3024 def _finishCoalesceLeaf(self, parent) -> None:
3025 if not parent.isSnapshot() or parent.isAttachedRW():
3026 parent.inflateFully()
3027 else:
3028 parent.deflate()
3030 @override
3031 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3032 return lvhdutil.calcSizeVHDLV(parent.sizeVirt) - parent.sizeLV
3034 @override
3035 def _handleInterruptedCoalesceLeaf(self) -> None:
3036 entries = self.journaler.getAll(VDI.JRN_LEAF)
3037 for uuid, parentUuid in entries.items():
3038 childLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + uuid
3039 tmpChildLV = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
3040 self.TMP_RENAME_PREFIX + uuid
3041 parentLV1 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + parentUuid
3042 parentLV2 = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_RAW] + parentUuid
3043 parentPresent = (self.lvmCache.checkLV(parentLV1) or \
3044 self.lvmCache.checkLV(parentLV2))
3045 if parentPresent or self.lvmCache.checkLV(tmpChildLV):
3046 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3047 else:
3048 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3049 self.journaler.remove(VDI.JRN_LEAF, uuid)
3050 vdi = self.getVDI(uuid)
3051 if vdi:
3052 vdi.ensureUnpaused()
3054 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3055 Util.log("*** UNDO LEAF-COALESCE")
3056 parent = self.getVDI(parentUuid)
3057 if not parent:
3058 parent = self.getVDI(childUuid)
3059 if not parent:
3060 raise util.SMException("Neither %s nor %s found" % \
3061 (parentUuid, childUuid))
3062 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
3063 parent.rename(parentUuid)
3064 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
3066 child = self.getVDI(childUuid)
3067 if not child:
3068 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3069 if not child:
3070 raise util.SMException("Neither %s nor %s found" % \
3071 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
3072 Util.log("Renaming child back to %s" % childUuid)
3073 child.rename(childUuid)
3074 Util.log("Updating the VDI record")
3075 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
3076 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
3077 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
3079 # refcount (best effort - assume that it had succeeded if the
3080 # second rename succeeded; if not, this adjustment will be wrong,
3081 # leading to a non-deactivation of the LV)
3082 ns = lvhdutil.NS_PREFIX_LVM + self.uuid
3083 cCnt, cBcnt = RefCounter.check(child.uuid, ns)
3084 pCnt, pBcnt = RefCounter.check(parent.uuid, ns)
3085 pCnt = pCnt + cBcnt
3086 RefCounter.set(parent.uuid, pCnt, 0, ns)
3087 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid)
3089 parent.deflate()
3090 child.inflateFully()
3091 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid)
3092 if child.hidden:
3093 child._setHidden(False)
3094 if not parent.hidden:
3095 parent._setHidden(True)
3096 if not parent.lvReadonly:
3097 self.lvmCache.setReadonly(parent.fileName, True)
3098 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3099 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
3100 Util.log("*** leaf-coalesce undo successful")
3101 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
3102 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
3104 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3105 Util.log("*** FINISH LEAF-COALESCE")
3106 vdi = self.getVDI(childUuid)
3107 if not vdi:
3108 raise util.SMException("VDI %s not found" % childUuid)
3109 vdi.inflateFully()
3110 util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid)
3111 try:
3112 self.forgetVDI(parentUuid)
3113 except XenAPI.Failure:
3114 pass
3115 self._updateSlavesOnResize(vdi)
3116 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
3117 Util.log("*** finished leaf-coalesce successfully")
3119 def _checkSlaves(self, vdi):
3120 """Confirm with all slaves in the pool that 'vdi' is not in use. We
3121 try to check all slaves, including those that the Agent believes are
3122 offline, but ignore failures for offline hosts. This is to avoid cases
3123 where the Agent thinks a host is offline but the host is up."""
3124 args = {"vgName": self.vgName,
3125 "action1": "deactivateNoRefcount",
3126 "lvName1": vdi.fileName,
3127 "action2": "cleanupLockAndRefcount",
3128 "uuid2": vdi.uuid,
3129 "ns2": lvhdutil.NS_PREFIX_LVM + self.uuid}
3130 onlineHosts = self.xapi.getOnlineHosts()
3131 abortFlag = IPCFlag(self.uuid)
3132 for pbdRecord in self.xapi.getAttachedPBDs():
3133 hostRef = pbdRecord["host"]
3134 if hostRef == self.xapi._hostRef:
3135 continue
3136 if abortFlag.test(FLAG_TYPE_ABORT):
3137 raise AbortException("Aborting due to signal")
3138 Util.log("Checking with slave %s (path %s)" % (
3139 self.xapi.getRecordHost(hostRef)['hostname'], vdi.path))
3140 try:
3141 self.xapi.ensureInactive(hostRef, args)
3142 except XenAPI.Failure:
3143 if hostRef in onlineHosts:
3144 raise
3146 @override
3147 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
3148 slaves = util.get_slaves_attached_on(self.xapi.session, [child.uuid])
3149 if not slaves:
3150 Util.log("Update-on-leaf-undo: VDI %s not attached on any slave" % \
3151 child)
3152 return
3154 tmpName = lvhdutil.LV_PREFIX[vhdutil.VDI_TYPE_VHD] + \
3155 self.TMP_RENAME_PREFIX + child.uuid
3156 args = {"vgName": self.vgName,
3157 "action1": "deactivateNoRefcount",
3158 "lvName1": tmpName,
3159 "action2": "deactivateNoRefcount",
3160 "lvName2": child.fileName,
3161 "action3": "refresh",
3162 "lvName3": child.fileName,
3163 "action4": "refresh",
3164 "lvName4": parent.fileName}
3165 for slave in slaves:
3166 Util.log("Updating %s, %s, %s on slave %s" % \
3167 (tmpName, child.fileName, parent.fileName,
3168 self.xapi.getRecordHost(slave)['hostname']))
3169 text = self.xapi.session.xenapi.host.call_plugin( \
3170 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3171 Util.log("call-plugin returned: '%s'" % text)
3173 @override
3174 def _updateSlavesOnRename(self, vdi, oldNameLV, origParentUuid) -> None:
3175 slaves = util.get_slaves_attached_on(self.xapi.session, [vdi.uuid])
3176 if not slaves:
3177 Util.log("Update-on-rename: VDI %s not attached on any slave" % vdi)
3178 return
3180 args = {"vgName": self.vgName,
3181 "action1": "deactivateNoRefcount",
3182 "lvName1": oldNameLV,
3183 "action2": "refresh",
3184 "lvName2": vdi.fileName,
3185 "action3": "cleanupLockAndRefcount",
3186 "uuid3": origParentUuid,
3187 "ns3": lvhdutil.NS_PREFIX_LVM + self.uuid}
3188 for slave in slaves:
3189 Util.log("Updating %s to %s on slave %s" % \
3190 (oldNameLV, vdi.fileName,
3191 self.xapi.getRecordHost(slave)['hostname']))
3192 text = self.xapi.session.xenapi.host.call_plugin( \
3193 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3194 Util.log("call-plugin returned: '%s'" % text)
3196 @override
3197 def _updateSlavesOnResize(self, vdi) -> None:
3198 uuids = [x.uuid for x in vdi.getAllLeaves()]
3199 slaves = util.get_slaves_attached_on(self.xapi.session, uuids)
3200 if not slaves:
3201 util.SMlog("Update-on-resize: %s not attached on any slave" % vdi)
3202 return
3203 lvhdutil.lvRefreshOnSlaves(self.xapi.session, self.uuid, self.vgName,
3204 vdi.fileName, vdi.uuid, slaves)
3207class LinstorSR(SR):
3208 TYPE = SR.TYPE_LINSTOR
3210 def __init__(self, uuid, xapi, createLock, force):
3211 if not LINSTOR_AVAILABLE:
3212 raise util.SMException(
3213 'Can\'t load cleanup LinstorSR: LINSTOR libraries are missing'
3214 )
3216 SR.__init__(self, uuid, xapi, createLock, force)
3217 self.path = LinstorVolumeManager.DEV_ROOT_PATH
3218 self._reloadLinstor()
3220 @override
3221 def deleteVDI(self, vdi) -> None:
3222 self._checkSlaves(vdi)
3223 SR.deleteVDI(self, vdi)
3225 @override
3226 def getFreeSpace(self) -> int:
3227 return self._linstor.max_volume_size_allowed
3229 @override
3230 def scan(self, force=False) -> None:
3231 all_vdi_info = self._scan(force)
3232 for uuid, vdiInfo in all_vdi_info.items():
3233 # When vdiInfo is None, the VDI is RAW.
3234 vdi = self.getVDI(uuid)
3235 if not vdi:
3236 self.logFilter.logNewVDI(uuid)
3237 vdi = LinstorVDI(self, uuid, not vdiInfo)
3238 self.vdis[uuid] = vdi
3239 if vdiInfo:
3240 vdi.load(vdiInfo)
3241 self._removeStaleVDIs(all_vdi_info.keys())
3242 self._buildTree(force)
3243 self.logFilter.logState()
3244 self._handleInterruptedCoalesceLeaf()
3246 @override
3247 def pauseVDIs(self, vdiList) -> None:
3248 self._linstor.ensure_volume_list_is_not_locked(
3249 vdiList, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3250 )
3251 return super(LinstorSR, self).pauseVDIs(vdiList)
3253 def _reloadLinstor(self):
3254 session = self.xapi.session
3255 host_ref = util.get_this_host_ref(session)
3256 sr_ref = session.xenapi.SR.get_by_uuid(self.uuid)
3258 pbd = util.find_my_pbd(session, host_ref, sr_ref)
3259 if pbd is None:
3260 raise util.SMException('Failed to find PBD')
3262 dconf = session.xenapi.PBD.get_device_config(pbd)
3263 group_name = dconf['group-name']
3265 controller_uri = get_controller_uri()
3266 self.journaler = LinstorJournaler(
3267 controller_uri, group_name, logger=util.SMlog
3268 )
3270 self._linstor = LinstorVolumeManager(
3271 controller_uri,
3272 group_name,
3273 repair=True,
3274 logger=util.SMlog
3275 )
3276 self._vhdutil = LinstorVhdUtil(session, self._linstor)
3278 def _scan(self, force):
3279 for i in range(SR.SCAN_RETRY_ATTEMPTS):
3280 self._reloadLinstor()
3281 error = False
3282 try:
3283 all_vdi_info = self._load_vdi_info()
3284 for uuid, vdiInfo in all_vdi_info.items():
3285 if vdiInfo and vdiInfo.error:
3286 error = True
3287 break
3288 if not error:
3289 return all_vdi_info
3290 Util.log('Scan error, retrying ({})'.format(i))
3291 except Exception as e:
3292 Util.log('Scan exception, retrying ({}): {}'.format(i, e))
3293 Util.log(traceback.format_exc())
3295 if force:
3296 return all_vdi_info
3297 raise util.SMException('Scan error')
3299 def _load_vdi_info(self):
3300 all_vdi_info = {}
3302 # TODO: Ensure metadata contains the right info.
3304 all_volume_info = self._linstor.get_volumes_with_info()
3305 volumes_metadata = self._linstor.get_volumes_with_metadata()
3306 for vdi_uuid, volume_info in all_volume_info.items():
3307 try:
3308 volume_metadata = volumes_metadata[vdi_uuid]
3309 if not volume_info.name and not list(volume_metadata.items()):
3310 continue # Ignore it, probably deleted.
3312 if vdi_uuid.startswith('DELETED_'):
3313 # Assume it's really a RAW volume of a failed snap without VHD header/footer.
3314 # We must remove this VDI now without adding it in the VDI list.
3315 # Otherwise `Relinking` calls and other actions can be launched on it.
3316 # We don't want that...
3317 Util.log('Deleting bad VDI {}'.format(vdi_uuid))
3319 self.lock()
3320 try:
3321 self._linstor.destroy_volume(vdi_uuid)
3322 try:
3323 self.forgetVDI(vdi_uuid)
3324 except:
3325 pass
3326 except Exception as e:
3327 Util.log('Cannot delete bad VDI: {}'.format(e))
3328 finally:
3329 self.unlock()
3330 continue
3332 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
3333 volume_name = self._linstor.get_volume_name(vdi_uuid)
3334 if volume_name.startswith(LINSTOR_PERSISTENT_PREFIX):
3335 # Always RAW!
3336 info = None
3337 elif vdi_type == vhdutil.VDI_TYPE_VHD:
3338 info = self._vhdutil.get_vhd_info(vdi_uuid)
3339 else:
3340 # Ensure it's not a VHD...
3341 try:
3342 info = self._vhdutil.get_vhd_info(vdi_uuid)
3343 except:
3344 try:
3345 self._vhdutil.force_repair(
3346 self._linstor.get_device_path(vdi_uuid)
3347 )
3348 info = self._vhdutil.get_vhd_info(vdi_uuid)
3349 except:
3350 info = None
3352 except Exception as e:
3353 Util.log(
3354 ' [VDI {}: failed to load VDI info]: {}'
3355 .format(vdi_uuid, e)
3356 )
3357 info = vhdutil.VHDInfo(vdi_uuid)
3358 info.error = 1
3360 all_vdi_info[vdi_uuid] = info
3362 return all_vdi_info
3364 @override
3365 def _prepareCoalesceLeaf(self, vdi) -> None:
3366 vdi._activateChain()
3367 vdi.deflate()
3368 vdi._inflateParentForCoalesce()
3370 @override
3371 def _finishCoalesceLeaf(self, parent) -> None:
3372 if not parent.isSnapshot() or parent.isAttachedRW():
3373 parent.inflateFully()
3374 else:
3375 parent.deflate()
3377 @override
3378 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3379 return LinstorVhdUtil.compute_volume_size(parent.sizeVirt, parent.vdi_type) - parent.getDrbdSize()
3381 def _hasValidDevicePath(self, uuid):
3382 try:
3383 self._linstor.get_device_path(uuid)
3384 except Exception:
3385 # TODO: Maybe log exception.
3386 return False
3387 return True
3389 @override
3390 def _liveLeafCoalesce(self, vdi) -> bool:
3391 self.lock()
3392 try:
3393 self._linstor.ensure_volume_is_not_locked(
3394 vdi.uuid, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3395 )
3396 return super(LinstorSR, self)._liveLeafCoalesce(vdi)
3397 finally:
3398 self.unlock()
3400 @override
3401 def _handleInterruptedCoalesceLeaf(self) -> None:
3402 entries = self.journaler.get_all(VDI.JRN_LEAF)
3403 for uuid, parentUuid in entries.items():
3404 if self._hasValidDevicePath(parentUuid) or \
3405 self._hasValidDevicePath(self.TMP_RENAME_PREFIX + uuid):
3406 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3407 else:
3408 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3409 self.journaler.remove(VDI.JRN_LEAF, uuid)
3410 vdi = self.getVDI(uuid)
3411 if vdi:
3412 vdi.ensureUnpaused()
3414 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3415 Util.log('*** UNDO LEAF-COALESCE')
3416 parent = self.getVDI(parentUuid)
3417 if not parent:
3418 parent = self.getVDI(childUuid)
3419 if not parent:
3420 raise util.SMException(
3421 'Neither {} nor {} found'.format(parentUuid, childUuid)
3422 )
3423 Util.log(
3424 'Renaming parent back: {} -> {}'.format(childUuid, parentUuid)
3425 )
3426 parent.rename(parentUuid)
3428 child = self.getVDI(childUuid)
3429 if not child:
3430 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3431 if not child:
3432 raise util.SMException(
3433 'Neither {} nor {} found'.format(
3434 childUuid, self.TMP_RENAME_PREFIX + childUuid
3435 )
3436 )
3437 Util.log('Renaming child back to {}'.format(childUuid))
3438 child.rename(childUuid)
3439 Util.log('Updating the VDI record')
3440 child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
3441 child.setConfig(VDI.DB_VDI_TYPE, vhdutil.VDI_TYPE_VHD)
3443 # TODO: Maybe deflate here.
3445 if child.hidden:
3446 child._setHidden(False)
3447 if not parent.hidden:
3448 parent._setHidden(True)
3449 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3450 Util.log('*** leaf-coalesce undo successful')
3452 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3453 Util.log('*** FINISH LEAF-COALESCE')
3454 vdi = self.getVDI(childUuid)
3455 if not vdi:
3456 raise util.SMException('VDI {} not found'.format(childUuid))
3457 # TODO: Maybe inflate.
3458 try:
3459 self.forgetVDI(parentUuid)
3460 except XenAPI.Failure:
3461 pass
3462 self._updateSlavesOnResize(vdi)
3463 Util.log('*** finished leaf-coalesce successfully')
3465 def _checkSlaves(self, vdi):
3466 try:
3467 all_openers = self._linstor.get_volume_openers(vdi.uuid)
3468 for openers in all_openers.values():
3469 for opener in openers.values():
3470 if opener['process-name'] != 'tapdisk':
3471 raise util.SMException(
3472 'VDI {} is in use: {}'.format(vdi.uuid, all_openers)
3473 )
3474 except LinstorVolumeManagerError as e:
3475 if e.code != LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
3476 raise
3479################################################################################
3480#
3481# Helpers
3482#
3483def daemonize():
3484 pid = os.fork()
3485 if pid:
3486 os.waitpid(pid, 0)
3487 Util.log("New PID [%d]" % pid)
3488 return False
3489 os.chdir("/")
3490 os.setsid()
3491 pid = os.fork()
3492 if pid:
3493 Util.log("Will finish as PID [%d]" % pid)
3494 os._exit(0)
3495 for fd in [0, 1, 2]:
3496 try:
3497 os.close(fd)
3498 except OSError:
3499 pass
3500 # we need to fill those special fd numbers or pread won't work
3501 sys.stdin = open("/dev/null", 'r')
3502 sys.stderr = open("/dev/null", 'w')
3503 sys.stdout = open("/dev/null", 'w')
3504 # As we're a new process we need to clear the lock objects
3505 lock.Lock.clearAll()
3506 return True
3509def normalizeType(type):
3510 if type in LVHDSR.SUBTYPES:
3511 type = SR.TYPE_LVHD
3512 if type in ["lvm", "lvmoiscsi", "lvmohba", "lvmofcoe"]:
3513 # temporary while LVHD is symlinked as LVM
3514 type = SR.TYPE_LVHD
3515 if type in [
3516 "ext", "nfs", "ocfsoiscsi", "ocfsohba", "smb", "cephfs", "glusterfs",
3517 "moosefs", "xfs", "zfs", "largeblock"
3518 ]:
3519 type = SR.TYPE_FILE
3520 if type in ["linstor"]:
3521 type = SR.TYPE_LINSTOR
3522 if type not in SR.TYPES:
3523 raise util.SMException("Unsupported SR type: %s" % type)
3524 return type
3526GCPAUSE_DEFAULT_SLEEP = 5 * 60
3529def _gc_init_file(sr_uuid):
3530 return os.path.join(NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init')
3533def _create_init_file(sr_uuid):
3534 util.makedirs(os.path.join(NON_PERSISTENT_DIR, str(sr_uuid)))
3535 with open(os.path.join(
3536 NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init'), 'w+') as f:
3537 f.write('1')
3540def _gcLoopPause(sr, dryRun=False, immediate=False):
3541 if immediate:
3542 return
3544 # Check to see if the GCPAUSE_FISTPOINT is present. If so the fist
3545 # point will just return. Otherwise, fall back on an abortable sleep.
3547 if util.fistpoint.is_active(util.GCPAUSE_FISTPOINT):
3549 util.fistpoint.activate_custom_fn(util.GCPAUSE_FISTPOINT, 3549 ↛ exitline 3549 didn't jump to the function exit
3550 lambda *args: None)
3551 elif os.path.exists(_gc_init_file(sr.uuid)):
3552 def abortTest():
3553 return IPCFlag(sr.uuid).test(FLAG_TYPE_ABORT)
3555 # If time.sleep hangs we are in deep trouble, however for
3556 # completeness we set the timeout of the abort thread to
3557 # 110% of GCPAUSE_DEFAULT_SLEEP.
3558 Util.log("GC active, about to go quiet")
3559 Util.runAbortable(lambda: time.sleep(GCPAUSE_DEFAULT_SLEEP), 3559 ↛ exitline 3559 didn't run the lambda on line 3559
3560 None, sr.uuid, abortTest, VDI.POLL_INTERVAL,
3561 GCPAUSE_DEFAULT_SLEEP * 1.1)
3562 Util.log("GC active, quiet period ended")
3565def _gcLoop(sr, dryRun=False, immediate=False):
3566 if not lockGCActive.acquireNoblock(): 3566 ↛ 3567line 3566 didn't jump to line 3567, because the condition on line 3566 was never true
3567 Util.log("Another GC instance already active, exiting")
3568 return
3570 # Check we're still attached after acquiring locks
3571 if not sr.xapi.isPluggedHere():
3572 Util.log("SR no longer attached, exiting")
3573 return
3575 # Clean up Intellicache files
3576 sr.cleanupCache()
3578 # Track how many we do
3579 coalesced = 0
3580 task_status = "success"
3581 try:
3582 # Check if any work needs to be done
3583 if not sr.xapi.isPluggedHere(): 3583 ↛ 3584line 3583 didn't jump to line 3584, because the condition on line 3583 was never true
3584 Util.log("SR no longer attached, exiting")
3585 return
3586 sr.scanLocked()
3587 if not sr.hasWork():
3588 Util.log("No work, exiting")
3589 return
3590 sr.xapi.create_task(
3591 "Garbage Collection",
3592 "Garbage collection for SR %s" % sr.uuid)
3593 _gcLoopPause(sr, dryRun, immediate=immediate)
3594 while True:
3595 if not sr.xapi.isPluggedHere(): 3595 ↛ 3596line 3595 didn't jump to line 3596, because the condition on line 3595 was never true
3596 Util.log("SR no longer attached, exiting")
3597 break
3598 sr.scanLocked()
3599 if not sr.hasWork():
3600 Util.log("No work, exiting")
3601 break
3603 if not lockGCRunning.acquireNoblock(): 3603 ↛ 3604line 3603 didn't jump to line 3604, because the condition on line 3603 was never true
3604 Util.log("Unable to acquire GC running lock.")
3605 return
3606 try:
3607 if not sr.gcEnabled(): 3607 ↛ 3608line 3607 didn't jump to line 3608, because the condition on line 3607 was never true
3608 break
3610 sr.xapi.update_task_progress("done", coalesced)
3612 sr.cleanupCoalesceJournals()
3613 # Create the init file here in case startup is waiting on it
3614 _create_init_file(sr.uuid)
3615 sr.scanLocked()
3616 sr.updateBlockInfo()
3618 howmany = len(sr.findGarbage())
3619 if howmany > 0:
3620 Util.log("Found %d orphaned vdis" % howmany)
3621 sr.lock()
3622 try:
3623 sr.garbageCollect(dryRun)
3624 finally:
3625 sr.unlock()
3626 sr.xapi.srUpdate()
3628 candidate = sr.findCoalesceable()
3629 if candidate:
3630 util.fistpoint.activate(
3631 "LVHDRT_finding_a_suitable_pair", sr.uuid)
3632 sr.coalesce(candidate, dryRun)
3633 sr.xapi.srUpdate()
3634 coalesced += 1
3635 continue
3637 candidate = sr.findLeafCoalesceable()
3638 if candidate: 3638 ↛ 3645line 3638 didn't jump to line 3645, because the condition on line 3638 was never false
3639 sr.coalesceLeaf(candidate, dryRun)
3640 sr.xapi.srUpdate()
3641 coalesced += 1
3642 continue
3644 finally:
3645 lockGCRunning.release() 3645 ↛ 3650line 3645 didn't jump to line 3650, because the break on line 3608 wasn't executed
3646 except:
3647 task_status = "failure"
3648 raise
3649 finally:
3650 sr.xapi.set_task_status(task_status)
3651 Util.log("GC process exiting, no work left")
3652 _create_init_file(sr.uuid)
3653 lockGCActive.release()
3656def _xapi_enabled(session, hostref):
3657 host = session.xenapi.host.get_record(hostref)
3658 return host['enabled']
3661def _ensure_xapi_initialised(session):
3662 """
3663 Don't want to start GC until Xapi is fully initialised
3664 """
3665 local_session = None
3666 if session is None:
3667 local_session = util.get_localAPI_session()
3668 session = local_session
3670 try:
3671 hostref = session.xenapi.host.get_by_uuid(util.get_this_host())
3672 while not _xapi_enabled(session, hostref):
3673 util.SMlog("Xapi not ready, GC waiting")
3674 time.sleep(15)
3675 finally:
3676 if local_session is not None:
3677 local_session.xenapi.session.logout()
3679def _gc(session, srUuid, dryRun=False, immediate=False):
3680 init(srUuid)
3681 _ensure_xapi_initialised(session)
3682 sr = SR.getInstance(srUuid, session)
3683 if not sr.gcEnabled(False): 3683 ↛ 3684line 3683 didn't jump to line 3684, because the condition on line 3683 was never true
3684 return
3686 try:
3687 _gcLoop(sr, dryRun, immediate=immediate)
3688 finally:
3689 sr.cleanup()
3690 sr.logFilter.logState()
3691 del sr.xapi
3694def _abort(srUuid, soft=False):
3695 """Aborts an GC/coalesce.
3697 srUuid: the UUID of the SR whose GC/coalesce must be aborted
3698 soft: If set to True and there is a pending abort signal, the function
3699 doesn't do anything. If set to False, a new abort signal is issued.
3701 returns: If soft is set to False, we return True holding lockGCActive. If
3702 soft is set to False and an abort signal is pending, we return False
3703 without holding lockGCActive. An exception is raised in case of error."""
3704 Util.log("=== SR %s: abort ===" % (srUuid))
3705 init(srUuid)
3706 if not lockGCActive.acquireNoblock():
3707 gotLock = False
3708 Util.log("Aborting currently-running instance (SR %s)" % srUuid)
3709 abortFlag = IPCFlag(srUuid)
3710 if not abortFlag.set(FLAG_TYPE_ABORT, soft):
3711 return False
3712 for i in range(SR.LOCK_RETRY_ATTEMPTS):
3713 gotLock = lockGCActive.acquireNoblock()
3714 if gotLock:
3715 break
3716 time.sleep(SR.LOCK_RETRY_INTERVAL)
3717 abortFlag.clear(FLAG_TYPE_ABORT)
3718 if not gotLock:
3719 raise util.CommandException(code=errno.ETIMEDOUT,
3720 reason="SR %s: error aborting existing process" % srUuid)
3721 return True
3724def init(srUuid):
3725 global lockGCRunning
3726 if not lockGCRunning: 3726 ↛ 3727line 3726 didn't jump to line 3727, because the condition on line 3726 was never true
3727 lockGCRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, srUuid)
3728 global lockGCActive
3729 if not lockGCActive: 3729 ↛ 3730line 3729 didn't jump to line 3730, because the condition on line 3729 was never true
3730 lockGCActive = LockActive(srUuid)
3733class LockActive:
3734 """
3735 Wraps the use of LOCK_TYPE_GC_ACTIVE such that the lock cannot be acquired
3736 if another process holds the SR lock.
3737 """
3738 def __init__(self, srUuid):
3739 self._lock = lock.Lock(LOCK_TYPE_GC_ACTIVE, srUuid)
3740 self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, srUuid)
3742 def acquireNoblock(self):
3743 self._srLock.acquire()
3745 try:
3746 return self._lock.acquireNoblock()
3747 finally:
3748 self._srLock.release()
3750 def release(self):
3751 self._lock.release()
3754def usage():
3755 output = """Garbage collect and/or coalesce VHDs in a VHD-based SR
3757Parameters:
3758 -u --uuid UUID SR UUID
3759 and one of:
3760 -g --gc garbage collect, coalesce, and repeat while there is work
3761 -G --gc_force garbage collect once, aborting any current operations
3762 -c --cache-clean <max_age> clean up IntelliCache cache files older than
3763 max_age hours
3764 -a --abort abort any currently running operation (GC or coalesce)
3765 -q --query query the current state (GC'ing, coalescing or not running)
3766 -x --disable disable GC/coalesce (will be in effect until you exit)
3767 -t --debug see Debug below
3769Options:
3770 -b --background run in background (return immediately) (valid for -g only)
3771 -f --force continue in the presence of VHDs with errors (when doing
3772 GC, this might cause removal of any such VHDs) (only valid
3773 for -G) (DANGEROUS)
3775Debug:
3776 The --debug parameter enables manipulation of LVHD VDIs for debugging
3777 purposes. ** NEVER USE IT ON A LIVE VM **
3778 The following parameters are required:
3779 -t --debug <cmd> <cmd> is one of "activate", "deactivate", "inflate",
3780 "deflate".
3781 -v --vdi_uuid VDI UUID
3782 """
3783 #-d --dry-run don't actually perform any SR-modifying operations
3784 print(output)
3785 Util.log("(Invalid usage)")
3786 sys.exit(1)
3789##############################################################################
3790#
3791# API
3792#
3793def abort(srUuid, soft=False):
3794 """Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair.
3795 """
3796 if _abort(srUuid, soft):
3797 Util.log("abort: releasing the process lock")
3798 lockGCActive.release()
3799 return True
3800 else:
3801 return False
3804def gc(session, srUuid, inBackground, dryRun=False):
3805 """Garbage collect all deleted VDIs in SR "srUuid". Fork & return
3806 immediately if inBackground=True.
3808 The following algorithm is used:
3809 1. If we are already GC'ing in this SR, return
3810 2. If we are already coalescing a VDI pair:
3811 a. Scan the SR and determine if the VDI pair is GC'able
3812 b. If the pair is not GC'able, return
3813 c. If the pair is GC'able, abort coalesce
3814 3. Scan the SR
3815 4. If there is nothing to collect, nor to coalesce, return
3816 5. If there is something to collect, GC all, then goto 3
3817 6. If there is something to coalesce, coalesce one pair, then goto 3
3818 """
3819 Util.log("=== SR %s: gc ===" % srUuid)
3820 if inBackground:
3821 if daemonize(): 3821 ↛ exitline 3821 didn't return from function 'gc', because the condition on line 3821 was never false
3822 # we are now running in the background. Catch & log any errors
3823 # because there is no other way to propagate them back at this
3824 # point
3826 try:
3827 _gc(None, srUuid, dryRun)
3828 except AbortException:
3829 Util.log("Aborted")
3830 except Exception:
3831 Util.logException("gc")
3832 Util.log("* * * * * SR %s: ERROR\n" % srUuid)
3833 os._exit(0)
3834 else:
3835 _gc(session, srUuid, dryRun, immediate=True)
3838def start_gc(session, sr_uuid):
3839 """
3840 This function is used to try to start a backgrounded GC session by forking
3841 the current process. If using the systemd version, call start_gc_service() instead.
3842 """
3843 # don't bother if an instance already running (this is just an
3844 # optimization to reduce the overhead of forking a new process if we
3845 # don't have to, but the process will check the lock anyways)
3846 lockRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, sr_uuid)
3847 if not lockRunning.acquireNoblock():
3848 if should_preempt(session, sr_uuid):
3849 util.SMlog("Aborting currently-running coalesce of garbage VDI")
3850 try:
3851 if not abort(sr_uuid, soft=True):
3852 util.SMlog("The GC has already been scheduled to re-start")
3853 except util.CommandException as e:
3854 if e.code != errno.ETIMEDOUT:
3855 raise
3856 util.SMlog('failed to abort the GC')
3857 else:
3858 util.SMlog("A GC instance already running, not kicking")
3859 return
3860 else:
3861 lockRunning.release()
3863 util.SMlog(f"Starting GC file is {__file__}")
3864 subprocess.run([__file__, '-b', '-u', sr_uuid, '-g'],
3865 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3867def start_gc_service(sr_uuid, wait=False):
3868 """
3869 This starts the templated systemd service which runs GC on the given SR UUID.
3870 If the service was already started, this is a no-op.
3872 Because the service is a one-shot with RemainAfterExit=no, when called with
3873 wait=True this will run the service synchronously and will not return until the
3874 run has finished. This is used to force a run of the GC instead of just kicking it
3875 in the background.
3876 """
3877 sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
3878 util.SMlog(f"Kicking SMGC@{sr_uuid}...")
3879 cmd=[ "/usr/bin/systemctl", "--quiet" ]
3880 if not wait: 3880 ↛ 3882line 3880 didn't jump to line 3882, because the condition on line 3880 was never false
3881 cmd.append("--no-block")
3882 cmd += ["start", f"SMGC@{sr_uuid_esc}"]
3883 subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3886def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False):
3887 """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure
3888 the SR lock is held.
3889 The following algorithm is used:
3890 1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce
3891 2. Scan the SR
3892 3. GC
3893 4. return
3894 """
3895 Util.log("=== SR %s: gc_force ===" % srUuid)
3896 init(srUuid)
3897 sr = SR.getInstance(srUuid, session, lockSR, True)
3898 if not lockGCActive.acquireNoblock():
3899 abort(srUuid)
3900 else:
3901 Util.log("Nothing was running, clear to proceed")
3903 if force:
3904 Util.log("FORCED: will continue even if there are VHD errors")
3905 sr.scanLocked(force)
3906 sr.cleanupCoalesceJournals()
3908 try:
3909 sr.cleanupCache()
3910 sr.garbageCollect(dryRun)
3911 finally:
3912 sr.cleanup()
3913 sr.logFilter.logState()
3914 lockGCActive.release()
3917def get_state(srUuid):
3918 """Return whether GC/coalesce is currently running or not. This asks systemd for
3919 the state of the templated SMGC service and will return True if it is "activating"
3920 or "running" (for completeness, as in practice it will never achieve the latter state)
3921 """
3922 sr_uuid_esc = srUuid.replace("-", "\\x2d")
3923 cmd=[ "/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"]
3924 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
3925 state = result.stdout.decode('utf-8').rstrip()
3926 if state == "activating" or state == "running":
3927 return True
3928 return False
3931def should_preempt(session, srUuid):
3932 sr = SR.getInstance(srUuid, session)
3933 entries = sr.journaler.getAll(VDI.JRN_COALESCE)
3934 if len(entries) == 0:
3935 return False
3936 elif len(entries) > 1:
3937 raise util.SMException("More than one coalesce entry: " + str(entries))
3938 sr.scanLocked()
3939 coalescedUuid = entries.popitem()[0]
3940 garbage = sr.findGarbage()
3941 for vdi in garbage:
3942 if vdi.uuid == coalescedUuid:
3943 return True
3944 return False
3947def get_coalesceable_leaves(session, srUuid, vdiUuids):
3948 coalesceable = []
3949 sr = SR.getInstance(srUuid, session)
3950 sr.scanLocked()
3951 for uuid in vdiUuids:
3952 vdi = sr.getVDI(uuid)
3953 if not vdi:
3954 raise util.SMException("VDI %s not found" % uuid)
3955 if vdi.isLeafCoalesceable():
3956 coalesceable.append(uuid)
3957 return coalesceable
3960def cache_cleanup(session, srUuid, maxAge):
3961 sr = SR.getInstance(srUuid, session)
3962 return sr.cleanupCache(maxAge)
3965def debug(sr_uuid, cmd, vdi_uuid):
3966 Util.log("Debug command: %s" % cmd)
3967 sr = SR.getInstance(sr_uuid, None)
3968 if not isinstance(sr, LVHDSR):
3969 print("Error: not an LVHD SR")
3970 return
3971 sr.scanLocked()
3972 vdi = sr.getVDI(vdi_uuid)
3973 if not vdi:
3974 print("Error: VDI %s not found")
3975 return
3976 print("Running %s on SR %s" % (cmd, sr))
3977 print("VDI before: %s" % vdi)
3978 if cmd == "activate":
3979 vdi._activate()
3980 print("VDI file: %s" % vdi.path)
3981 if cmd == "deactivate":
3982 ns = lvhdutil.NS_PREFIX_LVM + sr.uuid
3983 sr.lvmCache.deactivate(ns, vdi.uuid, vdi.fileName, False)
3984 if cmd == "inflate":
3985 vdi.inflateFully()
3986 sr.cleanup()
3987 if cmd == "deflate":
3988 vdi.deflate()
3989 sr.cleanup()
3990 sr.scanLocked()
3991 print("VDI after: %s" % vdi)
3994def abort_optional_reenable(uuid):
3995 print("Disabling GC/coalesce for %s" % uuid)
3996 ret = _abort(uuid)
3997 input("Press enter to re-enable...")
3998 print("GC/coalesce re-enabled")
3999 lockGCRunning.release()
4000 if ret:
4001 lockGCActive.release()
4004##############################################################################
4005#
4006# CLI
4007#
4008def main():
4009 action = ""
4010 uuid = ""
4011 background = False
4012 force = False
4013 dryRun = False
4014 debug_cmd = ""
4015 vdi_uuid = ""
4016 shortArgs = "gGc:aqxu:bfdt:v:"
4017 longArgs = ["gc", "gc_force", "clean_cache", "abort", "query", "disable",
4018 "uuid=", "background", "force", "dry-run", "debug=", "vdi_uuid="]
4020 try:
4021 opts, args = getopt.getopt(sys.argv[1:], shortArgs, longArgs)
4022 except getopt.GetoptError:
4023 usage()
4024 for o, a in opts:
4025 if o in ("-g", "--gc"):
4026 action = "gc"
4027 if o in ("-G", "--gc_force"):
4028 action = "gc_force"
4029 if o in ("-c", "--clean_cache"):
4030 action = "clean_cache"
4031 maxAge = int(a)
4032 if o in ("-a", "--abort"):
4033 action = "abort"
4034 if o in ("-q", "--query"):
4035 action = "query"
4036 if o in ("-x", "--disable"):
4037 action = "disable"
4038 if o in ("-u", "--uuid"):
4039 uuid = a
4040 if o in ("-b", "--background"):
4041 background = True
4042 if o in ("-f", "--force"):
4043 force = True
4044 if o in ("-d", "--dry-run"):
4045 Util.log("Dry run mode")
4046 dryRun = True
4047 if o in ("-t", "--debug"):
4048 action = "debug"
4049 debug_cmd = a
4050 if o in ("-v", "--vdi_uuid"):
4051 vdi_uuid = a
4053 if not action or not uuid:
4054 usage()
4055 if action == "debug" and not (debug_cmd and vdi_uuid) or \
4056 action != "debug" and (debug_cmd or vdi_uuid):
4057 usage()
4059 if action != "query" and action != "debug":
4060 print("All output goes to log")
4062 if action == "gc":
4063 gc(None, uuid, background, dryRun)
4064 elif action == "gc_force":
4065 gc_force(None, uuid, force, dryRun, True)
4066 elif action == "clean_cache":
4067 cache_cleanup(None, uuid, maxAge)
4068 elif action == "abort":
4069 abort(uuid)
4070 elif action == "query":
4071 print("Currently running: %s" % get_state(uuid))
4072 elif action == "disable":
4073 abort_optional_reenable(uuid)
4074 elif action == "debug":
4075 debug(uuid, debug_cmd, vdi_uuid)
4078if __name__ == '__main__': 4078 ↛ 4079line 4078 didn't jump to line 4079, because the condition on line 4078 was never true
4079 main()