Coverage for drivers/cleanup.py : 34%
Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1#!/usr/bin/python3
2#
3# Copyright (C) Citrix Systems Inc.
4#
5# This program is free software; you can redistribute it and/or modify
6# it under the terms of the GNU Lesser General Public License as published
7# by the Free Software Foundation; version 2.1 only.
8#
9# This program is distributed in the hope that it will be useful,
10# but WITHOUT ANY WARRANTY; without even the implied warranty of
11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12# GNU Lesser General Public License for more details.
13#
14# You should have received a copy of the GNU Lesser General Public License
15# along with this program; if not, write to the Free Software Foundation, Inc.,
16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17#
18# Script to coalesce and garbage collect COW-based SR's in the background
19#
21from sm_typing import Any, Dict, Optional, List, 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
36from pathlib import Path
38import XenAPI # pylint: disable=import-error
39import util
40import lvutil
41import lvmcache
42import journaler
43import fjournaler
44import lock
45import blktap2
46import xs_errors
47from refcounter import RefCounter
48from ipc import IPCFlag
49from lvmanager import LVActivator
50from srmetadata import LVMMetadataHandler, VDI_TYPE_TAG
51from functools import reduce
52from time import monotonic as _time
54from constants import NS_PREFIX_LVM, VG_LOCATION, VG_PREFIX
55from cowutil import CowImageInfo, CowUtil, getCowUtil
56from lvmcowutil import LV_PREFIX, LvmCowUtil
57from vditype import VdiType, VdiTypeExtension, VDI_COW_TYPES, VDI_TYPE_TO_EXTENSION
59try:
60 from linstorcowutil import LinstorCowUtil, MultiLinstorCowUtil
61 from linstorjournaler import LinstorJournaler
62 from linstorvolumemanager import get_controller_uri
63 from linstorvolumemanager import LinstorVolumeManager, LinstorVolumeManagerError, LinstorVolumeOpeners
64 from linstorvolumemanager import PERSISTENT_PREFIX as LINSTOR_PERSISTENT_PREFIX
66 LINSTOR_AVAILABLE = True
67except ImportError:
68 LINSTOR_AVAILABLE = False
70# Disable automatic leaf-coalescing. Online leaf-coalesce is currently not
71# possible due to lvhd_stop_using_() not working correctly. However, we leave
72# this option available through the explicit LEAFCLSC_FORCE flag in the VDI
73# record for use by the offline tool (which makes the operation safe by pausing
74# the VM first)
75AUTO_ONLINE_LEAF_COALESCE_ENABLED = True
77FLAG_TYPE_ABORT = "abort" # flag to request aborting of GC/coalesce
79# process "lock", used simply as an indicator that a process already exists
80# that is doing GC/coalesce on this SR (such a process holds the lock, and we
81# check for the fact by trying the lock).
82lockGCRunning = None
84# process "lock" to indicate that the GC process has been activated but may not
85# yet be running, stops a second process from being started.
86LOCK_TYPE_GC_ACTIVE = "gc_active"
87lockGCActive = None
89# Default coalesce error rate limit, in messages per minute. A zero value
90# disables throttling, and a negative value disables error reporting.
91DEFAULT_COALESCE_ERR_RATE = 1.0 / 60
93COALESCE_LAST_ERR_TAG = 'last-coalesce-error'
94COALESCE_ERR_RATE_TAG = 'coalesce-error-rate'
95VAR_RUN = "/var/run/"
96SPEED_LOG_ROOT = VAR_RUN + "{uuid}.speed_log"
98N_RUNNING_AVERAGE = 10
100NON_PERSISTENT_DIR = '/run/nonpersistent/sm'
102# Signal Handler
103SIGTERM = False
106class AbortException(util.SMException):
107 pass
109class CancelException(util.SMException):
110 pass
112def receiveSignal(signalNumber, frame):
113 global SIGTERM
115 util.SMlog("GC: recieved SIGTERM")
116 SIGTERM = True
117 return
120################################################################################
121#
122# Util
123#
124class Util:
125 RET_RC = 1
126 RET_STDOUT = 2
127 RET_STDERR = 4
129 UUID_LEN = 36
131 PREFIX = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024}
133 @staticmethod
134 def log(text) -> None:
135 util.SMlog(text, ident="SMGC")
137 @staticmethod
138 def logException(tag):
139 info = sys.exc_info()
140 if info[0] == SystemExit: 140 ↛ 142line 140 didn't jump to line 142, because the condition on line 140 was never true
141 # this should not be happening when catching "Exception", but it is
142 sys.exit(0)
143 tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2]))
144 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
145 Util.log(" ***********************")
146 Util.log(" * E X C E P T I O N *")
147 Util.log(" ***********************")
148 Util.log("%s: EXCEPTION %s, %s" % (tag, info[0], info[1]))
149 Util.log(tb)
150 Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
152 @staticmethod
153 def doexec(args, expectedRC, inputtext=None, ret=None, log=True):
154 "Execute a subprocess, then return its return code, stdout, stderr"
155 proc = subprocess.Popen(args,
156 stdin=subprocess.PIPE, \
157 stdout=subprocess.PIPE, \
158 stderr=subprocess.PIPE, \
159 shell=True, \
160 close_fds=True)
161 (stdout, stderr) = proc.communicate(inputtext)
162 stdout = str(stdout)
163 stderr = str(stderr)
164 rc = proc.returncode
165 if log:
166 Util.log("`%s`: %s" % (args, rc))
167 if type(expectedRC) != type([]):
168 expectedRC = [expectedRC]
169 if not rc in expectedRC:
170 reason = stderr.strip()
171 if stdout.strip():
172 reason = "%s (stdout: %s)" % (reason, stdout.strip())
173 Util.log("Failed: %s" % reason)
174 raise util.CommandException(rc, args, reason)
176 if ret == Util.RET_RC:
177 return rc
178 if ret == Util.RET_STDERR:
179 return stderr
180 return stdout
182 @staticmethod
183 def runAbortable(func, ret, ns, abortTest, pollInterval, timeOut, prefSig=signal.SIGKILL):
184 """execute func in a separate thread and kill it if abortTest signals
185 so"""
186 abortSignaled = abortTest() # check now before we clear resultFlag
187 resultFlag = IPCFlag(ns)
188 resultFlag.clearAll()
189 pid = os.fork()
190 if pid:
191 startTime = _time()
192 try:
193 while True:
194 if resultFlag.test("success"):
195 Util.log(" Child process completed successfully")
196 resultFlag.clear("success")
197 return
198 if resultFlag.test("failure"):
199 resultFlag.clear("failure")
200 raise util.SMException("Child process exited with error")
201 if abortTest() or abortSignaled or SIGTERM:
202 os.killpg(pid, prefSig)
203 raise AbortException("Aborting due to signal")
204 if timeOut and _time() - startTime > timeOut:
205 os.killpg(pid, prefSig)
206 resultFlag.clearAll()
207 raise util.SMException("Timed out")
208 time.sleep(pollInterval)
209 finally:
210 wait_pid = 0
211 rc = -1
212 count = 0
213 while wait_pid == 0 and count < 10:
214 wait_pid, rc = os.waitpid(pid, os.WNOHANG)
215 if wait_pid == 0:
216 time.sleep(2)
217 count += 1
219 if wait_pid == 0:
220 Util.log("runAbortable: wait for process completion timed out")
221 else:
222 os.setpgrp()
223 try:
224 if func() == ret:
225 resultFlag.set("success")
226 else:
227 resultFlag.set("failure")
228 except Exception as e:
229 Util.log("Child process failed with : (%s)" % e)
230 resultFlag.set("failure")
231 Util.logException("This exception has occured")
232 os._exit(0)
234 @staticmethod
235 def num2str(number):
236 for prefix in ("G", "M", "K"): 236 ↛ 239line 236 didn't jump to line 239, because the loop on line 236 didn't complete
237 if number >= Util.PREFIX[prefix]:
238 return "%.3f%s" % (float(number) / Util.PREFIX[prefix], prefix)
239 return "%s" % number
241 @staticmethod
242 def numBits(val):
243 count = 0
244 while val:
245 count += val & 1
246 val = val >> 1
247 return count
249 @staticmethod
250 def countBits(bitmap1, bitmap2):
251 """return bit count in the bitmap produced by ORing the two bitmaps"""
252 len1 = len(bitmap1)
253 len2 = len(bitmap2)
254 lenLong = len1
255 lenShort = len2
256 bitmapLong = bitmap1
257 if len2 > len1:
258 lenLong = len2
259 lenShort = len1
260 bitmapLong = bitmap2
262 count = 0
263 for i in range(lenShort):
264 val = bitmap1[i] | bitmap2[i]
265 count += Util.numBits(val)
267 for i in range(i + 1, lenLong):
268 val = bitmapLong[i]
269 count += Util.numBits(val)
270 return count
272 @staticmethod
273 def getThisScript():
274 thisScript = util.get_real_path(__file__)
275 if thisScript.endswith(".pyc"):
276 thisScript = thisScript[:-1]
277 return thisScript
280################################################################################
281#
282# XAPI
283#
284class XAPI:
285 USER = "root"
286 PLUGIN_ON_SLAVE = "on-slave"
288 CONFIG_SM = 0
289 CONFIG_OTHER = 1
290 CONFIG_ON_BOOT = 2
291 CONFIG_ALLOW_CACHING = 3
293 CONFIG_NAME = {
294 CONFIG_SM: "sm-config",
295 CONFIG_OTHER: "other-config",
296 CONFIG_ON_BOOT: "on-boot",
297 CONFIG_ALLOW_CACHING: "allow_caching"
298 }
300 class LookupError(util.SMException):
301 pass
303 def __init__(self, session, srUuid):
304 self.session = session
305 self._api_session = None
306 if self.session is None:
307 self._api_session = util.ApiSession("SMGC")
308 self.session = self._api_session.session
309 self._srRef = self.session.xenapi.SR.get_by_uuid(srUuid)
310 self.srRecord = self.session.xenapi.SR.get_record(self._srRef)
311 self.hostUuid = util.get_this_host()
312 self._hostRef = self.session.xenapi.host.get_by_uuid(self.hostUuid)
313 self.task = None
314 self.task_progress = {"coalescable": 0, "done": 0}
316 def __del__(self):
317 if self._api_session:
318 self._api_session.logout()
320 @property
321 def srRef(self):
322 return self._srRef
324 def isPluggedHere(self):
325 pbds = self.getAttachedPBDs()
326 for pbdRec in pbds:
327 if pbdRec["host"] == self._hostRef:
328 return True
329 return False
331 def poolOK(self):
332 host_recs = self.session.xenapi.host.get_all_records()
333 for host_ref, host_rec in host_recs.items():
334 if not host_rec["enabled"]:
335 Util.log("Host %s not enabled" % host_rec["uuid"])
336 return False
337 return True
339 def isMaster(self):
340 if self.srRecord["shared"]:
341 pool = list(self.session.xenapi.pool.get_all_records().values())[0]
342 return pool["master"] == self._hostRef
343 else:
344 pbds = self.getAttachedPBDs()
345 if len(pbds) < 1:
346 raise util.SMException("Local SR not attached")
347 elif len(pbds) > 1:
348 raise util.SMException("Local SR multiply attached")
349 return pbds[0]["host"] == self._hostRef
351 def getAttachedPBDs(self):
352 """Return PBD records for all PBDs of this SR that are currently
353 attached"""
354 attachedPBDs = []
355 pbds = self.session.xenapi.PBD.get_all_records()
356 for pbdRec in pbds.values():
357 if pbdRec["SR"] == self._srRef and pbdRec["currently_attached"]:
358 attachedPBDs.append(pbdRec)
359 return attachedPBDs
361 def getOnlineHosts(self):
362 return util.get_online_hosts(self.session)
364 def ensureInactive(self, hostRef, args):
365 text = self.session.xenapi.host.call_plugin( \
366 hostRef, self.PLUGIN_ON_SLAVE, "multi", args)
367 Util.log("call-plugin returned: '%s'" % text)
369 def getRecordHost(self, hostRef):
370 return self.session.xenapi.host.get_record(hostRef)
372 def _getRefVDI(self, uuid):
373 return self.session.xenapi.VDI.get_by_uuid(uuid)
375 def getRefVDI(self, vdi):
376 return self._getRefVDI(vdi.uuid)
378 def getRecordVDI(self, uuid):
379 try:
380 ref = self._getRefVDI(uuid)
381 return self.session.xenapi.VDI.get_record(ref)
382 except XenAPI.Failure:
383 return None
385 def singleSnapshotVDI(self, vdi):
386 return self.session.xenapi.VDI.snapshot(vdi.getRef(),
387 {"type": "internal"})
389 def forgetVDI(self, srUuid, vdiUuid):
390 """Forget the VDI, but handle the case where the VDI has already been
391 forgotten (i.e. ignore errors)"""
392 try:
393 vdiRef = self.session.xenapi.VDI.get_by_uuid(vdiUuid)
394 self.session.xenapi.VDI.forget(vdiRef)
395 except XenAPI.Failure:
396 pass
398 def getConfigVDI(self, vdi, key):
399 kind = vdi.CONFIG_TYPE[key]
400 if kind == self.CONFIG_SM:
401 cfg = self.session.xenapi.VDI.get_sm_config(vdi.getRef())
402 elif kind == self.CONFIG_OTHER:
403 cfg = self.session.xenapi.VDI.get_other_config(vdi.getRef())
404 elif kind == self.CONFIG_ON_BOOT:
405 cfg = self.session.xenapi.VDI.get_on_boot(vdi.getRef())
406 elif kind == self.CONFIG_ALLOW_CACHING:
407 cfg = self.session.xenapi.VDI.get_allow_caching(vdi.getRef())
408 else:
409 assert(False)
410 Util.log("Got %s for %s: %s" % (self.CONFIG_NAME[kind], vdi, repr(cfg)))
411 return cfg
413 def removeFromConfigVDI(self, vdi, key):
414 kind = vdi.CONFIG_TYPE[key]
415 if kind == self.CONFIG_SM:
416 self.session.xenapi.VDI.remove_from_sm_config(vdi.getRef(), key)
417 elif kind == self.CONFIG_OTHER:
418 self.session.xenapi.VDI.remove_from_other_config(vdi.getRef(), key)
419 else:
420 assert(False)
422 def addToConfigVDI(self, vdi, key, val):
423 kind = vdi.CONFIG_TYPE[key]
424 if kind == self.CONFIG_SM:
425 self.session.xenapi.VDI.add_to_sm_config(vdi.getRef(), key, val)
426 elif kind == self.CONFIG_OTHER:
427 self.session.xenapi.VDI.add_to_other_config(vdi.getRef(), key, val)
428 else:
429 assert(False)
431 def isSnapshot(self, vdi):
432 return self.session.xenapi.VDI.get_is_a_snapshot(vdi.getRef())
434 def markCacheSRsDirty(self):
435 sr_refs = self.session.xenapi.SR.get_all_records_where( \
436 'field "local_cache_enabled" = "true"')
437 for sr_ref in sr_refs:
438 Util.log("Marking SR %s dirty" % sr_ref)
439 util.set_dirty(self.session, sr_ref)
441 def srUpdate(self):
442 Util.log("Starting asynch srUpdate for SR %s" % self.srRecord["uuid"])
443 abortFlag = IPCFlag(self.srRecord["uuid"])
444 task = self.session.xenapi.Async.SR.update(self._srRef)
445 cancelTask = True
446 try:
447 for i in range(60):
448 status = self.session.xenapi.task.get_status(task)
449 if not status == "pending":
450 Util.log("SR.update_asynch status changed to [%s]" % status)
451 cancelTask = False
452 return
453 if abortFlag.test(FLAG_TYPE_ABORT):
454 Util.log("Abort signalled during srUpdate, cancelling task...")
455 try:
456 self.session.xenapi.task.cancel(task)
457 cancelTask = False
458 Util.log("Task cancelled")
459 except:
460 pass
461 return
462 time.sleep(1)
463 finally:
464 if cancelTask:
465 self.session.xenapi.task.cancel(task)
466 self.session.xenapi.task.destroy(task)
467 Util.log("Asynch srUpdate still running, but timeout exceeded.")
469 def update_task(self):
470 self.session.xenapi.task.set_other_config(
471 self.task,
472 {
473 "applies_to": self._srRef
474 })
475 total = self.task_progress['coalescable'] + self.task_progress['done']
476 if (total > 0):
477 self.session.xenapi.task.set_progress(
478 self.task, float(self.task_progress['done']) / total)
480 def create_task(self, label, description):
481 self.task = self.session.xenapi.task.create(label, description)
482 self.update_task()
484 def update_task_progress(self, key, value):
485 self.task_progress[key] = value
486 if self.task:
487 self.update_task()
489 def set_task_status(self, status):
490 if self.task:
491 self.session.xenapi.task.set_status(self.task, status)
494################################################################################
495#
496# VDI
497#
498class VDI(object):
499 """Object representing a VDI of a COW-based SR"""
501 POLL_INTERVAL = 1
502 POLL_TIMEOUT = 30
503 DEVICE_MAJOR = 202
505 # config keys & values
506 DB_VDI_PARENT = "vhd-parent"
507 DB_VDI_TYPE = "vdi_type"
508 DB_VDI_BLOCKS = "vhd-blocks"
509 DB_VDI_PAUSED = "paused"
510 DB_VDI_RELINKING = "relinking"
511 DB_VDI_ACTIVATING = "activating"
512 DB_GC = "gc"
513 DB_COALESCE = "coalesce"
514 DB_LEAFCLSC = "leaf-coalesce" # config key
515 DB_GC_NO_SPACE = "gc_no_space"
516 LEAFCLSC_DISABLED = "false" # set by user; means do not leaf-coalesce
517 LEAFCLSC_FORCE = "force" # set by user; means skip snap-coalesce
518 LEAFCLSC_OFFLINE = "offline" # set here for informational purposes: means
519 # no space to snap-coalesce or unable to keep
520 # up with VDI. This is not used by the SM, it
521 # might be used by external components.
522 DB_ONBOOT = "on-boot"
523 ONBOOT_RESET = "reset"
524 DB_ALLOW_CACHING = "allow_caching"
526 CONFIG_TYPE = {
527 DB_VDI_PARENT: XAPI.CONFIG_SM,
528 DB_VDI_TYPE: XAPI.CONFIG_SM,
529 DB_VDI_BLOCKS: XAPI.CONFIG_SM,
530 DB_VDI_PAUSED: XAPI.CONFIG_SM,
531 DB_VDI_RELINKING: XAPI.CONFIG_SM,
532 DB_VDI_ACTIVATING: XAPI.CONFIG_SM,
533 DB_GC: XAPI.CONFIG_OTHER,
534 DB_COALESCE: XAPI.CONFIG_OTHER,
535 DB_LEAFCLSC: XAPI.CONFIG_OTHER,
536 DB_ONBOOT: XAPI.CONFIG_ON_BOOT,
537 DB_ALLOW_CACHING: XAPI.CONFIG_ALLOW_CACHING,
538 DB_GC_NO_SPACE: XAPI.CONFIG_SM
539 }
541 LIVE_LEAF_COALESCE_MAX_SIZE = 20 * 1024 * 1024 # bytes
542 LIVE_LEAF_COALESCE_TIMEOUT = 10 # seconds
543 TIMEOUT_SAFETY_MARGIN = 0.5 # extra margin when calculating
544 # feasibility of leaf coalesce
546 JRN_RELINK = "relink" # journal entry type for relinking children
547 JRN_COALESCE = "coalesce" # to communicate which VDI is being coalesced
548 JRN_LEAF = "leaf" # used in coalesce-leaf
550 STR_TREE_INDENT = 4
552 def __init__(self, sr, uuid, vdi_type):
553 self.sr = sr
554 self.scanError = True
555 self.uuid = uuid
556 self.vdi_type = vdi_type
557 self.fileName = ""
558 self.parentUuid = ""
559 self.sizeVirt = -1
560 self._sizePhys = -1
561 self._sizeAllocated = -1
562 self._hidden = False
563 self.parent = None
564 self.children = []
565 self._vdiRef = None
566 self.cowutil = getCowUtil(vdi_type)
567 self._clearRef()
569 @staticmethod
570 def extractUuid(path):
571 raise NotImplementedError("Implement in sub class")
573 def load(self, info=None) -> None:
574 """Load VDI info"""
575 pass
577 def getDriverName(self) -> str:
578 return self.vdi_type
580 def getRef(self):
581 if self._vdiRef is None:
582 self._vdiRef = self.sr.xapi.getRefVDI(self)
583 return self._vdiRef
585 def getConfig(self, key, default=None):
586 config = self.sr.xapi.getConfigVDI(self, key)
587 if key == self.DB_ONBOOT or key == self.DB_ALLOW_CACHING: 587 ↛ 588line 587 didn't jump to line 588, because the condition on line 587 was never true
588 val = config
589 else:
590 val = config.get(key)
591 if val:
592 return val
593 return default
595 def setConfig(self, key, val):
596 self.sr.xapi.removeFromConfigVDI(self, key)
597 self.sr.xapi.addToConfigVDI(self, key, val)
598 Util.log("Set %s = %s for %s" % (key, val, self))
600 def delConfig(self, key):
601 self.sr.xapi.removeFromConfigVDI(self, key)
602 Util.log("Removed %s from %s" % (key, self))
604 def ensurePaused(self):
605 if not self.getConfig(self.DB_VDI_PAUSED) == "true":
606 self.pause(failfast=True)
608 def ensureUnpaused(self):
609 if self.getConfig(self.DB_VDI_PAUSED) == "true":
610 Util.log("Unpausing VDI %s" % self)
611 self.unpause()
613 def pause(self, failfast=False) -> None:
614 if not blktap2.VDI.tap_pause(self.sr.xapi.session, self.sr.uuid,
615 self.uuid, failfast):
616 raise util.SMException("Failed to pause VDI %s" % self)
618 def _report_tapdisk_unpause_error(self):
619 try:
620 xapi = self.sr.xapi.session.xenapi
621 sr_ref = xapi.SR.get_by_uuid(self.sr.uuid)
622 msg_name = "failed to unpause tapdisk"
623 msg_body = "Failed to unpause tapdisk for VDI %s, " \
624 "VMs using this tapdisk have lost access " \
625 "to the corresponding disk(s)" % self.uuid
626 xapi.message.create(msg_name, "4", "SR", self.sr.uuid, msg_body)
627 except Exception as e:
628 util.SMlog("failed to generate message: %s" % e)
630 def unpause(self):
631 if not blktap2.VDI.tap_unpause(self.sr.xapi.session, self.sr.uuid,
632 self.uuid):
633 self._report_tapdisk_unpause_error()
634 raise util.SMException("Failed to unpause VDI %s" % self)
636 def refresh(self, ignoreNonexistent=True):
637 """Pause-unpause in one step"""
638 self.sr.lock()
639 try:
640 try:
641 if not blktap2.VDI.tap_refresh(self.sr.xapi.session, 641 ↛ 643line 641 didn't jump to line 643, because the condition on line 641 was never true
642 self.sr.uuid, self.uuid):
643 self._report_tapdisk_unpause_error()
644 raise util.SMException("Failed to refresh %s" % self)
645 except XenAPI.Failure as e:
646 if util.isInvalidVDI(e) and ignoreNonexistent:
647 Util.log("VDI %s not found, ignoring" % self)
648 return
649 raise
650 finally:
651 self.sr.unlock()
653 def isSnapshot(self):
654 return self.sr.xapi.isSnapshot(self)
656 def isAttachedRW(self):
657 return util.is_attached_rw(
658 self.sr.xapi.session.xenapi.VDI.get_sm_config(self.getRef()))
660 def getVDIBlocks(self):
661 val = self.updateBlockInfo()
662 bitmap = zlib.decompress(base64.b64decode(val))
663 return bitmap
665 def isCoalesceable(self):
666 """A VDI is coalesceable if it has no siblings and is not a leaf"""
667 return (
668 not self.scanError and
669 self.parent and
670 len(self.parent.children) == 1 and
671 self.isHidden() and
672 len(self.children) > 0 and (
673 # Conditions below are Qcow2 specific:
674 # A Qcow2 chain can't be coalesce with more than one leaf attached.
675 # Put it another way, Qcow2 leaves activated on multiple hosts must
676 # prevent the coalesce of the chain.
677 self.vdi_type != VdiType.QCOW2 or
678 (self.vdi_type == VdiType.QCOW2 and len(self.sr.hasLeavesAttachedOn(self)) <= 1)
679 )
680 )
682 def isLeafCoalesceable(self):
683 """A VDI is leaf-coalesceable if it has no siblings and is a leaf"""
684 return not self.scanError and \
685 self.parent and \
686 len(self.parent.children) == 1 and \
687 not self.isHidden() and \
688 len(self.children) == 0
690 def canLiveCoalesce(self, speed):
691 """Can we stop-and-leaf-coalesce this VDI? The VDI must be
692 isLeafCoalesceable() already"""
693 feasibleSize = False
694 allowedDownTime = \
695 self.TIMEOUT_SAFETY_MARGIN * self.LIVE_LEAF_COALESCE_TIMEOUT
696 allocated_size = self.getAllocatedSize()
697 if speed:
698 feasibleSize = \
699 allocated_size // speed < allowedDownTime
700 else:
701 feasibleSize = \
702 allocated_size < self.LIVE_LEAF_COALESCE_MAX_SIZE
704 return (feasibleSize or
705 self.getConfig(self.DB_LEAFCLSC) == self.LEAFCLSC_FORCE)
707 def getAllPrunable(self):
708 if len(self.children) == 0: # base case
709 # it is possible to have a hidden leaf that was recently coalesced
710 # onto its parent, its children already relinked but not yet
711 # reloaded - in which case it may not be garbage collected yet:
712 # some tapdisks could still be using the file.
713 if self.sr.journaler.get(self.JRN_RELINK, self.uuid):
714 return []
715 if not self.scanError and self.isHidden():
716 return [self]
717 return []
719 thisPrunable = True
720 vdiList = []
721 for child in self.children:
722 childList = child.getAllPrunable()
723 vdiList.extend(childList)
724 if child not in childList:
725 thisPrunable = False
727 # We can destroy the current VDI if all childs are hidden BUT the
728 # current VDI must be hidden too to do that!
729 # Example in this case (after a failed live leaf coalesce):
730 #
731 # SMGC: [32436] SR 07ed ('linstor-nvme-sr') (2 VDIs in 1 VHD trees):
732 # SMGC: [32436] b5458d61(1.000G/4.127M)
733 # SMGC: [32436] *OLD_b545(1.000G/4.129M)
734 #
735 # OLD_b545 is hidden and must be removed, but b5458d61 not.
736 # Normally we are not in this function when the delete action is
737 # executed but in `_liveLeafCoalesce`.
739 if not self.scanError and not self.isHidden() and thisPrunable:
740 vdiList.append(self)
741 return vdiList
743 def getSizePhys(self) -> int:
744 return self._sizePhys
746 def getAllocatedSize(self) -> int:
747 return self._sizeAllocated
749 def getTreeRoot(self):
750 "Get the root of the tree that self belongs to"
751 root = self
752 while root.parent:
753 root = root.parent
754 return root
756 def getTreeHeight(self):
757 "Get the height of the subtree rooted at self"
758 if len(self.children) == 0:
759 return 1
761 maxChildHeight = 0
762 for child in self.children:
763 childHeight = child.getTreeHeight()
764 if childHeight > maxChildHeight:
765 maxChildHeight = childHeight
767 return maxChildHeight + 1
769 def getAllLeaves(self) -> List["VDI"]:
770 "Get all leaf nodes in the subtree rooted at self"
771 if len(self.children) == 0:
772 return [self]
774 leaves = []
775 for child in self.children:
776 leaves.extend(child.getAllLeaves())
777 return leaves
779 def updateBlockInfo(self) -> Optional[str]:
780 val = base64.b64encode(self._queryCowBlocks()).decode()
781 try:
782 self.setConfig(VDI.DB_VDI_BLOCKS, val)
783 except Exception:
784 if self.vdi_type != VdiType.QCOW2:
785 raise
786 # Sometime with QCOW2, our allocation table is too big to be stored in XAPI, in this case we do not store it
787 # and we write `skipped` instead so that hasWork is happy (and the GC doesn't run in loop indefinitely).
788 self.setConfig(VDI.DB_VDI_BLOCKS, "skipped")
790 return val
792 def rename(self, uuid) -> None:
793 "Rename the VDI file"
794 assert(not self.sr.vdis.get(uuid))
795 self._clearRef()
796 oldUuid = self.uuid
797 self.uuid = uuid
798 self.children = []
799 # updating the children themselves is the responsibility of the caller
800 del self.sr.vdis[oldUuid]
801 self.sr.vdis[self.uuid] = self
803 def delete(self) -> None:
804 "Physically delete the VDI"
805 lock.Lock.cleanup(self.uuid, NS_PREFIX_LVM + self.sr.uuid)
806 lock.Lock.cleanupAll(self.uuid)
807 self._clear()
809 def getParent(self) -> str:
810 return self.cowutil.getParent(self.path, lambda x: x.strip()) 810 ↛ exitline 810 didn't run the lambda on line 810
812 def repair(self, parent) -> None:
813 self.cowutil.repair(parent)
815 @override
816 def __str__(self) -> str:
817 strHidden = ""
818 if self.isHidden(): 818 ↛ 819line 818 didn't jump to line 819, because the condition on line 818 was never true
819 strHidden = "*"
820 strSizeVirt = "?"
821 if self.sizeVirt > 0: 821 ↛ 822line 821 didn't jump to line 822, because the condition on line 821 was never true
822 strSizeVirt = Util.num2str(self.sizeVirt)
823 strSizePhys = "?"
824 if self._sizePhys > 0: 824 ↛ 825line 824 didn't jump to line 825, because the condition on line 824 was never true
825 strSizePhys = "/%s" % Util.num2str(self._sizePhys)
826 strSizeAllocated = "?"
827 if self._sizeAllocated >= 0:
828 strSizeAllocated = "/%s" % Util.num2str(self._sizeAllocated)
829 strType = "[{}]".format(self.vdi_type)
831 return "%s%s(%s%s%s)%s" % (strHidden, self.uuid[0:8], strSizeVirt,
832 strSizePhys, strSizeAllocated, strType)
834 def validate(self, fast=False) -> None:
835 if self.cowutil.check(self.path, fast=fast) != CowUtil.CheckResult.Success: 835 ↛ 836line 835 didn't jump to line 836, because the condition on line 835 was never true
836 raise util.SMException("COW image %s corrupted" % self)
838 def _clear(self):
839 self.uuid = ""
840 self.path = ""
841 self.parentUuid = ""
842 self.parent = None
843 self._clearRef()
845 def _clearRef(self):
846 self._vdiRef = None
848 @staticmethod
849 def _cancel_exception(sig, frame):
850 raise CancelException()
852 def _call_plugin_coalesce(self, hostRef, leaf):
853 signal.signal(signal.SIGTERM, self._cancel_exception)
854 args = {"path": self.path, "vdi_type": self.vdi_type, "leaf_path": leaf.path}
855 Util.log("Calling remote coalesce plugin with: {}".format(args))
856 try:
857 ret = self.sr.xapi.session.xenapi.host.call_plugin( \
858 hostRef, XAPI.PLUGIN_ON_SLAVE, "commit_tapdisk", args)
859 Util.log("Remote coalesce returned {}".format(ret))
860 except CancelException:
861 Util.log(f"Cancelling online coalesce following signal {args}")
862 self.sr.xapi.session.xenapi.host.call_plugin( \
863 hostRef, XAPI.PLUGIN_ON_SLAVE, "commit_cancel", args)
864 raise
865 except Exception:
866 raise
868 def _doCoalesceOnHost(self, hostRef, leaf):
869 self.parent._increaseSizeVirt(self.sizeVirt)
870 self.sr._updateSlavesOnResize(self.parent)
872 self._coalesceCowImageOnHost(hostRef, leaf)
874 #self._verifyContents(0)
875 self.parent.updateBlockInfo()
877 def _isOpenOnHosts(self) -> Optional[str]:
878 for pbdRecord in self.sr.xapi.getAttachedPBDs():
879 hostRef = pbdRecord["host"]
880 args = {"path": self.path}
881 is_openers = util.strtobool(self.sr.xapi.session.xenapi.host.call_plugin( \
882 hostRef, XAPI.PLUGIN_ON_SLAVE, "is_openers", args))
883 if is_openers:
884 return hostRef
885 return None
887 def _doCoalesce(self) -> None:
888 """Coalesce self onto parent. Only perform the actual coalescing of
889 an image, but not the subsequent relinking. We'll do that as the next step,
890 after reloading the entire SR in case things have changed while we
891 were coalescing"""
892 self.validate()
893 self.parent.validate(True)
894 self.parent._increaseSizeVirt(self.sizeVirt)
895 self.sr._updateSlavesOnResize(self.parent)
896 self._coalesceCowImage(0)
897 self.parent.validate(True)
898 #self._verifyContents(0)
899 self.parent.updateBlockInfo()
901 def _verifyContents(self, timeOut):
902 Util.log(" Coalesce verification on %s" % self)
903 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
904 Util.runAbortable(lambda: self._runTapdiskDiff(), True,
905 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
906 Util.log(" Coalesce verification succeeded")
908 def _runTapdiskDiff(self):
909 cmd = "tapdisk-diff -n %s:%s -m %s:%s" % \
910 (self.getDriverName(), self.path, \
911 self.parent.getDriverName(), self.parent.path)
912 Util.doexec(cmd, 0)
913 return True
915 @staticmethod
916 def _reportCoalesceError(vdi, ce):
917 """Reports a coalesce error to XenCenter.
919 vdi: the VDI object on which the coalesce error occured
920 ce: the CommandException that was raised"""
922 msg_name = os.strerror(ce.code)
923 if ce.code == errno.ENOSPC:
924 # TODO We could add more information here, e.g. exactly how much
925 # space is required for the particular coalesce, as well as actions
926 # to be taken by the user and consequences of not taking these
927 # actions.
928 msg_body = 'Run out of space while coalescing.'
929 elif ce.code == errno.EIO:
930 msg_body = 'I/O error while coalescing.'
931 else:
932 msg_body = ''
933 util.SMlog('Coalesce failed on SR %s: %s (%s)'
934 % (vdi.sr.uuid, msg_name, msg_body))
936 # Create a XenCenter message, but don't spam.
937 xapi = vdi.sr.xapi.session.xenapi
938 sr_ref = xapi.SR.get_by_uuid(vdi.sr.uuid)
939 oth_cfg = xapi.SR.get_other_config(sr_ref)
940 if COALESCE_ERR_RATE_TAG in oth_cfg:
941 coalesce_err_rate = float(oth_cfg[COALESCE_ERR_RATE_TAG])
942 else:
943 coalesce_err_rate = DEFAULT_COALESCE_ERR_RATE
945 xcmsg = False
946 if coalesce_err_rate == 0:
947 xcmsg = True
948 elif coalesce_err_rate > 0:
949 now = datetime.datetime.now()
950 sm_cfg = xapi.SR.get_sm_config(sr_ref)
951 if COALESCE_LAST_ERR_TAG in sm_cfg:
952 # seconds per message (minimum distance in time between two
953 # messages in seconds)
954 spm = datetime.timedelta(seconds=(1.0 / coalesce_err_rate) * 60)
955 last = datetime.datetime.fromtimestamp(
956 float(sm_cfg[COALESCE_LAST_ERR_TAG]))
957 if now - last >= spm:
958 xapi.SR.remove_from_sm_config(sr_ref,
959 COALESCE_LAST_ERR_TAG)
960 xcmsg = True
961 else:
962 xcmsg = True
963 if xcmsg:
964 xapi.SR.add_to_sm_config(sr_ref, COALESCE_LAST_ERR_TAG,
965 str(now.strftime('%s')))
966 if xcmsg:
967 xapi.message.create(msg_name, "3", "SR", vdi.sr.uuid, msg_body)
969 def coalesce(self) -> int:
970 return self.cowutil.coalesce(self.path)
972 @staticmethod
973 def _doCoalesceCowImage(vdi: "VDI"):
974 try:
975 startTime = time.time()
976 allocated_size = vdi.getAllocatedSize()
977 coalesced_size = vdi.coalesce()
978 endTime = time.time()
979 vdi.sr.recordStorageSpeed(startTime, endTime, coalesced_size)
980 except util.CommandException as ce:
981 # We use try/except for the following piece of code because it runs
982 # in a separate process context and errors will not be caught and
983 # reported by anyone.
984 try:
985 # Report coalesce errors back to user via XC
986 VDI._reportCoalesceError(vdi, ce)
987 except Exception as e:
988 util.SMlog('failed to create XenCenter message: %s' % e)
989 raise ce
990 except:
991 raise
993 def _vdi_is_raw(self, vdi_path):
994 """
995 Given path to vdi determine if it is raw
996 """
997 uuid = self.extractUuid(vdi_path)
998 return self.sr.vdis[uuid].vdi_type == VdiType.RAW
1000 def _coalesceCowImage(self, timeOut):
1001 Util.log(" Running COW coalesce on %s" % self)
1002 def abortTest():
1003 if self.cowutil.isCoalesceableOnRemote():
1004 file = self.sr._gc_running_file(self)
1005 try:
1006 with open(file, "r") as f:
1007 if not f.read():
1008 return True
1009 except OSError as e:
1010 if e.errno == errno.ENOENT:
1011 util.SMlog("File {} does not exist".format(file))
1012 else:
1013 util.SMlog("IOError: {}".format(e))
1014 return True
1015 return IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1017 try:
1018 util.fistpoint.activate_custom_fn(
1019 "cleanup_coalesceVHD_inject_failure",
1020 util.inject_failure)
1021 Util.runAbortable(lambda: VDI._doCoalesceCowImage(self), None,
1022 self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
1023 except:
1024 # Exception at this phase could indicate a failure in COW coalesce
1025 # or a kill of COW coalesce by runAbortable due to timeOut
1026 # Try a repair and reraise the exception
1027 parent = ""
1028 try:
1029 parent = self.getParent()
1030 if not self._vdi_is_raw(parent):
1031 # Repair error is logged and ignored. Error reraised later
1032 util.SMlog('Coalesce failed on %s, attempting repair on ' \
1033 'parent %s' % (self.uuid, parent))
1034 self.repair(parent)
1035 except Exception as e:
1036 util.SMlog('(error ignored) Failed to repair parent %s ' \
1037 'after failed coalesce on %s, err: %s' %
1038 (parent, self.path, e))
1039 raise
1041 util.fistpoint.activate("LVHDRT_coalescing_VHD_data", self.sr.uuid)
1043 def _coalesceCowImageOnHost(self, hostRef, leaf):
1044 Util.log(" Running COW coalesce on {} via remote host {}".format(self, hostRef))
1045 def abortTest():
1046 file = self.sr._gc_running_file(self)
1047 try:
1048 with open(file, "r") as f:
1049 if not f.read():
1050 Util.log("abortTest: Cancelling coalesce")
1051 return True
1052 except OSError as e:
1053 if e.errno == errno.ENOENT:
1054 Util.log("File {} does not exist".format(file))
1055 else:
1056 Util.log("IOError: {}".format(e))
1057 return True
1058 return False
1060 Util.runAbortable(lambda: self._call_plugin_coalesce(hostRef, leaf),
1061 None, self.sr.uuid, abortTest, VDI.POLL_INTERVAL, 0, prefSig=signal.SIGTERM)
1063 def _relinkSkip(self) -> None:
1064 """Relink children of this VDI to point to the parent of this VDI"""
1065 abortFlag = IPCFlag(self.sr.uuid)
1066 for child in self.children:
1067 if abortFlag.test(FLAG_TYPE_ABORT): 1067 ↛ 1068line 1067 didn't jump to line 1068, because the condition on line 1067 was never true
1068 raise AbortException("Aborting due to signal")
1069 Util.log(" Relinking %s from %s to %s" % \
1070 (child, self, self.parent))
1071 util.fistpoint.activate("LVHDRT_relinking_grandchildren", self.sr.uuid)
1072 child._setParent(self.parent)
1073 self.children = []
1075 def _reloadChildren(self, vdiSkip):
1076 """Pause & unpause all VDIs in the subtree to cause blktap to reload
1077 the COW image metadata for this file in any online VDI"""
1078 abortFlag = IPCFlag(self.sr.uuid)
1079 for child in self.children:
1080 if child == vdiSkip:
1081 continue
1082 if abortFlag.test(FLAG_TYPE_ABORT): 1082 ↛ 1083line 1082 didn't jump to line 1083, because the condition on line 1082 was never true
1083 raise AbortException("Aborting due to signal")
1084 Util.log(" Reloading VDI %s" % child)
1085 child._reload()
1087 def _reload(self):
1088 """Pause & unpause to cause blktap to reload the image metadata"""
1089 for child in self.children: 1089 ↛ 1090line 1089 didn't jump to line 1090, because the loop on line 1089 never started
1090 child._reload()
1092 # only leaves can be attached
1093 if len(self.children) == 0: 1093 ↛ exitline 1093 didn't return from function '_reload', because the condition on line 1093 was never false
1094 try:
1095 self.delConfig(VDI.DB_VDI_RELINKING)
1096 except XenAPI.Failure as e:
1097 if not util.isInvalidVDI(e):
1098 raise
1099 self.refresh()
1101 def _needRelink(self, list_not_to_relink):
1102 """
1103 If we coalesce up the chain, we shouldn't need to do the relink at all, we only need to do the relink on the children if their direct parent was the one we were coalescing
1104 """
1105 if not list_not_to_relink: 1105 ↛ 1107line 1105 didn't jump to line 1107, because the condition on line 1105 was never false
1106 return True
1107 if self.uuid in list_not_to_relink:
1108 return False
1109 else:
1110 return True
1112 def _tagChildrenForRelink(self, list_not_to_relink=None):
1113 if len(self.children) == 0:
1114 retries = 0
1115 try:
1116 while retries < 15:
1117 retries += 1
1118 if self.getConfig(VDI.DB_VDI_ACTIVATING) is not None:
1119 Util.log("VDI %s is activating, wait to relink" %
1120 self.uuid)
1121 else:
1122 if self._needRelink(list_not_to_relink): 1122 ↛ 1132line 1122 didn't jump to line 1132, because the condition on line 1122 was never false
1123 self.setConfig(VDI.DB_VDI_RELINKING, "True")
1125 if self.getConfig(VDI.DB_VDI_ACTIVATING):
1126 self.delConfig(VDI.DB_VDI_RELINKING)
1127 Util.log("VDI %s started activating while tagging" %
1128 self.uuid)
1129 else:
1130 return
1131 else:
1132 Util.log(f"Not adding relinking tag to VDI {self.uuid}")
1133 return
1134 time.sleep(2)
1136 raise util.SMException("Failed to tag vdi %s for relink" % self)
1137 except XenAPI.Failure as e:
1138 if not util.isInvalidVDI(e):
1139 raise
1141 for child in self.children:
1142 child._tagChildrenForRelink(list_not_to_relink)
1144 def _loadInfoParent(self):
1145 ret = self.cowutil.getParent(self.path, LvmCowUtil.extractUuid)
1146 if ret:
1147 self.parentUuid = ret
1149 def _setParent(self, parent) -> None:
1150 self.cowutil.setParent(self.path, parent.path, False)
1151 self.parent = parent
1152 self.parentUuid = parent.uuid
1153 parent.children.append(self)
1154 try:
1155 self.setConfig(self.DB_VDI_PARENT, self.parentUuid)
1156 Util.log("Updated the vhd-parent field for child %s with %s" % \
1157 (self.uuid, self.parentUuid))
1158 except:
1159 Util.log("Failed to update %s with vhd-parent field %s" % \
1160 (self.uuid, self.parentUuid))
1162 def _ensureParentActiveForRelink(self) -> None:
1163 pass
1165 def _update_vhd_parent(self, real_parent_uuid):
1166 try:
1167 self.setConfig(self.DB_VDI_PARENT, real_parent_uuid)
1168 Util.log("Updated the vhd-parent field for child %s with real parent %s following a online coalesce" % \
1169 (self.uuid, real_parent_uuid))
1170 except:
1171 Util.log("Failed to update %s with vhd-parent field %s" % \
1172 (self.uuid, real_parent_uuid))
1174 def isHidden(self) -> bool:
1175 if self._hidden is None: 1175 ↛ 1176line 1175 didn't jump to line 1176, because the condition on line 1175 was never true
1176 self._loadInfoHidden()
1177 return self._hidden
1179 def _loadInfoHidden(self) -> None:
1180 hidden = self.cowutil.getHidden(self.path)
1181 self._hidden = (hidden != 0)
1183 def _setHidden(self, hidden=True) -> None:
1184 self._hidden = None
1185 self.cowutil.setHidden(self.path, hidden)
1186 self._hidden = hidden
1188 def _increaseSizeVirt(self, size, atomic=True) -> None:
1189 """ensure the virtual size of 'self' is at least 'size'. Note that
1190 resizing a COW image must always be offline and atomically: the file must
1191 not be open by anyone and no concurrent operations may take place.
1192 Thus we use the Agent API call for performing paused atomic
1193 operations. If the caller is already in the atomic context, it must
1194 call with atomic = False"""
1195 if self.sizeVirt >= size: 1195 ↛ 1197line 1195 didn't jump to line 1197, because the condition on line 1195 was never false
1196 return
1197 Util.log(" Expanding COW image virt size for VDI %s: %s -> %s" % \
1198 (self, Util.num2str(self.sizeVirt), Util.num2str(size)))
1200 msize = self.cowutil.getMaxResizeSize(self.path)
1201 if (size <= msize):
1202 self.cowutil.setSizeVirtFast(self.path, size)
1203 else:
1204 if atomic:
1205 vdiList = self._getAllSubtree()
1206 self.sr.lock()
1207 try:
1208 self.sr.pauseVDIs(vdiList)
1209 try:
1210 self._setSizeVirt(size)
1211 finally:
1212 self.sr.unpauseVDIs(vdiList)
1213 finally:
1214 self.sr.unlock()
1215 else:
1216 self._setSizeVirt(size)
1218 self.sizeVirt = self.cowutil.getSizeVirt(self.path)
1220 def _setSizeVirt(self, size) -> None:
1221 """WARNING: do not call this method directly unless all VDIs in the
1222 subtree are guaranteed to be unplugged (and remain so for the duration
1223 of the operation): this operation is only safe for offline COW images"""
1224 jFile = os.path.join(self.sr.path, self.uuid)
1225 self.cowutil.setSizeVirt(self.path, size, jFile)
1227 def _queryCowBlocks(self) -> bytes:
1228 return self.cowutil.getBlockBitmap(self.path)
1230 def _getCoalescedSizeData(self):
1231 """Get the data size of the resulting image if we coalesce self onto
1232 parent. We calculate the actual size by using the image block allocation
1233 information (as opposed to just adding up the two image sizes to get an
1234 upper bound)"""
1235 # make sure we don't use stale BAT info from vdi_rec since the child
1236 # was writable all this time
1237 self.delConfig(VDI.DB_VDI_BLOCKS)
1238 blocksChild = self.getVDIBlocks()
1239 blocksParent = self.parent.getVDIBlocks()
1240 numBlocks = Util.countBits(blocksChild, blocksParent)
1241 Util.log("Num combined blocks = %d" % numBlocks)
1242 sizeData = numBlocks * self.cowutil.getBlockSize(self.path)
1243 assert(sizeData <= self.sizeVirt)
1244 return sizeData
1246 def _calcExtraSpaceForCoalescing(self) -> int:
1247 sizeData = self._getCoalescedSizeData()
1248 sizeCoalesced = sizeData + self.cowutil.calcOverheadBitmap(sizeData) + \
1249 self.cowutil.calcOverheadEmpty(self.sizeVirt)
1250 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1251 return sizeCoalesced - self.parent.getSizePhys()
1253 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1254 """How much extra space in the SR will be required to
1255 [live-]leaf-coalesce this VDI"""
1256 # the space requirements are the same as for inline coalesce
1257 return self._calcExtraSpaceForCoalescing()
1259 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1260 """How much extra space in the SR will be required to
1261 snapshot-coalesce this VDI"""
1262 return self._calcExtraSpaceForCoalescing() + \
1263 self.cowutil.calcOverheadEmpty(self.sizeVirt) # extra snap leaf
1265 def _getAllSubtree(self):
1266 """Get self and all VDIs in the subtree of self as a flat list"""
1267 vdiList = [self]
1268 for child in self.children:
1269 vdiList.extend(child._getAllSubtree())
1270 return vdiList
1273class FileVDI(VDI):
1274 """Object representing a VDI in a file-based SR (EXT or NFS)"""
1276 @override
1277 @staticmethod
1278 def extractUuid(path):
1279 fileName = os.path.basename(path)
1280 return os.path.splitext(fileName)[0]
1282 def __init__(self, sr, uuid, vdi_type):
1283 VDI.__init__(self, sr, uuid, vdi_type)
1284 self.fileName = "%s%s" % (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type])
1286 @override
1287 def load(self, info=None) -> None:
1288 if not info:
1289 if not util.pathexists(self.path):
1290 raise util.SMException("%s not found" % self.path)
1291 try:
1292 info = self.cowutil.getInfo(self.path, self.extractUuid)
1293 except util.SMException:
1294 Util.log(" [VDI %s: failed to read COW image metadata]" % self.uuid)
1295 return
1296 self.parent = None
1297 self.children = []
1298 self.parentUuid = info.parentUuid
1299 self.sizeVirt = info.sizeVirt
1300 self._sizePhys = info.sizePhys
1301 self._sizeAllocated = info.sizeAllocated
1302 self._hidden = info.hidden
1303 self.scanError = False
1304 self.path = os.path.join(self.sr.path, "%s%s" % \
1305 (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type]))
1307 @override
1308 def rename(self, uuid) -> None:
1309 oldPath = self.path
1310 VDI.rename(self, uuid)
1311 self.fileName = "%s%s" % (self.uuid, VDI_TYPE_TO_EXTENSION[self.vdi_type])
1312 self.path = os.path.join(self.sr.path, self.fileName)
1313 assert(not util.pathexists(self.path))
1314 Util.log("Renaming %s -> %s" % (oldPath, self.path))
1315 os.rename(oldPath, self.path)
1317 @override
1318 def delete(self) -> None:
1319 if len(self.children) > 0: 1319 ↛ 1320line 1319 didn't jump to line 1320, because the condition on line 1319 was never true
1320 raise util.SMException("VDI %s has children, can't delete" % \
1321 self.uuid)
1322 try:
1323 self.sr.lock()
1324 try:
1325 os.unlink(self.path)
1326 self.sr.forgetVDI(self.uuid)
1327 finally:
1328 self.sr.unlock()
1329 except OSError:
1330 raise util.SMException("os.unlink(%s) failed" % self.path)
1331 VDI.delete(self)
1333 @override
1334 def getAllocatedSize(self) -> int:
1335 if self._sizeAllocated == -1: 1335 ↛ 1336line 1335 didn't jump to line 1336, because the condition on line 1335 was never true
1336 self._sizeAllocated = self.cowutil.getAllocatedSize(self.path)
1337 return self._sizeAllocated
1340class LVMVDI(VDI):
1341 """Object representing a VDI in an LVM SR"""
1343 JRN_ZERO = "zero" # journal entry type for zeroing out end of parent
1345 @override
1346 def load(self, info=None) -> None:
1347 # `info` is always set. `None` default value is only here to match parent method.
1348 assert info, "No info given to LVMVDI.load"
1349 self.parent = None
1350 self.children = []
1351 self._sizePhys = -1
1352 self._sizeAllocated = -1
1353 self.scanError = info.scanError
1354 self.sizeLV = info.sizeLV
1355 self.sizeVirt = info.sizeVirt
1356 self.fileName = info.lvName
1357 self.lvActive = info.lvActive
1358 self.lvOpen = info.lvOpen
1359 self.lvReadonly = info.lvReadonly
1360 self._hidden = info.hidden
1361 self.parentUuid = info.parentUuid
1362 self.path = os.path.join(self.sr.path, self.fileName)
1363 self.lvmcowutil = LvmCowUtil(self.cowutil)
1365 @override
1366 @staticmethod
1367 def extractUuid(path):
1368 return LvmCowUtil.extractUuid(path)
1370 def inflate(self, size):
1371 """inflate the LV containing the COW image to 'size'"""
1372 if not VdiType.isCowImage(self.vdi_type):
1373 return
1374 self._activate()
1375 self.sr.lock()
1376 try:
1377 self.lvmcowutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, self.vdi_type, size)
1378 util.fistpoint.activate("LVHDRT_inflating_the_parent", self.sr.uuid)
1379 finally:
1380 self.sr.unlock()
1381 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1382 self._sizePhys = -1
1383 self._sizeAllocated = -1
1385 def deflate(self):
1386 """deflate the LV containing the image to minimum"""
1387 if not VdiType.isCowImage(self.vdi_type):
1388 return
1389 self._activate()
1390 self.sr.lock()
1391 try:
1392 self.lvmcowutil.deflate(self.sr.lvmCache, self.fileName, self.getSizePhys())
1393 finally:
1394 self.sr.unlock()
1395 self.sizeLV = self.sr.lvmCache.getSize(self.fileName)
1396 self._sizePhys = -1
1397 self._sizeAllocated = -1
1399 def inflateFully(self):
1400 self.inflate(self.lvmcowutil.calcVolumeSize(self.sizeVirt))
1402 def inflateParentForCoalesce(self):
1403 """Inflate the parent only as much as needed for the purposes of
1404 coalescing"""
1405 if not VdiType.isCowImage(self.parent.vdi_type):
1406 return
1407 inc = self._calcExtraSpaceForCoalescing()
1408 if inc > 0:
1409 util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent", self.sr.uuid)
1410 self.parent.inflate(self.parent.sizeLV + inc)
1412 @override
1413 def updateBlockInfo(self) -> Optional[str]:
1414 if VdiType.isCowImage(self.vdi_type):
1415 return VDI.updateBlockInfo(self)
1416 return None
1418 @override
1419 def rename(self, uuid) -> None:
1420 oldUuid = self.uuid
1421 oldLVName = self.fileName
1422 VDI.rename(self, uuid)
1423 self.fileName = LV_PREFIX[self.vdi_type] + self.uuid
1424 self.path = os.path.join(self.sr.path, self.fileName)
1425 assert(not self.sr.lvmCache.checkLV(self.fileName))
1427 self.sr.lvmCache.rename(oldLVName, self.fileName)
1428 if self.sr.lvActivator.get(oldUuid, False):
1429 self.sr.lvActivator.replace(oldUuid, self.uuid, self.fileName, False)
1431 ns = NS_PREFIX_LVM + self.sr.uuid
1432 (cnt, bcnt) = RefCounter.check(oldUuid, ns)
1433 RefCounter.set(self.uuid, cnt, bcnt, ns)
1434 RefCounter.reset(oldUuid, ns)
1436 @override
1437 def delete(self) -> None:
1438 if len(self.children) > 0:
1439 raise util.SMException("VDI %s has children, can't delete" % \
1440 self.uuid)
1441 self.sr.lock()
1442 try:
1443 self.sr.lvmCache.remove(self.fileName)
1444 self.sr.forgetVDI(self.uuid)
1445 finally:
1446 self.sr.unlock()
1447 RefCounter.reset(self.uuid, NS_PREFIX_LVM + self.sr.uuid)
1448 VDI.delete(self)
1450 @override
1451 def getSizePhys(self) -> int:
1452 if self._sizePhys == -1:
1453 self._loadInfoSizePhys()
1454 return self._sizePhys
1456 def _loadInfoSizePhys(self):
1457 """Get the physical utilization of the COW image file. We do it individually
1458 (and not using the COW batch scanner) as an optimization: this info is
1459 relatively expensive and we need it only for VDI's involved in
1460 coalescing."""
1461 if not VdiType.isCowImage(self.vdi_type):
1462 return
1463 self._activate()
1464 self._sizePhys = self.cowutil.getSizePhys(self.path)
1465 if self._sizePhys <= 0:
1466 raise util.SMException("phys size of %s = %d" % \
1467 (self, self._sizePhys))
1469 @override
1470 def getAllocatedSize(self) -> int:
1471 if self._sizeAllocated == -1:
1472 self._loadInfoSizeAllocated()
1473 return self._sizeAllocated
1475 def _loadInfoSizeAllocated(self):
1476 """
1477 Get the allocated size of the COW volume.
1478 """
1479 if not VdiType.isCowImage(self.vdi_type):
1480 return
1481 self._activate()
1482 self._sizeAllocated = self.cowutil.getAllocatedSize(self.path)
1484 @override
1485 def _loadInfoHidden(self) -> None:
1486 if not VdiType.isCowImage(self.vdi_type):
1487 self._hidden = self.sr.lvmCache.getHidden(self.fileName)
1488 else:
1489 VDI._loadInfoHidden(self)
1491 @override
1492 def _setHidden(self, hidden=True) -> None:
1493 if not VdiType.isCowImage(self.vdi_type):
1494 self._hidden = None
1495 self.sr.lvmCache.setHidden(self.fileName, hidden)
1496 self._hidden = hidden
1497 else:
1498 VDI._setHidden(self, hidden)
1500 @override
1501 def __str__(self) -> str:
1502 strType = self.vdi_type
1503 if self.vdi_type == VdiType.RAW:
1504 strType = "RAW"
1505 strHidden = ""
1506 if self.isHidden():
1507 strHidden = "*"
1508 strSizePhys = ""
1509 if self._sizePhys > 0:
1510 strSizePhys = Util.num2str(self._sizePhys)
1511 strSizeAllocated = ""
1512 if self._sizeAllocated >= 0:
1513 strSizeAllocated = Util.num2str(self._sizeAllocated)
1514 strActive = "n"
1515 if self.lvActive:
1516 strActive = "a"
1517 if self.lvOpen:
1518 strActive += "o"
1519 return "%s%s[%s](%s/%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType,
1520 Util.num2str(self.sizeVirt), strSizePhys, strSizeAllocated,
1521 Util.num2str(self.sizeLV), strActive)
1523 @override
1524 def validate(self, fast=False) -> None:
1525 if VdiType.isCowImage(self.vdi_type):
1526 VDI.validate(self, fast)
1528 def _setChainRw(self) -> List[str]:
1529 """
1530 Set the readonly LV and children writable.
1531 It's needed because the coalesce can be done by tapdisk directly
1532 and it will need to write parent information for children.
1533 The VDI we want to coalesce into it's parent need to be writable for libqcow coalesce part.
1534 Return a list of the LV that were previously readonly to be made RO again after the coalesce.
1535 """
1536 was_ro = []
1537 if self.lvReadonly:
1538 self.sr.lvmCache.setReadonly(self.fileName, False)
1539 was_ro.append(self.fileName)
1541 for child in self.children:
1542 if child.lvReadonly:
1543 self.sr.lvmCache.setReadonly(child.fileName, False)
1544 was_ro.append(child.fileName)
1546 return was_ro
1548 def _setChainRo(self, was_ro: List[str]) -> None:
1549 """Set the list of LV in parameters to readonly"""
1550 for lvName in was_ro:
1551 self.sr.lvmCache.setReadonly(lvName, True)
1553 @override
1554 def _doCoalesce(self) -> None:
1555 """LVMVDI parents must first be activated, inflated, and made writable"""
1556 was_ro = []
1557 try:
1558 self._activateChain()
1559 self.sr.lvmCache.setReadonly(self.parent.fileName, False)
1560 self.parent.validate()
1561 self.inflateParentForCoalesce()
1562 was_ro = self._setChainRw()
1563 VDI._doCoalesce(self)
1564 finally:
1565 self.parent._loadInfoSizePhys()
1566 self.parent.deflate()
1567 self.sr.lvmCache.setReadonly(self.parent.fileName, True)
1568 self._setChainRo(was_ro)
1570 @override
1571 def _setParent(self, parent) -> None:
1572 self._activate()
1573 if self.lvReadonly:
1574 self.sr.lvmCache.setReadonly(self.fileName, False)
1576 try:
1577 self.cowutil.setParent(self.path, parent.path, parent.vdi_type == VdiType.RAW)
1578 finally:
1579 if self.lvReadonly:
1580 self.sr.lvmCache.setReadonly(self.fileName, True)
1581 self._deactivate()
1582 self.parent = parent
1583 self.parentUuid = parent.uuid
1584 parent.children.append(self)
1585 try:
1586 self.setConfig(self.DB_VDI_PARENT, self.parentUuid)
1587 Util.log("Updated the VDI-parent field for child %s with %s" % \
1588 (self.uuid, self.parentUuid))
1589 except:
1590 Util.log("Failed to update the VDI-parent with %s for child %s" % \
1591 (self.parentUuid, self.uuid))
1593 def _activate(self):
1594 self.sr.lvActivator.activate(self.uuid, self.fileName, False)
1596 def _activateChain(self):
1597 vdi = self
1598 while vdi:
1599 vdi._activate()
1600 vdi = vdi.parent
1602 def _deactivate(self):
1603 self.sr.lvActivator.deactivate(self.uuid, False)
1605 @override
1606 def _ensureParentActiveForRelink(self) -> None:
1607 self.parent._activate()
1609 @override
1610 def _increaseSizeVirt(self, size, atomic=True) -> None:
1611 "ensure the virtual size of 'self' is at least 'size'"
1612 self._activate()
1613 if VdiType.isCowImage(self.vdi_type):
1614 VDI._increaseSizeVirt(self, size, atomic)
1615 return
1617 # raw VDI case
1618 offset = self.sizeLV
1619 if self.sizeVirt < size:
1620 oldSize = self.sizeLV
1621 self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size)
1622 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV))
1623 self.sr.lvmCache.setSize(self.fileName, self.sizeLV)
1624 offset = oldSize
1625 unfinishedZero = False
1626 jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid)
1627 if jval:
1628 unfinishedZero = True
1629 offset = int(jval)
1630 length = self.sizeLV - offset
1631 if not length:
1632 return
1634 if unfinishedZero:
1635 Util.log(" ==> Redoing unfinished zeroing out")
1636 else:
1637 self.sr.journaler.create(self.JRN_ZERO, self.uuid, \
1638 str(offset))
1639 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
1640 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1641 func = lambda: util.zeroOut(self.path, offset, length)
1642 Util.runAbortable(func, True, self.sr.uuid, abortTest,
1643 VDI.POLL_INTERVAL, 0)
1644 self.sr.journaler.remove(self.JRN_ZERO, self.uuid)
1646 @override
1647 def _setSizeVirt(self, size) -> None:
1648 """WARNING: do not call this method directly unless all VDIs in the
1649 subtree are guaranteed to be unplugged (and remain so for the duration
1650 of the operation): this operation is only safe for offline COW images."""
1651 self._activate()
1652 jFile = self.lvmcowutil.createResizeJournal(self.sr.lvmCache, self.uuid)
1653 try:
1654 self.lvmcowutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid, self.vdi_type, size, jFile)
1655 finally:
1656 self.lvmcowutil.destroyResizeJournal(self.sr.lvmCache, self.uuid)
1658 @override
1659 def _queryCowBlocks(self) -> bytes:
1660 self._activate()
1661 return VDI._queryCowBlocks(self)
1663 @override
1664 def getParent(self) -> str:
1665 self._activate()
1666 parent = VDI.getParent(self)
1667 self._deactivate()
1668 return parent
1670 @override
1671 def _calcExtraSpaceForCoalescing(self) -> int:
1672 if not VdiType.isCowImage(self.parent.vdi_type):
1673 return 0 # raw parents are never deflated in the first place
1674 sizeCoalesced = self.lvmcowutil.calcVolumeSize(self._getCoalescedSizeData())
1675 Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
1676 return sizeCoalesced - self.parent.sizeLV
1678 @override
1679 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1680 """How much extra space in the SR will be required to
1681 [live-]leaf-coalesce this VDI"""
1682 # we can deflate the leaf to minimize the space requirements
1683 deflateDiff = self.sizeLV - lvutil.calcSizeLV(self.getSizePhys())
1684 return self._calcExtraSpaceForCoalescing() - deflateDiff
1686 @override
1687 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
1688 return self._calcExtraSpaceForCoalescing() + \
1689 lvutil.calcSizeLV(self.getSizePhys())
1692class LinstorVDI(VDI):
1693 """Object representing a VDI in a LINSTOR SR"""
1695 VOLUME_LOCK_TIMEOUT = 30
1697 @override
1698 def load(self, info=None) -> None:
1699 self.parentUuid = info.parentUuid
1700 self.scanError = True
1701 self.parent = None
1702 self.children = []
1704 self.fileName = self.sr._linstor.get_volume_name(self.uuid)
1705 self.path = self.sr._linstor.build_device_path(self.fileName)
1706 self.linstorcowutil = LinstorCowUtil(self.sr.xapi.session, self.sr._linstor, info.vdiType)
1708 if not info:
1709 try:
1710 info = self.linstorcowutil.get_info(self.uuid)
1711 except util.SMException:
1712 Util.log(
1713 ' [VDI {}: failed to read COW image metadata]'.format(self.uuid)
1714 )
1715 return
1717 self.parentUuid = info.parentUuid
1718 self.sizeVirt = info.sizeVirt
1719 self._sizePhys = -1
1720 self._sizeAllocated = -1
1721 self.drbd_size = -1
1722 self._hidden = info.hidden
1723 self.scanError = False
1725 @override
1726 def getSizePhys(self, fetch=False) -> int:
1727 if self._sizePhys < 0 or fetch:
1728 self._sizePhys = self.linstorcowutil.get_size_phys(self.uuid)
1729 return self._sizePhys
1731 def getDrbdSize(self, fetch=False):
1732 if self.drbd_size < 0 or fetch:
1733 self.drbd_size = self.linstorcowutil.get_drbd_size(self.uuid)
1734 return self.drbd_size
1736 @override
1737 def getAllocatedSize(self) -> int:
1738 if self._sizeAllocated == -1:
1739 if VdiType.isCowImage(self.vdi_type):
1740 self._sizeAllocated = self.linstorcowutil.get_allocated_size(self.uuid)
1741 return self._sizeAllocated
1743 def inflate(self, size):
1744 if not VdiType.isCowImage(self.vdi_type):
1745 return
1746 self.sr.lock()
1747 try:
1748 # Ensure we use the real DRBD size and not the cached one.
1749 # Why? Because this attribute can be changed if volume is resized by user.
1750 self.drbd_size = self.getDrbdSize(fetch=True)
1751 self.linstorcowutil.inflate(self.sr.journaler, self.uuid, self.path, size, self.drbd_size)
1752 finally:
1753 self.sr.unlock()
1754 self.drbd_size = -1
1755 self._sizePhys = -1
1756 self._sizeAllocated = -1
1758 def deflate(self):
1759 if not VdiType.isCowImage(self.vdi_type):
1760 return
1761 self.sr.lock()
1762 try:
1763 # Ensure we use the real sizes and not the cached info.
1764 self.drbd_size = self.getDrbdSize(fetch=True)
1765 self._sizePhys = self.getSizePhys(fetch=True)
1766 self.linstorcowutil.force_deflate(self.path, self._sizePhys, self.drbd_size, zeroize=False)
1767 finally:
1768 self.sr.unlock()
1769 self.drbd_size = -1
1770 self._sizePhys = -1
1771 self._sizeAllocated = -1
1773 def inflateFully(self):
1774 if VdiType.isCowImage(self.vdi_type):
1775 self.inflate(self.linstorcowutil.compute_volume_size(self.sizeVirt))
1777 @override
1778 def rename(self, uuid) -> None:
1779 Util.log('Renaming {} -> {} (path={})'.format(
1780 self.uuid, uuid, self.path
1781 ))
1782 self.sr._linstor.update_volume_uuid(self.uuid, uuid)
1783 VDI.rename(self, uuid)
1785 @override
1786 def delete(self) -> None:
1787 if len(self.children) > 0:
1788 raise util.SMException(
1789 'VDI {} has children, can\'t delete'.format(self.uuid)
1790 )
1791 self.sr.lock()
1792 try:
1793 self.sr._linstor.destroy_volume(self.uuid)
1794 self.sr.forgetVDI(self.uuid)
1795 finally:
1796 self.sr.unlock()
1797 VDI.delete(self)
1799 @override
1800 def validate(self, fast=False) -> None:
1801 if VdiType.isCowImage(self.vdi_type) and self.linstorcowutil.check(self.uuid, fast=fast) != CowUtil.CheckResult.Success:
1802 raise util.SMException('COW image {} corrupted'.format(self))
1804 @override
1805 def pause(self, failfast=False) -> None:
1806 self.sr._linstor.ensure_volume_is_not_locked(
1807 self.uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1808 )
1809 return super(LinstorVDI, self).pause(failfast)
1811 @override
1812 def coalesce(self) -> int:
1813 # Note: We raise `SMException` here to skip the current coalesce in case of failure.
1814 # Using another exception we can't execute the next coalesce calls.
1815 return self.linstorcowutil.force_coalesce(self.path)
1817 @override
1818 def getParent(self) -> str:
1819 return self.linstorcowutil.get_parent(
1820 self.sr._linstor.get_volume_uuid_from_device_path(self.path)
1821 )
1823 @override
1824 def repair(self, parent_uuid) -> None:
1825 self.linstorcowutil.force_repair(
1826 self.sr._linstor.get_device_path(parent_uuid)
1827 )
1829 @override
1830 def _relinkSkip(self) -> None:
1831 abortFlag = IPCFlag(self.sr.uuid)
1832 for child in self.children:
1833 if abortFlag.test(FLAG_TYPE_ABORT):
1834 raise AbortException('Aborting due to signal')
1835 Util.log(
1836 ' Relinking {} from {} to {}'.format(
1837 child, self, self.parent
1838 )
1839 )
1841 session = child.sr.xapi.session
1842 sr_uuid = child.sr.uuid
1843 vdi_uuid = child.uuid
1844 try:
1845 self.sr._linstor.ensure_volume_is_not_locked(
1846 vdi_uuid, timeout=self.VOLUME_LOCK_TIMEOUT
1847 )
1848 blktap2.VDI.tap_pause(session, sr_uuid, vdi_uuid)
1849 child._setParent(self.parent)
1850 finally:
1851 blktap2.VDI.tap_unpause(session, sr_uuid, vdi_uuid)
1852 self.children = []
1854 @override
1855 def _setParent(self, parent) -> None:
1856 self.sr._linstor.get_device_path(self.uuid)
1857 self.linstorcowutil.force_parent(self.path, parent.path)
1858 self.parent = parent
1859 self.parentUuid = parent.uuid
1860 parent.children.append(self)
1861 try:
1862 self.setConfig(self.DB_VDI_PARENT, self.parentUuid)
1863 Util.log("Updated the vhd-parent field for child %s with %s" % \
1864 (self.uuid, self.parentUuid))
1865 except:
1866 Util.log("Failed to update %s with vhd-parent field %s" % \
1867 (self.uuid, self.parentUuid))
1869 @override
1870 def _doCoalesce(self) -> None:
1871 try:
1872 self._activateChain()
1873 self.parent.validate()
1874 self._inflateParentForCoalesce()
1875 VDI._doCoalesce(self)
1876 finally:
1877 self.parent.deflate()
1879 def _activateChain(self):
1880 vdi = self
1881 while vdi:
1882 try:
1883 p = self.sr._linstor.get_device_path(vdi.uuid)
1884 except Exception as e:
1885 # Use SMException to skip coalesce.
1886 # Otherwise the GC is stopped...
1887 raise util.SMException(str(e))
1888 vdi = vdi.parent
1890 @override
1891 def _setHidden(self, hidden=True) -> None:
1892 HIDDEN_TAG = 'hidden'
1894 if not VdiType.isCowImage(self.vdi_type):
1895 self._hidden = None
1896 self.sr._linstor.update_volume_metadata(self.uuid, {
1897 HIDDEN_TAG: hidden
1898 })
1899 self._hidden = hidden
1900 else:
1901 VDI._setHidden(self, hidden)
1903 @override
1904 def _increaseSizeVirt(self, size, atomic=True):
1905 if self.vdi_type == VdiType.RAW:
1906 offset = self.drbd_size
1907 if self.sizeVirt < size:
1908 oldSize = self.drbd_size
1909 self.drbd_size = LinstorVolumeManager.round_up_volume_size(size)
1910 Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.drbd_size))
1911 self.sr._linstor.resize_volume(self.uuid, self.drbd_size)
1912 offset = oldSize
1913 unfinishedZero = False
1914 jval = self.sr.journaler.get(LinstorJournaler.ZERO, self.uuid)
1915 if jval:
1916 unfinishedZero = True
1917 offset = int(jval)
1918 length = self.drbd_size - offset
1919 if not length:
1920 return
1922 if unfinishedZero:
1923 Util.log(" ==> Redoing unfinished zeroing out")
1924 else:
1925 self.sr.journaler.create(LinstorJournaler.ZERO, self.uuid, str(offset))
1926 Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
1927 abortTest = lambda: IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
1928 func = lambda: util.zeroOut(self.path, offset, length)
1929 Util.runAbortable(func, True, self.sr.uuid, abortTest, VDI.POLL_INTERVAL, 0)
1930 self.sr.journaler.remove(LinstorJournaler.ZERO, self.uuid)
1931 return
1933 if self.sizeVirt >= size:
1934 return
1935 Util.log(" Expanding COW image virt size for VDI %s: %s -> %s" % \
1936 (self, Util.num2str(self.sizeVirt), Util.num2str(size)))
1938 msize = self.linstorcowutil.get_max_resize_size(self.uuid) * 1024 * 1024
1939 if (size <= msize):
1940 self.linstorcowutil.set_size_virt_fast(self.path, size)
1941 else:
1942 if atomic:
1943 vdiList = self._getAllSubtree()
1944 self.sr.lock()
1945 try:
1946 self.sr.pauseVDIs(vdiList)
1947 try:
1948 self._setSizeVirt(size)
1949 finally:
1950 self.sr.unpauseVDIs(vdiList)
1951 finally:
1952 self.sr.unlock()
1953 else:
1954 self._setSizeVirt(size)
1956 self.sizeVirt = self.linstorcowutil.get_size_virt(self.uuid)
1958 @override
1959 def _setSizeVirt(self, size) -> None:
1960 jfile = self.uuid + '-jvhd'
1961 self.sr._linstor.create_volume(
1962 jfile, self.cowutil.getResizeJournalSize(), persistent=False, volume_name=jfile
1963 )
1964 try:
1965 self.inflate(self.linstorcowutil.compute_volume_size(size))
1966 self.linstorcowutil.set_size_virt(self.path, size, jfile)
1967 finally:
1968 try:
1969 self.sr._linstor.destroy_volume(jfile)
1970 except Exception:
1971 # We can ignore it, in any case this volume is not persistent.
1972 pass
1974 @override
1975 def _queryCowBlocks(self) -> bytes:
1976 return self.linstorcowutil.get_block_bitmap(self.uuid)
1978 def _inflateParentForCoalesce(self):
1979 if not VdiType.isCowImage(self.parent.vdi_type):
1980 return
1981 inc = self._calcExtraSpaceForCoalescing()
1982 if inc > 0:
1983 self.parent.inflate(self.parent.getDrbdSize() + inc)
1985 @override
1986 def _calcExtraSpaceForCoalescing(self) -> int:
1987 if not VdiType.isCowImage(self.parent.vdi_type):
1988 return 0
1989 size_coalesced = self.linstorcowutil.compute_volume_size(self._getCoalescedSizeData())
1990 Util.log("Coalesced size = %s" % Util.num2str(size_coalesced))
1991 return size_coalesced - self.parent.getDrbdSize()
1993 @override
1994 def _calcExtraSpaceForLeafCoalescing(self) -> int:
1995 assert self.getDrbdSize() > 0
1996 assert self.getSizePhys() > 0
1997 deflate_diff = self.getDrbdSize() - LinstorVolumeManager.round_up_volume_size(self.getSizePhys())
1998 assert deflate_diff >= 0
1999 return self._calcExtraSpaceForCoalescing() - deflate_diff
2001 @override
2002 def _calcExtraSpaceForSnapshotCoalescing(self) -> int:
2003 assert self.getSizePhys() > 0
2004 return self._calcExtraSpaceForCoalescing() + \
2005 LinstorVolumeManager.round_up_volume_size(self.getSizePhys())
2007################################################################################
2008#
2009# SR
2010#
2011class SR(object):
2012 class LogFilter:
2013 def __init__(self, sr):
2014 self.sr = sr
2015 self.stateLogged = False
2016 self.prevState = {}
2017 self.currState = {}
2019 def logState(self):
2020 changes = ""
2021 self.currState.clear()
2022 for vdi in self.sr.vdiTrees:
2023 self.currState[vdi.uuid] = self._getTreeStr(vdi)
2024 if not self.prevState.get(vdi.uuid) or \
2025 self.prevState[vdi.uuid] != self.currState[vdi.uuid]:
2026 changes += self.currState[vdi.uuid]
2028 for uuid in self.prevState:
2029 if not self.currState.get(uuid):
2030 changes += "Tree %s gone\n" % uuid
2032 result = "SR %s (%d VDIs in %d COW trees): " % \
2033 (self.sr, len(self.sr.vdis), len(self.sr.vdiTrees))
2035 if len(changes) > 0:
2036 if self.stateLogged:
2037 result += "showing only COW trees that changed:"
2038 result += "\n%s" % changes
2039 else:
2040 result += "no changes"
2042 for line in result.split("\n"):
2043 Util.log("%s" % line)
2044 self.prevState.clear()
2045 for key, val in self.currState.items():
2046 self.prevState[key] = val
2047 self.stateLogged = True
2049 def logNewVDI(self, uuid):
2050 if self.stateLogged:
2051 Util.log("Found new VDI when scanning: %s" % uuid)
2053 def _getTreeStr(self, vdi, indent=8):
2054 treeStr = "%s%s\n" % (" " * indent, vdi)
2055 for child in vdi.children:
2056 treeStr += self._getTreeStr(child, indent + VDI.STR_TREE_INDENT)
2057 return treeStr
2059 TYPE_FILE = "file"
2060 TYPE_LVHD = "lvhd"
2061 TYPE_LINSTOR = "linstor"
2062 TYPES = [TYPE_LVHD, TYPE_FILE, TYPE_LINSTOR]
2064 LOCK_RETRY_INTERVAL = 3
2065 LOCK_RETRY_ATTEMPTS = 20
2066 LOCK_RETRY_ATTEMPTS_LOCK = 100
2068 SCAN_RETRY_ATTEMPTS = 3
2070 JRN_CLONE = "clone" # journal entry type for the clone operation (from SM)
2071 TMP_RENAME_PREFIX = "OLD_"
2073 KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline"
2074 KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override"
2076 @staticmethod
2077 def getInstance(uuid, xapiSession, createLock=True, force=False):
2078 xapi = XAPI(xapiSession, uuid)
2079 type = normalizeType(xapi.srRecord["type"])
2080 if type == SR.TYPE_FILE:
2081 return FileSR(uuid, xapi, createLock, force)
2082 elif type == SR.TYPE_LVHD:
2083 return LVMSR(uuid, xapi, createLock, force)
2084 elif type == SR.TYPE_LINSTOR:
2085 return LinstorSR(uuid, xapi, createLock, force)
2086 raise util.SMException("SR type %s not recognized" % type)
2088 def __init__(self, uuid, xapi, createLock, force):
2089 self.logFilter = self.LogFilter(self)
2090 self.uuid = uuid
2091 self.path = ""
2092 self.name = ""
2093 self.vdis = {}
2094 self.vdiTrees = []
2095 self.journaler = None
2096 self.xapi = xapi
2097 self._locked = 0
2098 self._srLock = None
2099 if createLock: 2099 ↛ 2100line 2099 didn't jump to line 2100, because the condition on line 2099 was never true
2100 self._srLock = lock.Lock(lock.LOCK_TYPE_SR, self.uuid)
2101 else:
2102 Util.log("Requested no SR locking")
2103 self.name = self.xapi.srRecord["name_label"]
2104 self._failedCoalesceTargets = []
2106 if not self.xapi.isPluggedHere():
2107 if force: 2107 ↛ 2108line 2107 didn't jump to line 2108, because the condition on line 2107 was never true
2108 Util.log("SR %s not attached on this host, ignoring" % uuid)
2109 else:
2110 if not self.wait_for_plug():
2111 raise util.SMException("SR %s not attached on this host" % uuid)
2113 if force: 2113 ↛ 2114line 2113 didn't jump to line 2114, because the condition on line 2113 was never true
2114 Util.log("Not checking if we are Master (SR %s)" % uuid)
2115 elif not self.xapi.isMaster(): 2115 ↛ 2116line 2115 didn't jump to line 2116, because the condition on line 2115 was never true
2116 raise util.SMException("This host is NOT master, will not run")
2118 self.no_space_candidates = {}
2120 def msg_cleared(self, xapi_session, msg_ref):
2121 try:
2122 msg = xapi_session.xenapi.message.get_record(msg_ref)
2123 except XenAPI.Failure:
2124 return True
2126 return msg is None
2128 def check_no_space_candidates(self):
2129 with util.ApiSession("SMGC-check-no-space") as xapi_session:
2130 msg_id = self.xapi.srRecord["sm_config"].get(VDI.DB_GC_NO_SPACE)
2131 if self.no_space_candidates:
2132 if msg_id is None or self.msg_cleared(xapi_session, msg_id):
2133 util.SMlog("Could not coalesce due to a lack of space "
2134 f"in SR {self.uuid}")
2135 msg_body = ("Unable to perform data coalesce due to a lack "
2136 f"of space in SR {self.uuid}")
2137 msg_id = xapi_session.xenapi.message.create(
2138 'SM_GC_NO_SPACE',
2139 3,
2140 "SR",
2141 self.uuid,
2142 msg_body)
2143 xapi_session.xenapi.SR.remove_from_sm_config(
2144 self.xapi.srRef, VDI.DB_GC_NO_SPACE)
2145 xapi_session.xenapi.SR.add_to_sm_config(
2146 self.xapi.srRef, VDI.DB_GC_NO_SPACE, msg_id)
2148 for candidate in self.no_space_candidates.values():
2149 candidate.setConfig(VDI.DB_GC_NO_SPACE, msg_id)
2150 elif msg_id is not None:
2151 # Everything was coalescable, remove the message
2152 xapi_session.xenapi.SR.remove_from_sm_config(self.xapi.srRef, VDI.DB_GC_NO_SPACE)
2153 xapi_session.xenapi.message.destroy(msg_id)
2155 def clear_no_space_msg(self, vdi):
2156 msg_id = None
2157 try:
2158 msg_id = vdi.getConfig(VDI.DB_GC_NO_SPACE)
2159 except XenAPI.Failure:
2160 pass
2162 self.no_space_candidates.pop(vdi.uuid, None)
2163 if msg_id is not None: 2163 ↛ exitline 2163 didn't return from function 'clear_no_space_msg', because the condition on line 2163 was never false
2164 vdi.delConfig(VDI.DB_GC_NO_SPACE)
2167 def wait_for_plug(self):
2168 for _ in range(1, 10):
2169 time.sleep(2)
2170 if self.xapi.isPluggedHere():
2171 return True
2172 return False
2174 def gcEnabled(self, refresh=True):
2175 if refresh:
2176 self.xapi.srRecord = \
2177 self.xapi.session.xenapi.SR.get_record(self.xapi._srRef)
2178 if self.xapi.srRecord["other_config"].get(VDI.DB_GC) == "false":
2179 Util.log("GC is disabled for this SR, abort")
2180 return False
2181 return True
2183 def scan(self, force=False) -> None:
2184 """Scan the SR and load VDI info for each VDI. If called repeatedly,
2185 update VDI objects if they already exist"""
2186 pass
2188 def scanLocked(self, force=False):
2189 self.lock()
2190 try:
2191 self.scan(force)
2192 finally:
2193 self.unlock()
2195 def getVDI(self, uuid):
2196 return self.vdis.get(uuid)
2198 def hasWork(self):
2199 if len(self.findGarbage()) > 0:
2200 return True
2201 if self.findCoalesceable():
2202 return True
2203 if self.findLeafCoalesceable():
2204 return True
2205 if self.needUpdateBlockInfo():
2206 return True
2207 return False
2209 def findCoalesceable(self):
2210 """Find a coalesceable VDI. Return a vdi that should be coalesced
2211 (choosing one among all coalesceable candidates according to some
2212 criteria) or None if there is no VDI that could be coalesced"""
2214 candidates = []
2216 srSwitch = self.xapi.srRecord["other_config"].get(VDI.DB_COALESCE)
2217 if srSwitch == "false":
2218 Util.log("Coalesce disabled for this SR")
2219 return candidates
2221 # finish any VDI for which a relink journal entry exists first
2222 journals = self.journaler.getAll(VDI.JRN_RELINK)
2223 for uuid in journals:
2224 vdi = self.getVDI(uuid)
2225 if vdi and vdi not in self._failedCoalesceTargets:
2226 return vdi
2228 for vdi in self.vdis.values():
2229 if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets:
2230 candidates.append(vdi)
2231 Util.log("%s is coalescable" % vdi.uuid)
2233 self.xapi.update_task_progress("coalescable", len(candidates))
2235 # pick one in the tallest tree
2236 treeHeight = dict()
2237 for c in candidates:
2238 height = c.getTreeRoot().getTreeHeight()
2239 if treeHeight.get(height):
2240 treeHeight[height].append(c)
2241 else:
2242 treeHeight[height] = [c]
2244 freeSpace = self.getFreeSpace()
2245 heights = list(treeHeight.keys())
2246 heights.sort(reverse=True)
2247 for h in heights:
2248 for c in treeHeight[h]:
2249 spaceNeeded = c._calcExtraSpaceForCoalescing()
2250 if spaceNeeded <= freeSpace:
2251 Util.log("Coalesce candidate: %s (tree height %d)" % (c, h))
2252 self.clear_no_space_msg(c)
2253 return c
2254 else:
2255 self.no_space_candidates[c.uuid] = c
2256 Util.log("No space to coalesce %s (free space: %d)" % \
2257 (c, freeSpace))
2258 return None
2260 def getSwitch(self, key):
2261 return self.xapi.srRecord["other_config"].get(key)
2263 def forbiddenBySwitch(self, switch, condition, fail_msg):
2264 srSwitch = self.getSwitch(switch)
2265 ret = False
2266 if srSwitch:
2267 ret = srSwitch == condition
2269 if ret:
2270 Util.log(fail_msg)
2272 return ret
2274 def leafCoalesceForbidden(self):
2275 return (self.forbiddenBySwitch(VDI.DB_COALESCE,
2276 "false",
2277 "Coalesce disabled for this SR") or
2278 self.forbiddenBySwitch(VDI.DB_LEAFCLSC,
2279 VDI.LEAFCLSC_DISABLED,
2280 "Leaf-coalesce disabled for this SR"))
2282 def findLeafCoalesceable(self):
2283 """Find leaf-coalesceable VDIs in each COW tree"""
2285 candidates = []
2286 if self.leafCoalesceForbidden():
2287 return candidates
2289 self.gatherLeafCoalesceable(candidates)
2291 self.xapi.update_task_progress("coalescable", len(candidates))
2293 freeSpace = self.getFreeSpace()
2294 for candidate in candidates:
2295 # check the space constraints to see if leaf-coalesce is actually
2296 # feasible for this candidate
2297 spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing()
2298 spaceNeededLive = spaceNeeded
2299 if spaceNeeded > freeSpace:
2300 spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing()
2301 if candidate.canLiveCoalesce(self.getStorageSpeed()):
2302 spaceNeeded = spaceNeededLive
2304 if spaceNeeded <= freeSpace:
2305 Util.log("Leaf-coalesce candidate: %s" % candidate)
2306 self.clear_no_space_msg(candidate)
2307 return candidate
2308 else:
2309 Util.log("No space to leaf-coalesce %s (free space: %d)" % \
2310 (candidate, freeSpace))
2311 if spaceNeededLive <= freeSpace:
2312 Util.log("...but enough space if skip snap-coalesce")
2313 candidate.setConfig(VDI.DB_LEAFCLSC,
2314 VDI.LEAFCLSC_OFFLINE)
2315 self.no_space_candidates[candidate.uuid] = candidate
2317 return None
2319 def gatherLeafCoalesceable(self, candidates):
2320 for vdi in self.vdis.values():
2321 if not vdi.isLeafCoalesceable():
2322 continue
2323 if vdi in self._failedCoalesceTargets:
2324 continue
2325 if vdi.getConfig(vdi.DB_ONBOOT) == vdi.ONBOOT_RESET:
2326 Util.log("Skipping reset-on-boot %s" % vdi)
2327 continue
2328 if vdi.getConfig(vdi.DB_ALLOW_CACHING):
2329 Util.log("Skipping allow_caching=true %s" % vdi)
2330 continue
2331 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED:
2332 Util.log("Leaf-coalesce disabled for %s" % vdi)
2333 continue
2334 if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or
2335 vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE):
2336 continue
2337 candidates.append(vdi)
2339 def coalesce(self, vdi, dryRun=False):
2340 """Coalesce vdi onto parent"""
2341 Util.log("Coalescing %s -> %s" % (vdi, vdi.parent))
2342 if dryRun: 2342 ↛ 2343line 2342 didn't jump to line 2343, because the condition on line 2342 was never true
2343 return
2345 try:
2346 self._coalesce(vdi)
2347 except util.SMException as e:
2348 if isinstance(e, AbortException): 2348 ↛ 2349line 2348 didn't jump to line 2349, because the condition on line 2348 was never true
2349 self.cleanup()
2350 raise
2351 else:
2352 self._failedCoalesceTargets.append(vdi)
2353 Util.logException("coalesce")
2354 Util.log("Coalesce failed, skipping")
2355 self.cleanup()
2357 def coalesceLeaf(self, vdi, dryRun=False):
2358 """Leaf-coalesce vdi onto parent"""
2359 Util.log("Leaf-coalescing %s -> %s" % (vdi, vdi.parent))
2360 if dryRun:
2361 return
2363 try:
2364 uuid = vdi.uuid
2365 try:
2366 # "vdi" object will no longer be valid after this call
2367 if vdi.cowutil.isCoalesceableOnRemote():
2368 Util.log("We will live coalesce leaf: {uuid}".format(uuid=vdi.uuid))
2369 self._liveLeafCoalesce(vdi, coalesce_on_remote=True)
2370 else:
2371 Util.log("We can't live coalesce leaf: {uuid}".format(uuid=vdi.uuid))
2372 self._coalesceLeaf(vdi)
2373 finally:
2374 vdi = self.getVDI(uuid)
2375 if vdi:
2376 vdi.delConfig(vdi.DB_LEAFCLSC)
2377 except AbortException:
2378 self.cleanup()
2379 raise
2380 except (util.SMException, XenAPI.Failure) as e:
2381 self._failedCoalesceTargets.append(vdi)
2382 Util.logException("leaf-coalesce")
2383 Util.log("Leaf-coalesce failed on %s, skipping" % vdi)
2384 self.cleanup()
2386 def garbageCollect(self, dryRun=False):
2387 vdiList = self.findGarbage()
2388 Util.log("Found %d VDIs for deletion:" % len(vdiList))
2389 for vdi in vdiList:
2390 Util.log(" %s" % vdi)
2391 if not dryRun:
2392 self.deleteVDIs(vdiList)
2393 self.cleanupJournals(dryRun)
2395 def findGarbage(self):
2396 vdiList = []
2397 for vdi in self.vdiTrees:
2398 vdiList.extend(vdi.getAllPrunable())
2399 return vdiList
2401 def deleteVDIs(self, vdiList) -> None:
2402 for vdi in vdiList:
2403 if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT):
2404 raise AbortException("Aborting due to signal")
2405 Util.log("Deleting unlinked VDI %s" % vdi)
2406 self.deleteVDI(vdi)
2408 def deleteVDI(self, vdi) -> None:
2409 assert(len(vdi.children) == 0)
2410 del self.vdis[vdi.uuid]
2411 if vdi.parent: 2411 ↛ 2413line 2411 didn't jump to line 2413, because the condition on line 2411 was never false
2412 vdi.parent.children.remove(vdi)
2413 if vdi in self.vdiTrees: 2413 ↛ 2414line 2413 didn't jump to line 2414, because the condition on line 2413 was never true
2414 self.vdiTrees.remove(vdi)
2415 vdi.delete()
2417 def forgetVDI(self, vdiUuid) -> None:
2418 self.xapi.forgetVDI(self.uuid, vdiUuid)
2420 def pauseVDIs(self, vdiList) -> None:
2421 paused = []
2422 failed = False
2423 for vdi in vdiList:
2424 try:
2425 vdi.pause()
2426 paused.append(vdi)
2427 except:
2428 Util.logException("pauseVDIs")
2429 failed = True
2430 break
2432 if failed:
2433 self.unpauseVDIs(paused)
2434 raise util.SMException("Failed to pause VDIs")
2436 def unpauseVDIs(self, vdiList):
2437 failed = False
2438 for vdi in vdiList:
2439 try:
2440 vdi.unpause()
2441 except:
2442 Util.log("ERROR: Failed to unpause VDI %s" % vdi)
2443 failed = True
2444 if failed:
2445 raise util.SMException("Failed to unpause VDIs")
2447 def getFreeSpace(self) -> int:
2448 return 0
2450 def cleanup(self):
2451 Util.log("In cleanup")
2452 return
2454 @override
2455 def __str__(self) -> str:
2456 if self.name:
2457 ret = "%s ('%s')" % (self.uuid[0:4], self.name)
2458 else:
2459 ret = "%s" % self.uuid
2460 return ret
2462 def lock(self):
2463 """Acquire the SR lock. Nested acquire()'s are ok. Check for Abort
2464 signal to avoid deadlocking (trying to acquire the SR lock while the
2465 lock is held by a process that is trying to abort us)"""
2466 if not self._srLock:
2467 return
2469 if self._locked == 0:
2470 abortFlag = IPCFlag(self.uuid)
2471 for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK):
2472 if self._srLock.acquireNoblock():
2473 self._locked += 1
2474 return
2475 if abortFlag.test(FLAG_TYPE_ABORT):
2476 raise AbortException("Abort requested")
2477 time.sleep(SR.LOCK_RETRY_INTERVAL)
2478 raise util.SMException("Unable to acquire the SR lock")
2480 self._locked += 1
2482 def unlock(self):
2483 if not self._srLock: 2483 ↛ 2485line 2483 didn't jump to line 2485, because the condition on line 2483 was never false
2484 return
2485 assert(self._locked > 0)
2486 self._locked -= 1
2487 if self._locked == 0:
2488 self._srLock.release()
2490 def needUpdateBlockInfo(self) -> bool:
2491 for vdi in self.vdis.values():
2492 if vdi.scanError or len(vdi.children) == 0:
2493 continue
2494 if not vdi.getConfig(vdi.DB_VDI_BLOCKS):
2495 return True
2496 return False
2498 def updateBlockInfo(self) -> None:
2499 for vdi in self.vdis.values():
2500 if vdi.scanError or len(vdi.children) == 0:
2501 continue
2502 if not vdi.getConfig(vdi.DB_VDI_BLOCKS):
2503 vdi.updateBlockInfo()
2505 def cleanupCoalesceJournals(self):
2506 """Remove stale coalesce VDI indicators"""
2507 entries = self.journaler.getAll(VDI.JRN_COALESCE)
2508 for uuid, jval in entries.items():
2509 self.journaler.remove(VDI.JRN_COALESCE, uuid)
2511 def cleanupJournals(self, dryRun=False):
2512 """delete journal entries for non-existing VDIs"""
2513 for t in [LVMVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]:
2514 entries = self.journaler.getAll(t)
2515 for uuid, jval in entries.items():
2516 if self.getVDI(uuid):
2517 continue
2518 if t == SR.JRN_CLONE:
2519 baseUuid, clonUuid = jval.split("_")
2520 if self.getVDI(baseUuid):
2521 continue
2522 Util.log(" Deleting stale '%s' journal entry for %s "
2523 "(%s)" % (t, uuid, jval))
2524 if not dryRun:
2525 self.journaler.remove(t, uuid)
2527 def cleanupCache(self, maxAge=-1) -> int:
2528 return 0
2530 def hasLeavesAttachedOn(self, vdi: VDI):
2531 leaves = vdi.getAllLeaves()
2532 leaves_vdi = [leaf.uuid for leaf in leaves]
2533 return util.get_hosts_attached_on_with_vdi_uuid(self.xapi.session, leaves_vdi)
2535 def _gc_running_file(self, vdi: VDI):
2536 run_file = "gc_running_{}".format(vdi.uuid)
2537 return os.path.join(NON_PERSISTENT_DIR, str(self.uuid), run_file)
2539 def _create_running_file(self, vdi: VDI):
2540 with open(self._gc_running_file(vdi), "w") as f:
2541 f.write("1")
2543 def _delete_running_file(self, vdi: VDI):
2544 os.unlink(self._gc_running_file(vdi))
2546 def _coalesce(self, vdi: VDI):
2547 list_not_to_relink = None
2548 if self.journaler.get(vdi.JRN_RELINK, vdi.uuid): 2548 ↛ 2551line 2548 didn't jump to line 2551, because the condition on line 2548 was never true
2549 # this means we had done the actual coalescing already and just
2550 # need to finish relinking and/or refreshing the children
2551 Util.log("==> Coalesce apparently already done: skipping")
2553 # The parent volume must be active for the parent change to occur.
2554 # The parent volume may become inactive if the host is rebooted.
2555 vdi._ensureParentActiveForRelink()
2556 else:
2557 # JRN_COALESCE is used to check which VDI is being coalesced in
2558 # order to decide whether to abort the coalesce. We remove the
2559 # journal as soon as the COW coalesce step is done, because we
2560 # don't expect the rest of the process to take long
2562 if os.path.exists(self._gc_running_file(vdi)): 2562 ↛ 2563line 2562 didn't jump to line 2563, because the condition on line 2562 was never true
2563 util.SMlog("gc_running already exist for {}. Ignoring...".format(self.uuid))
2565 self._create_running_file(vdi)
2567 self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1")
2568 host_refs = self.hasLeavesAttachedOn(vdi)
2569 # This check of multiple host_refs was done earlier in `isCoalesceable` but
2570 # we recheck here to be sure another leaf wasn't activated in the meantime.
2571 if vdi.cowutil.isCoalesceableOnRemote() and len(host_refs) > 1: 2571 ↛ 2572line 2571 didn't jump to line 2572, because the condition on line 2571 was never true
2572 Util.log("Not coalesceable, chain activated more than once")
2573 Util.log(f"VDI '{vdi.uuid}' has leaves attached on {host_refs}")
2574 raise Exception("Not coalesceable, chain activated more than once")
2576 try:
2577 if host_refs and vdi.cowutil.isCoalesceableOnRemote(): 2577 ↛ 2579line 2577 didn't jump to line 2579, because the condition on line 2577 was never true
2578 #Leaf opened on another host, we need to call online coalesce
2579 Util.log("Remote coalesce for {}".format(vdi.path))
2581 leaf_for_coalesce_uuid, host_ref = next(iter(host_refs.items())) # First host_ref since we should only have one
2582 leaf_for_coalesce = [leaf for leaf in vdi.getAllLeaves() if leaf.uuid == leaf_for_coalesce_uuid][0]
2584 vdi._doCoalesceOnHost(host_ref, leaf_for_coalesce)
2585 # If we use a host OpaqueRef to do a online coalesce, this vdi will not need to be relinked since it was done by tapdisk
2586 # If we coalesce up the chain, we shouldn't need to do the relink at all, we only need to do the relink on the children if their direct parent was the one we were coalescing
2587 for child in vdi.children:
2588 real_parent_uuid = child.extractUuid(child.getParent())
2589 if real_parent_uuid == vdi.parent.uuid:
2590 child._update_vhd_parent(real_parent_uuid) # We update the sm-config:vhd-parent value for this VDI since it has already been relinked
2591 list_not_to_relink = [leaf.uuid for leaf in child.getAllLeaves()]
2592 else:
2593 Util.log("Offline coalesce for {}".format(vdi.path))
2594 vdi._doCoalesce()
2595 except Exception as e:
2596 Util.log("EXCEPTION while coalescing: {}".format(e))
2597 self._delete_running_file(vdi)
2598 raise
2600 self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid)
2601 self._delete_running_file(vdi)
2603 util.fistpoint.activate("LVHDRT_before_create_relink_journal", self.uuid)
2605 # we now need to relink the children: lock the SR to prevent ops
2606 # like SM.clone from manipulating the VDIs we'll be relinking and
2607 # rescan the SR first in case the children changed since the last
2608 # scan
2609 self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1")
2611 self.lock()
2612 try:
2613 vdi.parent._tagChildrenForRelink(list_not_to_relink)
2614 self.scan()
2615 vdi._relinkSkip()
2616 finally:
2617 self.unlock()
2618 # Reload the children to leave things consistent
2619 vdi.parent._reloadChildren(vdi)
2620 self.journaler.remove(vdi.JRN_RELINK, vdi.uuid)
2622 self.deleteVDI(vdi)
2624 class CoalesceTracker:
2625 GRACE_ITERATIONS = 2
2626 MAX_ITERATIONS_NO_PROGRESS = 3
2627 MAX_ITERATIONS = 20
2628 MAX_INCREASE_FROM_MINIMUM = 1.2
2629 HISTORY_STRING = "Iteration: {its} -- Initial size {initSize}" \
2630 " --> Final size {finSize}"
2632 def __init__(self, sr):
2633 self.itsNoProgress = 0
2634 self.its = 0
2635 self.minSize = float("inf")
2636 self._history = []
2637 self.reason = ""
2638 self.startSize = None
2639 self.finishSize = None
2640 self.sr = sr
2641 self.grace_remaining = self.GRACE_ITERATIONS
2643 @property
2644 def history(self):
2645 return [x['msg'] for x in self._history]
2647 def moving_average(self):
2648 """
2649 Calculate a three point moving average
2650 """
2651 assert len(self._history) >= 3
2653 mv_average = sum([x['finalsize'] for x in self._history]) / len(self._history)
2654 util.SMlog(f'Calculated moving average as {mv_average}')
2655 return mv_average
2657 def abortCoalesce(self, prevSize, curSize):
2658 self.its += 1
2659 self._history.append(
2660 {
2661 'finalsize': curSize,
2662 'msg': self.HISTORY_STRING.format(its=self.its,
2663 initSize=prevSize,
2664 finSize=curSize)
2665 }
2666 )
2668 self.finishSize = curSize
2670 if self.startSize is None:
2671 self.startSize = prevSize
2673 if curSize < self.minSize:
2674 self.minSize = curSize
2676 if prevSize < self.minSize:
2677 self.minSize = prevSize
2679 if self.its < 4:
2680 # Perform at least three iterations
2681 return False
2683 if prevSize >= curSize or curSize < self.moving_average():
2684 # We made progress
2685 return False
2686 else:
2687 self.itsNoProgress += 1
2688 Util.log("No progress, attempt:"
2689 " {attempt}".format(attempt=self.itsNoProgress))
2690 util.fistpoint.activate("cleanup_tracker_no_progress", self.sr.uuid)
2692 if self.its > self.MAX_ITERATIONS:
2693 max = self.MAX_ITERATIONS
2694 self.reason = \
2695 "Max iterations ({max}) exceeded".format(max=max)
2696 return True
2698 if self.itsNoProgress > self.MAX_ITERATIONS_NO_PROGRESS:
2699 max = self.MAX_ITERATIONS_NO_PROGRESS
2700 self.reason = \
2701 "No progress made for {max} iterations".format(max=max)
2702 return True
2704 maxSizeFromMin = self.MAX_INCREASE_FROM_MINIMUM * self.minSize
2705 if curSize > maxSizeFromMin:
2706 self.grace_remaining -= 1
2707 if self.grace_remaining == 0:
2708 self.reason = "Unexpected bump in size," \
2709 " compared to minimum achieved"
2711 return True
2713 return False
2715 def printSizes(self):
2716 Util.log("Starting size was {size}"
2717 .format(size=self.startSize))
2718 Util.log("Final size was {size}"
2719 .format(size=self.finishSize))
2720 Util.log("Minimum size achieved was {size}"
2721 .format(size=self.minSize))
2723 def printReasoning(self):
2724 Util.log("Aborted coalesce")
2725 for hist in self.history:
2726 Util.log(hist)
2727 Util.log(self.reason)
2728 self.printSizes()
2730 def printSummary(self):
2731 if self.its == 0:
2732 return
2734 if self.reason: 2734 ↛ 2735line 2734 didn't jump to line 2735, because the condition on line 2734 was never true
2735 Util.log("Aborted coalesce")
2736 Util.log(self.reason)
2737 else:
2738 Util.log("Coalesce summary")
2740 Util.log(f"Performed {self.its} iterations")
2741 self.printSizes()
2744 def _coalesceLeaf(self, vdi):
2745 """Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot
2746 complete due to external changes, namely vdi_delete and vdi_snapshot
2747 that alter leaf-coalescibility of vdi"""
2748 tracker = self.CoalesceTracker(self)
2749 while not vdi.canLiveCoalesce(self.getStorageSpeed()):
2750 prevSizePhys = vdi.getSizePhys()
2751 if not self._snapshotCoalesce(vdi): 2751 ↛ 2752line 2751 didn't jump to line 2752, because the condition on line 2751 was never true
2752 return False
2753 if tracker.abortCoalesce(prevSizePhys, vdi.getSizePhys()):
2754 tracker.printReasoning()
2755 raise util.SMException("VDI {uuid} could not be coalesced"
2756 .format(uuid=vdi.uuid))
2757 tracker.printSummary()
2758 return self._liveLeafCoalesce(vdi)
2760 def calcStorageSpeed(self, startTime, endTime, coalescedSize):
2761 speed = None
2762 total_time = endTime - startTime
2763 if total_time > 0:
2764 speed = float(coalescedSize) / float(total_time)
2765 return speed
2767 def writeSpeedToFile(self, speed):
2768 content = []
2769 speedFile = None
2770 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2771 self.lock()
2772 try:
2773 Util.log("Writing to file: {myfile}".format(myfile=path))
2774 lines = ""
2775 if not os.path.isfile(path):
2776 lines = str(speed) + "\n"
2777 else:
2778 speedFile = open(path, "r+")
2779 content = speedFile.readlines()
2780 content.append(str(speed) + "\n")
2781 if len(content) > N_RUNNING_AVERAGE:
2782 del content[0]
2783 lines = "".join(content)
2785 util.atomicFileWrite(path, VAR_RUN, lines)
2786 finally:
2787 if speedFile is not None:
2788 speedFile.close()
2789 Util.log("Closing file: {myfile}".format(myfile=path))
2790 self.unlock()
2792 def recordStorageSpeed(self, startTime, endTime, coalescedSize):
2793 speed = self.calcStorageSpeed(startTime, endTime, coalescedSize)
2794 if speed is None:
2795 return
2797 self.writeSpeedToFile(speed)
2799 def getStorageSpeed(self):
2800 speedFile = None
2801 path = SPEED_LOG_ROOT.format(uuid=self.uuid)
2802 self.lock()
2803 try:
2804 speed = None
2805 if os.path.isfile(path):
2806 speedFile = open(path)
2807 content = speedFile.readlines()
2808 try:
2809 content = [float(i) for i in content]
2810 except ValueError:
2811 Util.log("Something bad in the speed log:{log}".
2812 format(log=speedFile.readlines()))
2813 return speed
2815 if len(content):
2816 speed = sum(content) / float(len(content))
2817 if speed <= 0: 2817 ↛ 2819line 2817 didn't jump to line 2819, because the condition on line 2817 was never true
2818 # Defensive, should be impossible.
2819 Util.log("Bad speed: {speed} calculated for SR: {uuid}".
2820 format(speed=speed, uuid=self.uuid))
2821 speed = None
2822 else:
2823 Util.log("Speed file empty for SR: {uuid}".
2824 format(uuid=self.uuid))
2825 else:
2826 Util.log("Speed log missing for SR: {uuid}".
2827 format(uuid=self.uuid))
2828 return speed
2829 finally:
2830 if not (speedFile is None):
2831 speedFile.close()
2832 self.unlock()
2834 def _snapshotCoalesce(self, vdi):
2835 # Note that because we are not holding any locks here, concurrent SM
2836 # operations may change this tree under our feet. In particular, vdi
2837 # can be deleted, or it can be snapshotted.
2838 assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED)
2839 Util.log("Single-snapshotting %s" % vdi)
2840 util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid)
2841 try:
2842 ret = self.xapi.singleSnapshotVDI(vdi)
2843 Util.log("Single-snapshot returned: %s" % ret)
2844 except XenAPI.Failure as e:
2845 if util.isInvalidVDI(e):
2846 Util.log("The VDI appears to have been concurrently deleted")
2847 return False
2848 raise
2849 self.scanLocked()
2850 tempSnap = vdi.parent
2851 if not tempSnap.isCoalesceable():
2852 Util.log("The VDI appears to have been concurrently snapshotted")
2853 return False
2854 Util.log("Coalescing parent %s" % tempSnap)
2855 util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid)
2856 sizePhys = vdi.getSizePhys()
2857 self._coalesce(tempSnap)
2858 if not vdi.isLeafCoalesceable():
2859 Util.log("The VDI tree appears to have been altered since")
2860 return False
2861 return True
2863 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool:
2864 util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid)
2865 self.lock()
2866 try:
2867 self.scan()
2868 if not self.getVDI(vdi.uuid):
2869 Util.log("The VDI appears to have been deleted meanwhile")
2870 return False
2871 if not vdi.isLeafCoalesceable():
2872 Util.log("The VDI is no longer leaf-coalesceable")
2873 return False
2875 uuid = vdi.uuid
2876 if not coalesce_on_remote:
2877 vdi.pause(failfast=True)
2878 try:
2879 try:
2880 self._create_running_file(vdi)
2881 # "vdi" object will no longer be valid after this call
2882 self._doCoalesceLeaf(vdi, coalesce_on_remote)
2883 except:
2884 Util.logException("_doCoalesceLeaf")
2885 self._handleInterruptedCoalesceLeaf()
2886 raise
2887 finally:
2888 vdi = self.getVDI(uuid)
2889 if vdi:
2890 vdi.ensureUnpaused()
2891 self._delete_running_file(vdi)
2892 vdiOld = self.getVDI(self.TMP_RENAME_PREFIX + uuid)
2893 if vdiOld:
2894 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2895 self.deleteVDI(vdiOld)
2896 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2897 finally:
2898 self.cleanup()
2899 self.unlock()
2900 self.logFilter.logState()
2901 return True
2903 def _doCoalesceLeaf(self, vdi: VDI, coalesce_on_remote: bool):
2904 """Actual coalescing of a leaf VDI onto parent. Must be called in an
2905 offline/atomic context"""
2906 self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid)
2907 self._prepareCoalesceLeaf(vdi)
2908 vdi.parent._setHidden(False)
2909 vdi.parent._increaseSizeVirt(vdi.sizeVirt, False)
2910 host_refs = self.hasLeavesAttachedOn(vdi) if coalesce_on_remote else None
2911 if host_refs: 2911 ↛ 2912line 2911 didn't jump to line 2912, because the condition on line 2911 was never true
2912 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
2913 _, host_ref = next(iter(host_refs.items()))
2914 vdi._coalesceCowImageOnHost(host_ref, vdi) # vdi is the leaf for the online coalesce
2915 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
2916 vdi.pause(failfast=True)
2917 # We make a pause here after the online coalesce but before the rename so we can refresh the chain for tapdisk.
2918 # It's also needed to be paused for the rename on slaves with LVMSR.
2919 # We let the caller `_liveLeafCoalesce` do the unpause with the call to `vdi.ensureUnpaused()`
2920 else:
2921 vdi.validate(True)
2922 vdi.parent.validate(True)
2923 util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
2924 timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT
2925 if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE: 2925 ↛ 2926line 2925 didn't jump to line 2926, because the condition on line 2925 was never true
2926 Util.log("Leaf-coalesce forced, will not use timeout")
2927 timeout = 0
2928 vdi._coalesceCowImage(timeout)
2929 util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
2930 vdi.parent.validate(True)
2931 #vdi._verifyContents(timeout / 2)
2933 # rename
2934 vdiUuid = vdi.uuid
2935 oldName = vdi.fileName
2936 origParentUuid = vdi.parent.uuid
2937 vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid)
2938 util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid)
2939 vdi.parent.rename(vdiUuid)
2940 util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid)
2941 self._updateSlavesOnRename(vdi.parent, oldName, origParentUuid)
2943 # Note that "vdi.parent" is now the single remaining leaf and "vdi" is
2944 # garbage
2946 # update the VDI record
2947 if vdi.parent.vdi_type == VdiType.RAW: 2947 ↛ 2948line 2947 didn't jump to line 2948, because the condition on line 2947 was never true
2948 vdi.parent.setConfig(VDI.DB_VDI_TYPE, VdiType.RAW)
2949 vdi.parent.delConfig(VDI.DB_VDI_BLOCKS)
2950 util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid)
2952 self._updateNode(vdi)
2954 # delete the obsolete leaf & inflate the parent (in that order, to
2955 # minimize free space requirements)
2956 parent = vdi.parent
2957 vdi._setHidden(True)
2958 vdi.parent.children = []
2959 vdi.parent = None
2961 if parent.parent is None:
2962 parent.delConfig(VDI.DB_VDI_PARENT)
2964 extraSpace = self._calcExtraSpaceNeeded(vdi, parent)
2965 freeSpace = self.getFreeSpace()
2966 if freeSpace < extraSpace: 2966 ↛ 2969line 2966 didn't jump to line 2969, because the condition on line 2966 was never true
2967 # don't delete unless we need the space: deletion is time-consuming
2968 # because it requires contacting the slaves, and we're paused here
2969 util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
2970 self.deleteVDI(vdi)
2971 util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
2973 util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid)
2974 self.journaler.remove(VDI.JRN_LEAF, vdiUuid)
2976 self.forgetVDI(origParentUuid)
2977 self._finishCoalesceLeaf(parent)
2978 self._updateSlavesOnResize(parent)
2980 def _calcExtraSpaceNeeded(self, child, parent) -> int:
2981 assert(VdiType.isCowImage(parent.vdi_type))
2982 extra = child.getSizePhys() - parent.getSizePhys()
2983 if extra < 0: 2983 ↛ 2984line 2983 didn't jump to line 2984, because the condition on line 2983 was never true
2984 extra = 0
2985 return extra
2987 def _prepareCoalesceLeaf(self, vdi) -> None:
2988 pass
2990 def _updateNode(self, vdi) -> None:
2991 pass
2993 def _finishCoalesceLeaf(self, parent) -> None:
2994 pass
2996 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
2997 pass
2999 def _updateSlavesOnRename(self, vdi, oldName, origParentUuid) -> None:
3000 pass
3002 def _updateSlavesOnResize(self, vdi) -> None:
3003 pass
3005 def _removeStaleVDIs(self, uuidsPresent) -> None:
3006 for uuid in list(self.vdis.keys()):
3007 if not uuid in uuidsPresent:
3008 Util.log("VDI %s disappeared since last scan" % \
3009 self.vdis[uuid])
3010 del self.vdis[uuid]
3012 def _handleInterruptedCoalesceLeaf(self) -> None:
3013 """An interrupted leaf-coalesce operation may leave the COW tree in an
3014 inconsistent state. If the old-leaf VDI is still present, we revert the
3015 operation (in case the original error is persistent); otherwise we must
3016 finish the operation"""
3017 pass
3019 def _buildTree(self, force):
3020 self.vdiTrees = []
3021 for vdi in self.vdis.values():
3022 if vdi.parentUuid:
3023 parent = self.getVDI(vdi.parentUuid)
3024 if not parent:
3025 if vdi.uuid.startswith(self.TMP_RENAME_PREFIX):
3026 self.vdiTrees.append(vdi)
3027 continue
3028 if force:
3029 Util.log("ERROR: Parent VDI %s not found! (for %s)" % \
3030 (vdi.parentUuid, vdi.uuid))
3031 self.vdiTrees.append(vdi)
3032 continue
3033 else:
3034 raise util.SMException("Parent VDI %s of %s not " \
3035 "found" % (vdi.parentUuid, vdi.uuid))
3036 vdi.parent = parent
3037 parent.children.append(vdi)
3038 else:
3039 self.vdiTrees.append(vdi)
3042class FileSR(SR):
3043 TYPE = SR.TYPE_FILE
3044 CACHE_FILE_EXT = ".vhdcache"
3045 # cache cleanup actions
3046 CACHE_ACTION_KEEP = 0
3047 CACHE_ACTION_REMOVE = 1
3048 CACHE_ACTION_REMOVE_IF_INACTIVE = 2
3050 def __init__(self, uuid, xapi, createLock, force):
3051 SR.__init__(self, uuid, xapi, createLock, force)
3052 self.path = "/var/run/sr-mount/%s" % self.uuid
3053 self.journaler = fjournaler.Journaler(self.path)
3055 @override
3056 def scan(self, force=False) -> None:
3057 if not util.pathexists(self.path):
3058 raise util.SMException("directory %s not found!" % self.uuid)
3060 uuidsPresent: List[str] = []
3062 for vdi_type in VDI_COW_TYPES:
3063 scan_result = self._scan(vdi_type, force)
3064 for uuid, image_info in scan_result.items():
3065 vdi = self.getVDI(uuid)
3066 if not vdi:
3067 self.logFilter.logNewVDI(uuid)
3068 vdi = FileVDI(self, uuid, vdi_type)
3069 self.vdis[uuid] = vdi
3070 vdi.load(image_info)
3071 uuidsPresent.extend(scan_result.keys())
3073 rawList = [x for x in os.listdir(self.path) if x.endswith(VdiTypeExtension.RAW)]
3074 for rawName in rawList:
3075 uuid = FileVDI.extractUuid(rawName)
3076 uuidsPresent.append(uuid)
3077 vdi = self.getVDI(uuid)
3078 if not vdi:
3079 self.logFilter.logNewVDI(uuid)
3080 vdi = FileVDI(self, uuid, VdiType.RAW)
3081 self.vdis[uuid] = vdi
3082 self._removeStaleVDIs(uuidsPresent)
3083 self._buildTree(force)
3084 self.logFilter.logState()
3085 self._handleInterruptedCoalesceLeaf()
3087 @override
3088 def getFreeSpace(self) -> int:
3089 return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path)
3091 @override
3092 def deleteVDIs(self, vdiList) -> None:
3093 rootDeleted = False
3094 for vdi in vdiList:
3095 if not vdi.parent:
3096 rootDeleted = True
3097 break
3098 SR.deleteVDIs(self, vdiList)
3099 if self.xapi.srRecord["type"] == "nfs" and rootDeleted:
3100 self.xapi.markCacheSRsDirty()
3102 @override
3103 def cleanupCache(self, maxAge=-1) -> int:
3104 """Clean up IntelliCache cache files. Caches for leaf nodes are
3105 removed when the leaf node no longer exists or its allow-caching
3106 attribute is not set. Caches for parent nodes are removed when the
3107 parent node no longer exists or it hasn't been used in more than
3108 <maxAge> hours.
3109 Return number of caches removed.
3110 """
3111 numRemoved = 0
3112 cacheFiles = [x for x in os.listdir(self.path) if self._isCacheFileName(x)]
3113 Util.log("Found %d cache files" % len(cacheFiles))
3114 cutoff = datetime.datetime.now() - datetime.timedelta(hours=maxAge)
3115 for cacheFile in cacheFiles:
3116 uuid = cacheFile[:-len(self.CACHE_FILE_EXT)]
3117 action = self.CACHE_ACTION_KEEP
3118 rec = self.xapi.getRecordVDI(uuid)
3119 if not rec:
3120 Util.log("Cache %s: VDI doesn't exist" % uuid)
3121 action = self.CACHE_ACTION_REMOVE
3122 elif rec["managed"] and not rec["allow_caching"]:
3123 Util.log("Cache %s: caching disabled" % uuid)
3124 action = self.CACHE_ACTION_REMOVE
3125 elif not rec["managed"] and maxAge >= 0:
3126 lastAccess = datetime.datetime.fromtimestamp( \
3127 os.path.getatime(os.path.join(self.path, cacheFile)))
3128 if lastAccess < cutoff:
3129 Util.log("Cache %s: older than %d hrs" % (uuid, maxAge))
3130 action = self.CACHE_ACTION_REMOVE_IF_INACTIVE
3132 if action == self.CACHE_ACTION_KEEP:
3133 Util.log("Keeping cache %s" % uuid)
3134 continue
3136 lockId = uuid
3137 parentUuid = None
3138 if rec and rec["managed"]:
3139 parentUuid = rec["sm_config"].get("vhd-parent")
3140 if parentUuid:
3141 lockId = parentUuid
3143 cacheLock = lock.Lock(blktap2.VDI.LOCK_CACHE_SETUP, lockId)
3144 cacheLock.acquire()
3145 try:
3146 if self._cleanupCache(uuid, action):
3147 numRemoved += 1
3148 finally:
3149 cacheLock.release()
3150 return numRemoved
3152 def _cleanupCache(self, uuid, action):
3153 assert(action != self.CACHE_ACTION_KEEP)
3154 rec = self.xapi.getRecordVDI(uuid)
3155 if rec and rec["allow_caching"]:
3156 Util.log("Cache %s appears to have become valid" % uuid)
3157 return False
3159 fullPath = os.path.join(self.path, uuid + self.CACHE_FILE_EXT)
3160 tapdisk = blktap2.Tapdisk.find_by_path(fullPath)
3161 if tapdisk:
3162 if action == self.CACHE_ACTION_REMOVE_IF_INACTIVE:
3163 Util.log("Cache %s still in use" % uuid)
3164 return False
3165 Util.log("Shutting down tapdisk for %s" % fullPath)
3166 tapdisk.shutdown()
3168 Util.log("Deleting file %s" % fullPath)
3169 os.unlink(fullPath)
3170 return True
3172 def _isCacheFileName(self, name):
3173 return (len(name) == Util.UUID_LEN + len(self.CACHE_FILE_EXT)) and \
3174 name.endswith(self.CACHE_FILE_EXT)
3176 def _scan(self, vdi_type, force):
3177 for i in range(SR.SCAN_RETRY_ATTEMPTS):
3178 error = False
3179 pattern = os.path.join(self.path, "*%s" % VDI_TYPE_TO_EXTENSION[vdi_type])
3180 scan_result = getCowUtil(vdi_type).getAllInfoFromVG(pattern, FileVDI.extractUuid)
3181 for uuid, vdiInfo in scan_result.items():
3182 if vdiInfo.error:
3183 error = True
3184 break
3185 if not error:
3186 return scan_result
3187 Util.log("Scan error on attempt %d" % i)
3188 if force:
3189 return scan_result
3190 raise util.SMException("Scan error")
3192 @override
3193 def deleteVDI(self, vdi) -> None:
3194 self._checkSlaves(vdi)
3195 SR.deleteVDI(self, vdi)
3197 def _checkSlaves(self, vdi):
3198 onlineHosts = self.xapi.getOnlineHosts()
3199 abortFlag = IPCFlag(self.uuid)
3200 for pbdRecord in self.xapi.getAttachedPBDs():
3201 hostRef = pbdRecord["host"]
3202 if hostRef == self.xapi._hostRef:
3203 continue
3204 if abortFlag.test(FLAG_TYPE_ABORT):
3205 raise AbortException("Aborting due to signal")
3206 try:
3207 self._checkSlave(hostRef, vdi)
3208 except XenAPI.Failure:
3209 if hostRef in onlineHosts:
3210 raise
3212 def _checkSlave(self, hostRef, vdi):
3213 call = (hostRef, "nfs-on-slave", "check", {'path': vdi.path})
3214 Util.log("Checking with slave: %s" % repr(call))
3215 _host = self.xapi.session.xenapi.host
3216 text = _host.call_plugin( * call)
3218 @override
3219 def _handleInterruptedCoalesceLeaf(self) -> None:
3220 entries = self.journaler.getAll(VDI.JRN_LEAF)
3221 extensions = tuple(VDI_TYPE_TO_EXTENSION[vdi_type] for vdi_type in VDI_COW_TYPES)
3222 srRoot = Path(self.path)
3224 for uuid, parentUuid in entries.items():
3225 hasCowPath = False
3227 for extension in extensions:
3228 hasCowPath = (
3229 (srRoot / (self.TMP_RENAME_PREFIX + uuid + extension)).exists() or
3230 (srRoot / (parentUuid + extension)).exists()
3231 )
3233 if hasCowPath:
3234 break
3236 if hasCowPath or (srRoot / (parentUuid + VdiTypeExtension.RAW)).exists():
3237 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3238 else:
3239 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3241 self.journaler.remove(VDI.JRN_LEAF, uuid)
3243 vdi = self.getVDI(uuid)
3244 if vdi:
3245 vdi.ensureUnpaused()
3247 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3248 Util.log("*** UNDO LEAF-COALESCE")
3249 parent = self.getVDI(parentUuid)
3250 if not parent:
3251 parent = self.getVDI(childUuid)
3252 if not parent:
3253 raise util.SMException("Neither %s nor %s found" % \
3254 (parentUuid, childUuid))
3255 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
3256 parent.rename(parentUuid)
3257 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
3259 child = self.getVDI(childUuid)
3260 if not child:
3261 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3262 if not child:
3263 raise util.SMException("Neither %s nor %s found" % \
3264 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
3265 Util.log("Renaming child back to %s" % childUuid)
3266 child.rename(childUuid)
3267 Util.log("Updating the VDI record")
3268 child.setConfig(VDI.DB_VDI_PARENT, parentUuid)
3269 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type)
3270 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
3272 if child.isHidden():
3273 child._setHidden(False)
3274 if not parent.isHidden():
3275 parent._setHidden(True)
3276 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3277 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
3278 Util.log("*** leaf-coalesce undo successful")
3279 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
3280 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
3282 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3283 Util.log("*** FINISH LEAF-COALESCE")
3284 vdi = self.getVDI(childUuid)
3285 if not vdi:
3286 Util.log(f"_finishInterruptedCoalesceLeaf, vdi {childUuid} not found, aborting")
3287 raise util.SMException("VDI %s not found" % childUuid)
3288 try:
3289 self.forgetVDI(parentUuid)
3290 except XenAPI.Failure:
3291 Util.logException('_finishInterruptedCoalesceLeaf')
3292 pass
3293 self._updateSlavesOnResize(vdi)
3294 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
3295 Util.log("*** finished leaf-coalesce successfully")
3298class LVMSR(SR):
3299 TYPE = SR.TYPE_LVHD
3300 SUBTYPES = ["lvhdoiscsi", "lvhdohba"]
3302 def __init__(self, uuid, xapi, createLock, force):
3303 SR.__init__(self, uuid, xapi, createLock, force)
3304 self.vgName = "%s%s" % (VG_PREFIX, self.uuid)
3305 self.path = os.path.join(VG_LOCATION, self.vgName)
3307 sr_ref = self.xapi.session.xenapi.SR.get_by_uuid(self.uuid)
3308 other_conf = self.xapi.session.xenapi.SR.get_other_config(sr_ref)
3309 lvm_conf = other_conf.get('lvm-conf') if other_conf else None
3310 self.lvmCache = lvmcache.LVMCache(self.vgName, lvm_conf)
3312 self.lvActivator = LVActivator(self.uuid, self.lvmCache)
3313 self.journaler = journaler.Journaler(self.lvmCache)
3315 @override
3316 def deleteVDI(self, vdi) -> None:
3317 if self.lvActivator.get(vdi.uuid, False):
3318 self.lvActivator.deactivate(vdi.uuid, False)
3319 self._checkSlaves(vdi)
3320 SR.deleteVDI(self, vdi)
3322 @override
3323 def forgetVDI(self, vdiUuid) -> None:
3324 SR.forgetVDI(self, vdiUuid)
3325 mdpath = os.path.join(self.path, lvutil.MDVOLUME_NAME)
3326 LVMMetadataHandler(mdpath).deleteVdiFromMetadata(vdiUuid)
3328 @override
3329 def getFreeSpace(self) -> int:
3330 stats = lvutil._getVGstats(self.vgName)
3331 return stats['physical_size'] - stats['physical_utilisation']
3333 @override
3334 def cleanup(self):
3335 if not self.lvActivator.deactivateAll():
3336 Util.log("ERROR deactivating LVs while cleaning up")
3338 @override
3339 def needUpdateBlockInfo(self) -> bool:
3340 for vdi in self.vdis.values():
3341 if vdi.scanError or not VdiType.isCowImage(vdi.vdi_type) or len(vdi.children) == 0:
3342 continue
3343 if not vdi.getConfig(vdi.DB_VDI_BLOCKS):
3344 return True
3345 return False
3347 @override
3348 def updateBlockInfo(self) -> None:
3349 numUpdated = 0
3350 for vdi in self.vdis.values():
3351 if vdi.scanError or not VdiType.isCowImage(vdi.vdi_type) or len(vdi.children) == 0:
3352 continue
3353 if not vdi.getConfig(vdi.DB_VDI_BLOCKS):
3354 vdi.updateBlockInfo()
3355 numUpdated += 1
3356 if numUpdated:
3357 # deactivate the LVs back sooner rather than later. If we don't
3358 # now, by the time this thread gets to deactivations, another one
3359 # might have leaf-coalesced a node and deleted it, making the child
3360 # inherit the refcount value and preventing the correct decrement
3361 self.cleanup()
3363 @override
3364 def scan(self, force=False) -> None:
3365 vdis = self._scan(force)
3366 for uuid, vdiInfo in vdis.items():
3367 vdi = self.getVDI(uuid)
3368 if not vdi:
3369 self.logFilter.logNewVDI(uuid)
3370 vdi = LVMVDI(self, uuid, vdiInfo.vdiType)
3371 self.vdis[uuid] = vdi
3372 vdi.load(vdiInfo)
3373 self._removeStaleVDIs(vdis.keys())
3374 self._buildTree(force)
3375 self.logFilter.logState()
3376 self._handleInterruptedCoalesceLeaf()
3378 def _scan(self, force):
3379 for i in range(SR.SCAN_RETRY_ATTEMPTS):
3380 error = False
3381 self.lvmCache.refresh()
3382 vdis = LvmCowUtil.getVDIInfo(self.lvmCache)
3383 for uuid, vdiInfo in vdis.items():
3384 if vdiInfo.scanError:
3385 error = True
3386 break
3387 if not error:
3388 return vdis
3389 Util.log("Scan error, retrying (%d)" % i)
3390 if force:
3391 return vdis
3392 raise util.SMException("Scan error")
3394 @override
3395 def _removeStaleVDIs(self, uuidsPresent) -> None:
3396 for uuid in list(self.vdis.keys()):
3397 if not uuid in uuidsPresent:
3398 Util.log("VDI %s disappeared since last scan" % \
3399 self.vdis[uuid])
3400 del self.vdis[uuid]
3401 if self.lvActivator.get(uuid, False):
3402 self.lvActivator.remove(uuid, False)
3404 @override
3405 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool:
3406 """If the parent is raw and the child was resized (virt. size), then
3407 we'll need to resize the parent, which can take a while due to zeroing
3408 out of the extended portion of the LV. Do it before pausing the child
3409 to avoid a protracted downtime"""
3410 if not VdiType.isCowImage(vdi.parent.vdi_type) and vdi.sizeVirt > vdi.parent.sizeVirt:
3411 self.lvmCache.setReadonly(vdi.parent.fileName, False)
3412 vdi.parent._increaseSizeVirt(vdi.sizeVirt)
3414 return SR._liveLeafCoalesce(self, vdi, coalesce_on_remote)
3416 @override
3417 def _prepareCoalesceLeaf(self, vdi) -> None:
3418 vdi._activateChain()
3419 self.lvmCache.setReadonly(vdi.parent.fileName, False)
3420 vdi.deflate()
3421 vdi.inflateParentForCoalesce()
3423 @override
3424 def _updateNode(self, vdi) -> None:
3425 # fix the refcounts: the remaining node should inherit the binary
3426 # refcount from the leaf (because if it was online, it should remain
3427 # refcounted as such), but the normal refcount from the parent (because
3428 # this node is really the parent node) - minus 1 if it is online (since
3429 # non-leaf nodes increment their normal counts when they are online and
3430 # we are now a leaf, storing that 1 in the binary refcount).
3431 ns = NS_PREFIX_LVM + self.uuid
3432 cCnt, cBcnt = RefCounter.check(vdi.uuid, ns)
3433 pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns)
3434 pCnt = pCnt - cBcnt
3435 assert(pCnt >= 0)
3436 RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns)
3438 @override
3439 def _finishCoalesceLeaf(self, parent) -> None:
3440 if not parent.isSnapshot() or parent.isAttachedRW():
3441 parent.inflateFully()
3442 else:
3443 parent.deflate()
3445 @override
3446 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3447 return parent.lvmcowutil.calcVolumeSize(parent.sizeVirt) - parent.sizeLV
3449 @override
3450 def _handleInterruptedCoalesceLeaf(self) -> None:
3451 entries = self.journaler.getAll(VDI.JRN_LEAF)
3452 for uuid, parentUuid in entries.items():
3453 undo = False
3454 for prefix in LV_PREFIX.values():
3455 parentLV = prefix + parentUuid
3456 undo = self.lvmCache.checkLV(parentLV)
3457 if undo:
3458 break
3460 if not undo:
3461 for prefix in LV_PREFIX.values():
3462 tmpChildLV = prefix + uuid
3463 undo = self.lvmCache.checkLV(tmpChildLV)
3464 if undo:
3465 break
3467 if undo:
3468 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3469 else:
3470 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3471 self.journaler.remove(VDI.JRN_LEAF, uuid)
3472 vdi = self.getVDI(uuid)
3473 if vdi:
3474 vdi.ensureUnpaused()
3476 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3477 Util.log("*** UNDO LEAF-COALESCE")
3478 parent = self.getVDI(parentUuid)
3479 if not parent:
3480 parent = self.getVDI(childUuid)
3481 if not parent:
3482 raise util.SMException("Neither %s nor %s found" % \
3483 (parentUuid, childUuid))
3484 Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
3485 parent.rename(parentUuid)
3486 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
3488 child = self.getVDI(childUuid)
3489 if not child:
3490 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3491 if not child:
3492 raise util.SMException("Neither %s nor %s found" % \
3493 (childUuid, self.TMP_RENAME_PREFIX + childUuid))
3494 Util.log("Renaming child back to %s" % childUuid)
3495 child.rename(childUuid)
3496 Util.log("Updating the VDI record")
3497 child.setConfig(VDI.DB_VDI_PARENT, parentUuid)
3498 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type)
3499 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
3501 # refcount (best effort - assume that it had succeeded if the
3502 # second rename succeeded; if not, this adjustment will be wrong,
3503 # leading to a non-deactivation of the LV)
3504 ns = NS_PREFIX_LVM + self.uuid
3505 cCnt, cBcnt = RefCounter.check(child.uuid, ns)
3506 pCnt, pBcnt = RefCounter.check(parent.uuid, ns)
3507 pCnt = pCnt + cBcnt
3508 RefCounter.set(parent.uuid, pCnt, 0, ns)
3509 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid)
3511 parent.deflate()
3512 child.inflateFully()
3513 util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid)
3514 if child.isHidden():
3515 child._setHidden(False)
3516 if not parent.isHidden():
3517 parent._setHidden(True)
3518 if not parent.lvReadonly:
3519 self.lvmCache.setReadonly(parent.fileName, True)
3520 child.ensurePaused()
3521 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3522 util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
3523 Util.log("*** leaf-coalesce undo successful")
3524 if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
3525 child.setConfig(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
3527 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3528 Util.log("*** FINISH LEAF-COALESCE")
3529 vdi = self.getVDI(childUuid)
3530 if not vdi:
3531 raise util.SMException("VDI %s not found" % childUuid)
3532 vdi.inflateFully()
3533 util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid)
3534 try:
3535 self.forgetVDI(parentUuid)
3536 except XenAPI.Failure:
3537 pass
3538 self._updateSlavesOnResize(vdi)
3539 util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
3540 Util.log("*** finished leaf-coalesce successfully")
3542 def _checkSlaves(self, vdi):
3543 """Confirm with all slaves in the pool that 'vdi' is not in use. We
3544 try to check all slaves, including those that the Agent believes are
3545 offline, but ignore failures for offline hosts. This is to avoid cases
3546 where the Agent thinks a host is offline but the host is up."""
3547 args = {"vgName": self.vgName,
3548 "action1": "deactivateNoRefcount",
3549 "lvName1": vdi.fileName,
3550 "action2": "cleanupLockAndRefcount",
3551 "uuid2": vdi.uuid,
3552 "ns2": NS_PREFIX_LVM + self.uuid}
3553 onlineHosts = self.xapi.getOnlineHosts()
3554 abortFlag = IPCFlag(self.uuid)
3555 for pbdRecord in self.xapi.getAttachedPBDs():
3556 hostRef = pbdRecord["host"]
3557 if hostRef == self.xapi._hostRef:
3558 continue
3559 if abortFlag.test(FLAG_TYPE_ABORT):
3560 raise AbortException("Aborting due to signal")
3561 Util.log("Checking with slave %s (path %s)" % (
3562 self.xapi.getRecordHost(hostRef)['hostname'], vdi.path))
3563 try:
3564 self.xapi.ensureInactive(hostRef, args)
3565 except XenAPI.Failure:
3566 if hostRef in onlineHosts:
3567 raise
3569 @override
3570 def _updateSlavesOnUndoLeafCoalesce(self, parent, child) -> None:
3571 slaves = util.get_slaves_attached_on(self.xapi.session, [child.uuid])
3572 if not slaves:
3573 Util.log("Update-on-leaf-undo: VDI %s not attached on any slave" % \
3574 child)
3575 return
3577 tmpName = LV_PREFIX[child.vdi_type] + self.TMP_RENAME_PREFIX + child.uuid
3578 args = {"vgName": self.vgName,
3579 "action1": "deactivateNoRefcount",
3580 "lvName1": tmpName,
3581 "action2": "deactivateNoRefcount",
3582 "lvName2": child.fileName,
3583 "action3": "refresh",
3584 "lvName3": child.fileName,
3585 "action4": "refresh",
3586 "lvName4": parent.fileName}
3587 for slave in slaves:
3588 Util.log("Updating %s, %s, %s on slave %s" % \
3589 (tmpName, child.fileName, parent.fileName,
3590 self.xapi.getRecordHost(slave)['hostname']))
3591 text = self.xapi.session.xenapi.host.call_plugin( \
3592 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3593 Util.log("call-plugin returned: '%s'" % text)
3595 @override
3596 def _updateSlavesOnRename(self, vdi, oldNameLV, origParentUuid) -> None:
3597 slaves = util.get_slaves_attached_on(self.xapi.session, [vdi.uuid])
3598 if not slaves:
3599 Util.log("Update-on-rename: VDI %s not attached on any slave" % vdi)
3600 return
3602 args = {"vgName": self.vgName,
3603 "action1": "deactivateNoRefcount",
3604 "lvName1": oldNameLV,
3605 "action2": "refresh",
3606 "lvName2": vdi.fileName,
3607 "action3": "cleanupLockAndRefcount",
3608 "uuid3": origParentUuid,
3609 "ns3": NS_PREFIX_LVM + self.uuid}
3610 for slave in slaves:
3611 Util.log("Updating %s to %s on slave %s" % \
3612 (oldNameLV, vdi.fileName,
3613 self.xapi.getRecordHost(slave)['hostname']))
3614 text = self.xapi.session.xenapi.host.call_plugin( \
3615 slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
3616 Util.log("call-plugin returned: '%s'" % text)
3618 @override
3619 def _updateSlavesOnResize(self, vdi) -> None:
3620 uuids = [x.uuid for x in vdi.getAllLeaves()]
3621 slaves = util.get_slaves_attached_on(self.xapi.session, uuids)
3622 if not slaves:
3623 util.SMlog("Update-on-resize: %s not attached on any slave" % vdi)
3624 return
3625 LvmCowUtil.refreshVolumeOnSlaves(self.xapi.session, self.uuid, self.vgName,
3626 vdi.fileName, vdi.uuid, slaves)
3629class LinstorSR(SR):
3630 TYPE = SR.TYPE_LINSTOR
3632 def __init__(self, uuid, xapi, createLock, force):
3633 if not LINSTOR_AVAILABLE:
3634 raise util.SMException(
3635 'Can\'t load cleanup LinstorSR: LINSTOR libraries are missing'
3636 )
3638 SR.__init__(self, uuid, xapi, createLock, force)
3639 self.path = LinstorVolumeManager.DEV_ROOT_PATH
3641 class LinstorProxy:
3642 def __init__(self, sr: LinstorSR) -> None:
3643 self.sr = sr
3645 def __getattr__(self, attr: str) -> Any:
3646 assert self.sr, "Cannot use `LinstorProxy` without valid `LinstorVolumeManager` instance"
3647 return getattr(self.sr._linstor, attr)
3649 self._linstor = None
3650 self._linstor_proxy = LinstorProxy(self)
3651 self._reloadLinstor(journaler_only=True)
3653 @override
3654 def deleteVDI(self, vdi) -> None:
3655 self._checkSlaves(vdi)
3656 SR.deleteVDI(self, vdi)
3658 @override
3659 def getFreeSpace(self) -> int:
3660 return self._linstor.max_volume_size_allowed
3662 @override
3663 def scan(self, force=False) -> None:
3664 all_vdi_info = self._scan(force)
3665 for uuid, vdiInfo in all_vdi_info.items():
3666 # When vdiInfo is None, the VDI is RAW.
3667 vdi = self.getVDI(uuid)
3668 if not vdi:
3669 self.logFilter.logNewVDI(uuid)
3670 vdi = LinstorVDI(self, uuid, vdiInfo.vdiType if vdiInfo else VdiType.RAW)
3671 self.vdis[uuid] = vdi
3672 if vdiInfo:
3673 vdi.load(vdiInfo)
3674 self._removeStaleVDIs(all_vdi_info.keys())
3675 self._buildTree(force)
3676 self.logFilter.logState()
3677 self._handleInterruptedCoalesceLeaf()
3679 @override
3680 def pauseVDIs(self, vdiList) -> None:
3681 self._linstor.ensure_volume_list_is_not_locked(
3682 vdiList, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3683 )
3684 return super(LinstorSR, self).pauseVDIs(vdiList)
3686 def _reloadLinstor(self, journaler_only=False):
3687 session = self.xapi.session
3688 host_ref = util.get_this_host_ref(session)
3689 sr_ref = session.xenapi.SR.get_by_uuid(self.uuid)
3691 pbd = util.find_my_pbd(session, host_ref, sr_ref)
3692 if pbd is None:
3693 raise util.SMException('Failed to find PBD')
3695 dconf = session.xenapi.PBD.get_device_config(pbd)
3696 group_name = dconf['group-name']
3698 controller_uri = get_controller_uri()
3700 if not journaler_only:
3701 self._linstor = LinstorVolumeManager(
3702 controller_uri,
3703 group_name,
3704 repair=True,
3705 logger=util.SMlog
3706 )
3708 self.journaler = LinstorJournaler(
3709 group_name,
3710 uri=None if self._linstor else controller_uri,
3711 native_client=self._linstor.native_client if self._linstor else None,
3712 logger=util.SMlog
3713 )
3715 def _scan(self, force):
3716 for i in range(SR.SCAN_RETRY_ATTEMPTS):
3717 self._reloadLinstor()
3718 error = False
3719 try:
3720 all_vdi_info = self._load_vdi_info()
3721 for uuid, vdiInfo in all_vdi_info.items():
3722 if vdiInfo and vdiInfo.error:
3723 error = True
3724 break
3725 if not error:
3726 return all_vdi_info
3727 Util.log('Scan error, retrying ({})'.format(i))
3728 except Exception as e:
3729 Util.log('Scan exception, retrying ({}): {}'.format(i, e))
3730 Util.log(traceback.format_exc())
3732 if force:
3733 return all_vdi_info
3734 raise util.SMException('Scan error')
3736 def _load_vdi_info(self):
3737 all_volume_info = self._linstor.get_volumes_with_info()
3738 volumes_metadata = self._linstor.get_volumes_with_metadata()
3740 all_vdi_info = {}
3741 pending_vdis = []
3743 def handle_fail(vdi_uuid, e):
3744 Util.log(f" [VDI {vdi_uuid}: failed to load VDI info]: {e}")
3745 info = CowImageInfo(vdi_uuid)
3746 info.error = 1
3747 return info
3749 for vdi_uuid, volume_info in all_volume_info.items():
3750 vdi_type = VdiType.RAW
3751 try:
3752 volume_metadata = volumes_metadata[vdi_uuid]
3753 if not volume_info.name and not list(volume_metadata.items()):
3754 continue # Ignore it, probably deleted.
3756 if vdi_uuid.startswith('DELETED_'):
3757 # Assume it's really a RAW volume of a failed snap without COW header/footer.
3758 # We must remove this VDI now without adding it in the VDI list.
3759 # Otherwise `Relinking` calls and other actions can be launched on it.
3760 # We don't want that...
3761 Util.log('Deleting bad VDI {}'.format(vdi_uuid))
3763 self.lock()
3764 try:
3765 self._linstor.destroy_volume(vdi_uuid)
3766 try:
3767 self.forgetVDI(vdi_uuid)
3768 except:
3769 pass
3770 except Exception as e:
3771 Util.log('Cannot delete bad VDI: {}'.format(e))
3772 finally:
3773 self.unlock()
3774 continue
3776 vdi_type = volume_metadata.get(VDI_TYPE_TAG)
3777 if VdiType.isCowImage(vdi_type):
3778 pending_vdis.append((vdi_uuid, vdi_type))
3779 else:
3780 all_vdi_info[vdi_uuid] = None
3781 except Exception as e:
3782 all_vdi_info[vdi_uuid] = handle_fail(vdi_uuid, e)
3784 multi_cowutil = MultiLinstorCowUtil(self._linstor.uri, self._linstor.group_name)
3786 def load_info(vdi, multi_cowutil):
3787 vdi_uuid, vdi_type = vdi
3788 try:
3789 vdiInfo = multi_cowutil.get_local_cowutil(vdi_type).get_info(vdi_uuid)
3790 except Exception as e:
3791 vdiInfo = handle_fail(vdi_uuid, e)
3792 vdiInfo.vdiType = vdi_type
3793 return vdiInfo
3795 try:
3796 for vdiInfo in multi_cowutil.run(load_info, pending_vdis):
3797 all_vdi_info[vdiInfo.uuid] = vdiInfo
3798 finally:
3799 del multi_cowutil
3801 return all_vdi_info
3803 @override
3804 def _prepareCoalesceLeaf(self, vdi) -> None:
3805 vdi._activateChain()
3806 vdi.deflate()
3807 vdi._inflateParentForCoalesce()
3809 @override
3810 def _finishCoalesceLeaf(self, parent) -> None:
3811 if not parent.isSnapshot() or parent.isAttachedRW():
3812 parent.inflateFully()
3813 else:
3814 parent.deflate()
3816 @override
3817 def _calcExtraSpaceNeeded(self, child, parent) -> int:
3818 return LinstorCowUtil(
3819 self.xapi.session, self._linstor, parent.vdi_type
3820 ).compute_volume_size(parent.sizeVirt) - parent.getDrbdSize()
3822 def _hasValidDevicePath(self, uuid):
3823 try:
3824 self._linstor.get_device_path(uuid)
3825 except Exception:
3826 # TODO: Maybe log exception.
3827 return False
3828 return True
3830 @override
3831 def _liveLeafCoalesce(self, vdi: VDI, coalesce_on_remote: bool = False) -> bool:
3832 self.lock()
3833 try:
3834 self._linstor.ensure_volume_is_not_locked(
3835 vdi.uuid, timeout=LinstorVDI.VOLUME_LOCK_TIMEOUT
3836 )
3837 return super(LinstorSR, self)._liveLeafCoalesce(vdi)
3838 finally:
3839 self.unlock()
3841 @override
3842 def _handleInterruptedCoalesceLeaf(self) -> None:
3843 entries = self.journaler.get_all(VDI.JRN_LEAF)
3844 for uuid, parentUuid in entries.items():
3845 if self._hasValidDevicePath(parentUuid) or \
3846 self._hasValidDevicePath(self.TMP_RENAME_PREFIX + uuid):
3847 self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
3848 else:
3849 self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
3850 self.journaler.remove(VDI.JRN_LEAF, uuid)
3851 vdi = self.getVDI(uuid)
3852 if vdi:
3853 vdi.ensureUnpaused()
3855 def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3856 Util.log('*** UNDO LEAF-COALESCE')
3857 parent = self.getVDI(parentUuid)
3858 if not parent:
3859 parent = self.getVDI(childUuid)
3860 if not parent:
3861 raise util.SMException(
3862 'Neither {} nor {} found'.format(parentUuid, childUuid)
3863 )
3864 Util.log(
3865 'Renaming parent back: {} -> {}'.format(childUuid, parentUuid)
3866 )
3867 parent.rename(parentUuid)
3869 child = self.getVDI(childUuid)
3870 if not child:
3871 child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
3872 if not child:
3873 raise util.SMException(
3874 'Neither {} nor {} found'.format(
3875 childUuid, self.TMP_RENAME_PREFIX + childUuid
3876 )
3877 )
3878 Util.log('Renaming child back to {}'.format(childUuid))
3879 child.rename(childUuid)
3880 Util.log('Updating the VDI record')
3881 child.setConfig(VDI.DB_VDI_PARENT, parentUuid)
3882 child.setConfig(VDI.DB_VDI_TYPE, child.vdi_type)
3884 # TODO: Maybe deflate here.
3886 if child.isHidden():
3887 child._setHidden(False)
3888 if not parent.isHidden():
3889 parent._setHidden(True)
3890 self._updateSlavesOnUndoLeafCoalesce(parent, child)
3891 Util.log('*** leaf-coalesce undo successful')
3893 def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
3894 Util.log('*** FINISH LEAF-COALESCE')
3895 vdi = self.getVDI(childUuid)
3896 if not vdi:
3897 raise util.SMException('VDI {} not found'.format(childUuid))
3898 # TODO: Maybe inflate.
3899 try:
3900 self.forgetVDI(parentUuid)
3901 except XenAPI.Failure:
3902 pass
3903 self._updateSlavesOnResize(vdi)
3904 Util.log('*** finished leaf-coalesce successfully')
3906 def _checkSlaves(self, vdi):
3907 try:
3908 openers = self._linstor.get_volume_openers(vdi.uuid)
3909 for host_openers in openers.values():
3910 for opener in host_openers.values():
3911 if opener['process-name'] != 'tapdisk':
3912 raise util.SMException(
3913 'VDI {} is in use: {}'.format(vdi.uuid, openers)
3914 )
3915 except LinstorVolumeManagerError as e:
3916 if e.code != LinstorVolumeManagerError.ERR_VOLUME_NOT_EXISTS:
3917 raise
3919 @classmethod
3920 def abort_gc_from_openers_vdi(cls, vdi_uuid: str, openers: "LinstorVolumeOpeners") -> bool:
3921 return cls._abort_gc_from_openers(vdi_uuid, True, openers)
3923 @classmethod
3924 def abort_gc_from_openers_sr(cls, sr_uuid: str, openers: "LinstorVolumeOpeners") -> bool:
3925 return cls._abort_gc_from_openers(sr_uuid, False, openers)
3927 @staticmethod
3928 def _abort_gc_from_openers(uuid: str, is_vdi_uuid: bool, openers: "LinstorVolumeOpeners") -> bool:
3929 from linstorcowutil import MANAGER_PLUGIN
3931 node_name = None
3933 for host_openers in openers.values():
3934 for hostname, opener in host_openers.items():
3935 # Not the most accurate check but it works...
3936 # `vhd-util` is probably prefixed with a "+" which is ignored here.
3937 if not opener["process-name"].endswith("vhd-util") or "coalesce" not in opener["cmdline"]:
3938 continue
3940 if not node_name:
3941 import socket
3942 node_name = socket.gethostname()
3944 if node_name == hostname:
3945 continue
3947 with util.timeout(5), util.ApiSession("SMGC-coalescing") as session:
3948 sr_uuid = util.get_sr_uuid_from_vdi_uuid(session, uuid) if is_vdi_uuid else uuid
3949 util.SMlog(f"LINSTOR volume is coalescing on `{sr_uuid}`. We're going to interrupt the GC...")
3950 return util.strtobool(session.xenapi.host.call_plugin(
3951 util.get_master_ref(session), MANAGER_PLUGIN, "abortGc", {"srUuid": sr_uuid}
3952 ))
3953 return False
3957################################################################################
3958#
3959# Helpers
3960#
3961def daemonize():
3962 pid = os.fork()
3963 if pid:
3964 os.waitpid(pid, 0)
3965 Util.log("New PID [%d]" % pid)
3966 return False
3967 os.chdir("/")
3968 os.setsid()
3969 pid = os.fork()
3970 if pid:
3971 Util.log("Will finish as PID [%d]" % pid)
3972 os._exit(0)
3973 for fd in [0, 1, 2]:
3974 try:
3975 os.close(fd)
3976 except OSError:
3977 pass
3978 # we need to fill those special fd numbers or pread won't work
3979 sys.stdin = open("/dev/null", 'r')
3980 sys.stderr = open("/dev/null", 'w')
3981 sys.stdout = open("/dev/null", 'w')
3982 # As we're a new process we need to clear the lock objects
3983 lock.Lock.clearAll()
3984 return True
3987def normalizeType(type):
3988 if type in LVMSR.SUBTYPES:
3989 type = SR.TYPE_LVHD
3990 if type in ["lvm", "lvmoiscsi", "lvmohba", "lvmofcoe"]:
3991 # temporary while LVHD is symlinked as LVM
3992 type = SR.TYPE_LVHD
3993 if type in [
3994 "ext", "nfs", "ocfsoiscsi", "ocfsohba", "smb", "cephfs", "glusterfs",
3995 "moosefs", "xfs", "zfs", "largeblock"
3996 ]:
3997 type = SR.TYPE_FILE
3998 if type in ["linstor"]:
3999 type = SR.TYPE_LINSTOR
4000 if type not in SR.TYPES:
4001 raise util.SMException("Unsupported SR type: %s" % type)
4002 return type
4004GCPAUSE_DEFAULT_SLEEP = 5 * 60
4007def _gc_init_file(sr_uuid):
4008 return os.path.join(NON_PERSISTENT_DIR, str(sr_uuid), 'gc_init')
4011def _create_init_file(sr_uuid):
4012 util.makedirs(os.path.join(NON_PERSISTENT_DIR, str(sr_uuid)))
4013 with open(os.path.join(_gc_init_file(sr_uuid)), 'w+') as f:
4014 f.write('1')
4017def _gcLoopPause(sr, dryRun=False, immediate=False):
4018 if immediate:
4019 return
4021 # Check to see if the GCPAUSE_FISTPOINT is present. If so the fist
4022 # point will just return. Otherwise, fall back on an abortable sleep.
4024 if util.fistpoint.is_active(util.GCPAUSE_FISTPOINT):
4026 util.fistpoint.activate_custom_fn(util.GCPAUSE_FISTPOINT, 4026 ↛ exitline 4026 didn't jump to the function exit
4027 lambda *args: None)
4028 elif os.path.exists(_gc_init_file(sr.uuid)):
4029 def abortTest():
4030 return IPCFlag(sr.uuid).test(FLAG_TYPE_ABORT)
4032 # If time.sleep hangs we are in deep trouble, however for
4033 # completeness we set the timeout of the abort thread to
4034 # 110% of GCPAUSE_DEFAULT_SLEEP.
4035 Util.log("GC active, about to go quiet")
4036 Util.runAbortable(lambda: time.sleep(GCPAUSE_DEFAULT_SLEEP), 4036 ↛ exitline 4036 didn't run the lambda on line 4036
4037 None, sr.uuid, abortTest, VDI.POLL_INTERVAL,
4038 GCPAUSE_DEFAULT_SLEEP * 1.1)
4039 Util.log("GC active, quiet period ended")
4042def _gcLoop(sr, dryRun=False, immediate=False):
4043 if not lockGCActive.acquireNoblock(): 4043 ↛ 4044line 4043 didn't jump to line 4044, because the condition on line 4043 was never true
4044 Util.log("Another GC instance already active, exiting")
4045 return
4047 # Check we're still attached after acquiring locks
4048 if not sr.xapi.isPluggedHere():
4049 Util.log("SR no longer attached, exiting")
4050 return
4052 # Clean up Intellicache files
4053 sr.cleanupCache()
4055 # Track how many we do
4056 coalesced = 0
4057 task_status = "success"
4058 try:
4059 # Check if any work needs to be done
4060 if not sr.xapi.isPluggedHere(): 4060 ↛ 4061line 4060 didn't jump to line 4061, because the condition on line 4060 was never true
4061 Util.log("SR no longer attached, exiting")
4062 return
4063 sr.scanLocked()
4064 if not sr.hasWork():
4065 Util.log("No work, exiting")
4066 return
4067 sr.xapi.create_task(
4068 "Garbage Collection",
4069 "Garbage collection for SR %s" % sr.uuid)
4070 _gcLoopPause(sr, dryRun, immediate=immediate)
4071 while True:
4072 if SIGTERM:
4073 Util.log("Term requested")
4074 return
4076 if not sr.xapi.isPluggedHere(): 4076 ↛ 4077line 4076 didn't jump to line 4077, because the condition on line 4076 was never true
4077 Util.log("SR no longer attached, exiting")
4078 break
4079 sr.scanLocked()
4080 if not sr.hasWork():
4081 Util.log("No work, exiting")
4082 break
4084 if not lockGCRunning.acquireNoblock(): 4084 ↛ 4085line 4084 didn't jump to line 4085, because the condition on line 4084 was never true
4085 Util.log("Unable to acquire GC running lock.")
4086 return
4087 try:
4088 if not sr.gcEnabled(): 4088 ↛ 4089line 4088 didn't jump to line 4089, because the condition on line 4088 was never true
4089 break
4091 sr.xapi.update_task_progress("done", coalesced)
4093 sr.cleanupCoalesceJournals()
4094 # Create the init file here in case startup is waiting on it
4095 _create_init_file(sr.uuid)
4096 sr.scanLocked()
4097 sr.updateBlockInfo()
4099 howmany = len(sr.findGarbage())
4100 if howmany > 0:
4101 Util.log("Found %d orphaned vdis" % howmany)
4102 sr.lock()
4103 try:
4104 sr.garbageCollect(dryRun)
4105 finally:
4106 sr.unlock()
4107 sr.xapi.srUpdate()
4109 candidate = sr.findCoalesceable()
4110 if candidate:
4111 util.fistpoint.activate(
4112 "LVHDRT_finding_a_suitable_pair", sr.uuid)
4113 sr.coalesce(candidate, dryRun)
4114 sr.xapi.srUpdate()
4115 coalesced += 1
4116 continue
4118 candidate = sr.findLeafCoalesceable()
4119 if candidate: 4119 ↛ 4126line 4119 didn't jump to line 4126, because the condition on line 4119 was never false
4120 sr.coalesceLeaf(candidate, dryRun)
4121 sr.xapi.srUpdate()
4122 coalesced += 1
4123 continue
4125 finally:
4126 lockGCRunning.release() 4126 ↛ 4131line 4126 didn't jump to line 4131, because the break on line 4089 wasn't executed
4127 except:
4128 task_status = "failure"
4129 raise
4130 finally:
4131 sr.xapi.set_task_status(task_status)
4132 Util.log("GC process exiting, no work left")
4133 _create_init_file(sr.uuid)
4134 lockGCActive.release()
4137def _gc(session, srUuid, dryRun=False, immediate=False):
4138 init(srUuid)
4139 sr = SR.getInstance(srUuid, session)
4140 if not sr.gcEnabled(False): 4140 ↛ 4141line 4140 didn't jump to line 4141, because the condition on line 4140 was never true
4141 return
4143 try:
4144 _gcLoop(sr, dryRun, immediate=immediate)
4145 finally:
4146 sr.check_no_space_candidates()
4147 sr.cleanup()
4148 sr.logFilter.logState()
4149 del sr.xapi
4152def _abort(srUuid, soft=False):
4153 """Aborts an GC/coalesce.
4155 srUuid: the UUID of the SR whose GC/coalesce must be aborted
4156 soft: If set to True and there is a pending abort signal, the function
4157 doesn't do anything. If set to False, a new abort signal is issued.
4159 returns: If soft is set to False, we return True holding lockGCActive. If
4160 soft is set to False and an abort signal is pending, we return False
4161 without holding lockGCActive. An exception is raised in case of error."""
4162 Util.log("=== SR %s: abort ===" % (srUuid))
4163 init(srUuid)
4164 if not lockGCActive.acquireNoblock():
4165 gotLock = False
4166 Util.log("Aborting currently-running instance (SR %s)" % srUuid)
4167 abortFlag = IPCFlag(srUuid)
4168 if not abortFlag.set(FLAG_TYPE_ABORT, soft):
4169 return False
4170 for i in range(SR.LOCK_RETRY_ATTEMPTS):
4171 gotLock = lockGCActive.acquireNoblock()
4172 if gotLock:
4173 break
4174 time.sleep(SR.LOCK_RETRY_INTERVAL)
4175 abortFlag.clear(FLAG_TYPE_ABORT)
4176 if not gotLock:
4177 raise util.CommandException(code=errno.ETIMEDOUT,
4178 reason="SR %s: error aborting existing process" % srUuid)
4179 return True
4182def init(srUuid):
4183 global lockGCRunning
4184 if not lockGCRunning: 4184 ↛ 4185line 4184 didn't jump to line 4185, because the condition on line 4184 was never true
4185 lockGCRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, srUuid)
4186 global lockGCActive
4187 if not lockGCActive: 4187 ↛ 4188line 4187 didn't jump to line 4188, because the condition on line 4187 was never true
4188 lockGCActive = LockActive(srUuid)
4191class LockActive:
4192 """
4193 Wraps the use of LOCK_TYPE_GC_ACTIVE such that the lock cannot be acquired
4194 if another process holds the SR lock.
4195 """
4196 def __init__(self, srUuid):
4197 self._lock = lock.Lock(LOCK_TYPE_GC_ACTIVE, srUuid)
4198 self._srLock = lock.Lock(lock.LOCK_TYPE_SR, srUuid)
4200 def acquireNoblock(self):
4201 self._srLock.acquire()
4203 try:
4204 return self._lock.acquireNoblock()
4205 finally:
4206 self._srLock.release()
4208 def release(self):
4209 self._lock.release()
4212def usage():
4213 output = """Garbage collect and/or coalesce COW images in a COW-based SR
4215Parameters:
4216 -u --uuid UUID SR UUID
4217 and one of:
4218 -g --gc garbage collect, coalesce, and repeat while there is work
4219 -G --gc_force garbage collect once, aborting any current operations
4220 -c --cache-clean <max_age> clean up IntelliCache cache files older than
4221 max_age hours
4222 -a --abort abort any currently running operation (GC or coalesce)
4223 -q --query query the current state (GC'ing, coalescing or not running)
4224 -x --disable disable GC/coalesce (will be in effect until you exit)
4225 -t --debug see Debug below
4227Options:
4228 -b --background run in background (return immediately) (valid for -g only)
4229 -f --force continue in the presence of COW images with errors (when doing
4230 GC, this might cause removal of any such images) (only valid
4231 for -G) (DANGEROUS)
4233Debug:
4234 The --debug parameter enables manipulation of LVHD VDIs for debugging
4235 purposes. ** NEVER USE IT ON A LIVE VM **
4236 The following parameters are required:
4237 -t --debug <cmd> <cmd> is one of "activate", "deactivate", "inflate",
4238 "deflate".
4239 -v --vdi_uuid VDI UUID
4240 """
4241 #-d --dry-run don't actually perform any SR-modifying operations
4242 print(output)
4243 Util.log("(Invalid usage)")
4244 sys.exit(1)
4247##############################################################################
4248#
4249# API
4250#
4251def abort(srUuid, soft=False):
4252 """Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair.
4253 """
4254 if _abort(srUuid, soft):
4255 stop_gc_service(srUuid)
4256 Util.log("abort: releasing the process lock")
4257 lockGCActive.release()
4258 return True
4259 else:
4260 return False
4263def run_gc(session, srUuid, dryRun, immediate=False):
4264 try:
4265 _gc(session, srUuid, dryRun, immediate=immediate)
4266 return 0
4267 except AbortException:
4268 Util.log("Aborted")
4269 return 2
4270 except Exception:
4271 Util.logException("gc")
4272 Util.log("* * * * * SR %s: ERROR\n" % srUuid)
4273 return 1
4276def gc(session, srUuid, inBackground, dryRun=False):
4277 """Garbage collect all deleted VDIs in SR "srUuid". Fork & return
4278 immediately if inBackground=True.
4280 The following algorithm is used:
4281 1. If we are already GC'ing in this SR, return
4282 2. If we are already coalescing a VDI pair:
4283 a. Scan the SR and determine if the VDI pair is GC'able
4284 b. If the pair is not GC'able, return
4285 c. If the pair is GC'able, abort coalesce
4286 3. Scan the SR
4287 4. If there is nothing to collect, nor to coalesce, return
4288 5. If there is something to collect, GC all, then goto 3
4289 6. If there is something to coalesce, coalesce one pair, then goto 3
4290 """
4291 Util.log("=== SR %s: gc ===" % srUuid)
4293 signal.signal(signal.SIGTERM, receiveSignal)
4295 if inBackground:
4296 if daemonize(): 4296 ↛ exitline 4296 didn't return from function 'gc', because the condition on line 4296 was never false
4297 # we are now running in the background. Catch & log any errors
4298 # because there is no other way to propagate them back at this
4299 # point
4301 run_gc(None, srUuid, dryRun)
4302 os._exit(0)
4303 else:
4304 os._exit(run_gc(session, srUuid, dryRun, immediate=True))
4307def start_gc(session, sr_uuid):
4308 """
4309 This function is used to try to start a backgrounded GC session by forking
4310 the current process. If using the systemd version, call start_gc_service() instead.
4311 """
4312 # don't bother if an instance already running (this is just an
4313 # optimization to reduce the overhead of forking a new process if we
4314 # don't have to, but the process will check the lock anyways)
4315 lockRunning = lock.Lock(lock.LOCK_TYPE_GC_RUNNING, sr_uuid)
4316 if not lockRunning.acquireNoblock():
4317 if should_preempt(session, sr_uuid):
4318 util.SMlog("Aborting currently-running coalesce of garbage VDI")
4319 try:
4320 if not abort(sr_uuid, soft=True):
4321 util.SMlog("The GC has already been scheduled to re-start")
4322 except util.CommandException as e:
4323 if e.code != errno.ETIMEDOUT:
4324 raise
4325 util.SMlog('failed to abort the GC')
4326 else:
4327 util.SMlog("A GC instance already running, not kicking")
4328 return
4329 else:
4330 lockRunning.release()
4332 util.SMlog(f"Starting GC file is {__file__}")
4333 subprocess.run([__file__, '-b', '-u', sr_uuid, '-g'],
4334 stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
4336def _gc_service_cmd(sr_uuid, action, extra_args=None):
4337 """
4338 Build and run the systemctl command for the GC service using util.doexec.
4339 """
4340 sr_uuid_esc = sr_uuid.replace("-", "\\x2d")
4341 cmd=["/usr/bin/systemctl", "--quiet"]
4342 if extra_args:
4343 cmd.extend(extra_args)
4344 cmd += [action, f"SMGC@{sr_uuid_esc}"]
4345 return util.doexec(cmd)
4348def start_gc_service(sr_uuid, wait=False):
4349 """
4350 This starts the templated systemd service which runs GC on the given SR UUID.
4351 If the service was already started, this is a no-op.
4353 Because the service is a one-shot with RemainAfterExit=no, when called with
4354 wait=True this will run the service synchronously and will not return until the
4355 run has finished. This is used to force a run of the GC instead of just kicking it
4356 in the background.
4357 """
4358 util.SMlog(f"Kicking SMGC@{sr_uuid}...")
4359 _gc_service_cmd(sr_uuid, "start", extra_args=None if wait else ["--no-block"])
4362def stop_gc_service(sr_uuid):
4363 """
4364 Stops the templated systemd service which runs GC on the given SR UUID.
4365 """
4366 util.SMlog(f"Stopping SMGC@{sr_uuid}...")
4367 (rc, _stdout, stderr) = _gc_service_cmd(sr_uuid, "stop")
4368 if rc != 0: 4368 ↛ exitline 4368 didn't return from function 'stop_gc_service', because the condition on line 4368 was never false
4369 util.SMlog(f"Failed to stop gc service `SMGC@{sr_uuid}`: `{stderr}`")
4372def wait_for_completion(sr_uuid):
4373 while get_state(sr_uuid):
4374 time.sleep(5)
4377def gc_force(session, srUuid, force=False, dryRun=False, lockSR=False):
4378 """Garbage collect all deleted VDIs in SR "srUuid". The caller must ensure
4379 the SR lock is held.
4380 The following algorithm is used:
4381 1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce
4382 2. Scan the SR
4383 3. GC
4384 4. return
4385 """
4386 Util.log("=== SR %s: gc_force ===" % srUuid)
4387 init(srUuid)
4388 sr = SR.getInstance(srUuid, session, lockSR, True)
4389 if not lockGCActive.acquireNoblock():
4390 abort(srUuid)
4391 else:
4392 Util.log("Nothing was running, clear to proceed")
4394 if force:
4395 Util.log("FORCED: will continue even if there are COW image errors")
4396 sr.scanLocked(force)
4397 sr.cleanupCoalesceJournals()
4399 try:
4400 sr.cleanupCache()
4401 sr.garbageCollect(dryRun)
4402 finally:
4403 sr.cleanup()
4404 sr.logFilter.logState()
4405 lockGCActive.release()
4408def get_state(srUuid):
4409 """Return whether GC/coalesce is currently running or not. This asks systemd for
4410 the state of the templated SMGC service and will return True if it is "activating"
4411 or "running" (for completeness, as in practice it will never achieve the latter state)
4412 """
4413 sr_uuid_esc = srUuid.replace("-", "\\x2d")
4414 cmd=[ "/usr/bin/systemctl", "is-active", f"SMGC@{sr_uuid_esc}"]
4415 result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, close_fds=True)
4416 state = result.stdout.decode('utf-8').rstrip()
4417 if state == "activating" or state == "running":
4418 return True
4419 return False
4422def should_preempt(session, srUuid):
4423 sr = SR.getInstance(srUuid, session)
4424 entries = sr.journaler.getAll(VDI.JRN_COALESCE)
4425 if len(entries) == 0:
4426 return False
4427 elif len(entries) > 1:
4428 raise util.SMException("More than one coalesce entry: " + str(entries))
4429 sr.scanLocked()
4430 coalescedUuid = entries.popitem()[0]
4431 garbage = sr.findGarbage()
4432 for vdi in garbage:
4433 if vdi.uuid == coalescedUuid:
4434 return True
4435 return False
4438def get_coalesceable_leaves(session, srUuid, vdiUuids):
4439 coalesceable = []
4440 sr = SR.getInstance(srUuid, session)
4441 sr.scanLocked()
4442 for uuid in vdiUuids:
4443 vdi = sr.getVDI(uuid)
4444 if not vdi:
4445 raise util.SMException("VDI %s not found" % uuid)
4446 if vdi.isLeafCoalesceable():
4447 coalesceable.append(uuid)
4448 return coalesceable
4451def cache_cleanup(session, srUuid, maxAge):
4452 sr = SR.getInstance(srUuid, session)
4453 return sr.cleanupCache(maxAge)
4456def debug(sr_uuid, cmd, vdi_uuid):
4457 Util.log("Debug command: %s" % cmd)
4458 sr = SR.getInstance(sr_uuid, None)
4459 if not isinstance(sr, LVMSR):
4460 print("Error: not an LVM SR")
4461 return
4462 sr.scanLocked()
4463 vdi = sr.getVDI(vdi_uuid)
4464 if not vdi:
4465 print("Error: VDI %s not found")
4466 return
4467 print("Running %s on SR %s" % (cmd, sr))
4468 print("VDI before: %s" % vdi)
4469 if cmd == "activate":
4470 vdi._activate()
4471 print("VDI file: %s" % vdi.path)
4472 if cmd == "deactivate":
4473 ns = NS_PREFIX_LVM + sr.uuid
4474 sr.lvmCache.deactivate(ns, vdi.uuid, vdi.fileName, False)
4475 if cmd == "inflate":
4476 vdi.inflateFully()
4477 sr.cleanup()
4478 if cmd == "deflate":
4479 vdi.deflate()
4480 sr.cleanup()
4481 sr.scanLocked()
4482 print("VDI after: %s" % vdi)
4485def abort_optional_reenable(uuid):
4486 print("Disabling GC/coalesce for %s" % uuid)
4487 ret = _abort(uuid)
4488 input("Press enter to re-enable...")
4489 print("GC/coalesce re-enabled")
4490 lockGCRunning.release()
4491 if ret:
4492 lockGCActive.release()
4495##############################################################################
4496#
4497# CLI
4498#
4499def main():
4500 action = ""
4501 maxAge = 0
4502 uuid = ""
4503 background = False
4504 force = False
4505 dryRun = False
4506 debug_cmd = ""
4507 vdi_uuid = ""
4508 shortArgs = "gGc:aqxu:bfdt:v:"
4509 longArgs = ["gc", "gc_force", "clean_cache", "abort", "query", "disable",
4510 "uuid=", "background", "force", "dry-run", "debug=", "vdi_uuid="]
4512 try:
4513 opts, args = getopt.getopt(sys.argv[1:], shortArgs, longArgs)
4514 except getopt.GetoptError:
4515 usage()
4516 for o, a in opts:
4517 if o in ("-g", "--gc"):
4518 action = "gc"
4519 if o in ("-G", "--gc_force"):
4520 action = "gc_force"
4521 if o in ("-c", "--clean_cache"):
4522 action = "clean_cache"
4523 maxAge = int(a)
4524 if o in ("-a", "--abort"):
4525 action = "abort"
4526 if o in ("-q", "--query"):
4527 action = "query"
4528 if o in ("-x", "--disable"):
4529 action = "disable"
4530 if o in ("-u", "--uuid"):
4531 uuid = a
4532 if o in ("-b", "--background"):
4533 background = True
4534 if o in ("-f", "--force"):
4535 force = True
4536 if o in ("-d", "--dry-run"):
4537 Util.log("Dry run mode")
4538 dryRun = True
4539 if o in ("-t", "--debug"):
4540 action = "debug"
4541 debug_cmd = a
4542 if o in ("-v", "--vdi_uuid"):
4543 vdi_uuid = a
4545 if not action or not uuid:
4546 usage()
4547 if action == "debug" and not (debug_cmd and vdi_uuid) or \
4548 action != "debug" and (debug_cmd or vdi_uuid):
4549 usage()
4551 if action != "query" and action != "debug":
4552 print("All output goes to log")
4554 if action == "gc":
4555 gc(None, uuid, background, dryRun)
4556 elif action == "gc_force":
4557 gc_force(None, uuid, force, dryRun, True)
4558 elif action == "clean_cache":
4559 cache_cleanup(None, uuid, maxAge)
4560 elif action == "abort":
4561 abort(uuid)
4562 elif action == "query":
4563 print("Currently running: %s" % get_state(uuid))
4564 elif action == "disable":
4565 abort_optional_reenable(uuid)
4566 elif action == "debug":
4567 debug(uuid, debug_cmd, vdi_uuid)
4570if __name__ == '__main__': 4570 ↛ 4571line 4570 didn't jump to line 4571, because the condition on line 4570 was never true
4571 main()