mirror of
https://github.com/apache/cloudstack.git
synced 2025-10-26 08:42:29 +01:00
2281 lines
82 KiB
Python
Executable File
2281 lines
82 KiB
Python
Executable File
#!/usr/bin/python
|
|
# Copyright (C) 2006-2007 XenSource Ltd.
|
|
# Copyright (C) 2008-2009 Citrix Ltd.
|
|
#
|
|
# This program is free software; you can redistribute it and/or modify
|
|
# it under the terms of the GNU Lesser General Public License as published
|
|
# by the Free Software Foundation; version 2.1 only.
|
|
#
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Lesser General Public License for more details.
|
|
#
|
|
# Script to coalesce and garbage collect VHD-based SR's in the background
|
|
#
|
|
|
|
import os
|
|
import sys
|
|
import time
|
|
import signal
|
|
import subprocess
|
|
import getopt
|
|
import datetime
|
|
import exceptions
|
|
import traceback
|
|
import base64
|
|
import zlib
|
|
|
|
import XenAPI
|
|
import util
|
|
import lvutil
|
|
import vhdutil
|
|
import lvhdutil
|
|
import lvmcache
|
|
import journaler
|
|
import fjournaler
|
|
import lock
|
|
import atomicop
|
|
from refcounter import RefCounter
|
|
from ipc import IPCFlag
|
|
from lvmanager import LVActivator
|
|
|
|
# Disable automatic leaf-coalescing. Online leaf-coalesce is currently not
|
|
# possible due to lvhd_stop_using_() not working correctly. However, we leave
|
|
# this option available through the explicit LEAFCLSC_FORCE flag in the VDI
|
|
# record for use by the offline tool (which makes the operation safe by pausing
|
|
# the VM first)
|
|
AUTO_ONLINE_LEAF_COALESCE_ENABLED = False
|
|
|
|
LOG_FILE = "/var/log/SMlog"
|
|
FLAG_TYPE_ABORT = "abort" # flag to request aborting of GC/coalesce
|
|
|
|
# process "lock", used simply as an indicator that a process already exists
|
|
# that is doing GC/coalesce on this SR (such a process holds the lock, and we
|
|
# check for the fact by trying the lock).
|
|
LOCK_TYPE_RUNNING = "running"
|
|
lockRunning = None
|
|
|
|
|
|
class AbortException(util.SMException):
|
|
pass
|
|
|
|
################################################################################
|
|
#
|
|
# Util
|
|
#
|
|
class Util:
|
|
RET_RC = 1
|
|
RET_STDOUT = 2
|
|
RET_STDERR = 4
|
|
|
|
PREFIX = {"G": 1024 * 1024 * 1024, "M": 1024 * 1024, "K": 1024}
|
|
|
|
def log(text):
|
|
f = open(LOG_FILE, 'a')
|
|
f.write("<%d> %s\t%s\n" % (os.getpid(), datetime.datetime.now(), text))
|
|
f.close()
|
|
log = staticmethod(log)
|
|
|
|
def logException(tag):
|
|
info = sys.exc_info()
|
|
if info[0] == exceptions.SystemExit:
|
|
# this should not be happening when catching "Exception", but it is
|
|
sys.exit(0)
|
|
tb = reduce(lambda a, b: "%s%s" % (a, b), traceback.format_tb(info[2]))
|
|
Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
|
|
Util.log(" ***********************")
|
|
Util.log(" * E X C E P T I O N *")
|
|
Util.log(" ***********************")
|
|
Util.log("%s: EXCEPTION %s, %s" % (tag, info[0], info[1]))
|
|
Util.log(tb)
|
|
Util.log("*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*~*")
|
|
logException = staticmethod(logException)
|
|
|
|
def doexec(args, expectedRC, inputtext=None, ret=None, log=True):
|
|
"Execute a subprocess, then return its return code, stdout, stderr"
|
|
proc = subprocess.Popen(args,
|
|
stdin=subprocess.PIPE,\
|
|
stdout=subprocess.PIPE,\
|
|
stderr=subprocess.PIPE,\
|
|
shell=True,\
|
|
close_fds=True)
|
|
(stdout, stderr) = proc.communicate(inputtext)
|
|
stdout = str(stdout)
|
|
stderr = str(stderr)
|
|
rc = proc.returncode
|
|
if log:
|
|
Util.log("`%s`: %s" % (args, rc))
|
|
if type(expectedRC) != type([]):
|
|
expectedRC = [expectedRC]
|
|
if not rc in expectedRC:
|
|
reason = stderr.strip()
|
|
if stdout.strip():
|
|
reason = "%s (stdout: %s)" % (reason, stdout.strip())
|
|
Util.log("Failed: %s" % reason)
|
|
raise util.CommandException(rc, args, reason)
|
|
|
|
if ret == Util.RET_RC:
|
|
return rc
|
|
if ret == Util.RET_STDERR:
|
|
return stderr
|
|
return stdout
|
|
doexec = staticmethod(doexec)
|
|
|
|
def runAbortable(func, ret, ns, abortTest, pollInterval, timeOut):
|
|
"""execute func in a separate thread and kill it if abortTest signals
|
|
so"""
|
|
resultFlag = IPCFlag(ns)
|
|
pid = os.fork()
|
|
if pid:
|
|
startTime = time.time()
|
|
while True:
|
|
if resultFlag.test("success"):
|
|
Util.log(" Child process completed successfully")
|
|
resultFlag.clear("success")
|
|
return
|
|
if resultFlag.test("failure"):
|
|
resultFlag.clear("failure")
|
|
raise util.SMException("Child process exited with error")
|
|
if abortTest():
|
|
os.killpg(pid, signal.SIGKILL)
|
|
raise AbortException("Aborting due to signal")
|
|
if timeOut and time.time() - startTime > timeOut:
|
|
os.killpg(pid, signal.SIGKILL)
|
|
raise util.SMException("Timed out")
|
|
time.sleep(pollInterval)
|
|
else:
|
|
os.setpgrp()
|
|
try:
|
|
if func() == ret:
|
|
resultFlag.set("success")
|
|
else:
|
|
resultFlag.set("failure")
|
|
except:
|
|
resultFlag.set("failure")
|
|
os._exit(0)
|
|
runAbortable = staticmethod(runAbortable)
|
|
|
|
def num2str(number):
|
|
for prefix in ("G", "M", "K"):
|
|
if number >= Util.PREFIX[prefix]:
|
|
return "%.2f%s" % (float(number) / Util.PREFIX[prefix], prefix)
|
|
return "%s" % number
|
|
num2str = staticmethod(num2str)
|
|
|
|
def numBits(val):
|
|
count = 0
|
|
while val:
|
|
count += val & 1
|
|
val = val >> 1
|
|
return count
|
|
numBits = staticmethod(numBits)
|
|
|
|
def countBits(bitmap1, bitmap2):
|
|
"""return bit count in the bitmap produced by ORing the two bitmaps"""
|
|
len1 = len(bitmap1)
|
|
len2 = len(bitmap2)
|
|
lenLong = len1
|
|
lenShort = len2
|
|
bitmapLong = bitmap1
|
|
if len2 > len1:
|
|
lenLong = len2
|
|
lenShort = len1
|
|
bitmapLong = bitmap2
|
|
|
|
count = 0
|
|
for i in range(lenShort):
|
|
val = ord(bitmap1[i]) | ord(bitmap2[i])
|
|
count += Util.numBits(val)
|
|
|
|
for i in range(i + 1, lenLong):
|
|
val = ord(bitmapLong[i])
|
|
count += Util.numBits(val)
|
|
return count
|
|
countBits = staticmethod(countBits)
|
|
|
|
def getThisScript():
|
|
thisScript = util.get_real_path(__file__)
|
|
if thisScript.endswith(".pyc"):
|
|
thisScript = thisScript[:-1]
|
|
return thisScript
|
|
getThisScript = staticmethod(getThisScript)
|
|
|
|
def getThisHost():
|
|
uuid = None
|
|
f = open("/etc/xensource-inventory", 'r')
|
|
for line in f.readlines():
|
|
if line.startswith("INSTALLATION_UUID"):
|
|
uuid = line.split("'")[1]
|
|
f.close()
|
|
return uuid
|
|
getThisHost = staticmethod(getThisHost)
|
|
|
|
|
|
################################################################################
|
|
#
|
|
# XAPI
|
|
#
|
|
class XAPI:
|
|
USER = "root"
|
|
PLUGIN_ON_SLAVE = "on-slave"
|
|
PLUGIN_PAUSE_VDIS = "atomicop.py"
|
|
|
|
CONFIG_SM = 0
|
|
CONFIG_OTHER = 1
|
|
|
|
class LookupError(util.SMException):
|
|
pass
|
|
|
|
def getSession():
|
|
session = XenAPI.xapi_local()
|
|
session.xenapi.login_with_password(XAPI.USER, '')
|
|
return session
|
|
getSession = staticmethod(getSession)
|
|
|
|
def __init__(self, session, srUuid):
|
|
self.sessionPrivate = False
|
|
self.session = session
|
|
if self.session == None:
|
|
self.session = self.getSession()
|
|
self.sessionPrivate = True
|
|
self._srRef = self.session.xenapi.SR.get_by_uuid(srUuid)
|
|
self.srRecord = self.session.xenapi.SR.get_record(self._srRef)
|
|
self.hostUuid = Util.getThisHost()
|
|
self._hostRef = self.session.xenapi.host.get_by_uuid(self.hostUuid)
|
|
|
|
def __del__(self):
|
|
if self.sessionPrivate:
|
|
self.session.xenapi.session.logout()
|
|
|
|
def isInvalidHandleError(exception):
|
|
return exception.details[0] == "HANDLE_INVALID"
|
|
isInvalidHandleError = staticmethod(isInvalidHandleError)
|
|
|
|
def isPluggedHere(self):
|
|
pbds = self.getAttachedPBDs()
|
|
for pbdRec in pbds:
|
|
if pbdRec["host"] == self._hostRef:
|
|
return True
|
|
return False
|
|
|
|
def isMaster(self):
|
|
if self.srRecord["shared"]:
|
|
pool = self.session.xenapi.pool.get_all_records().values()[0]
|
|
return pool["master"] == self._hostRef
|
|
else:
|
|
pbds = self.getAttachedPBDs()
|
|
if len(pbds) < 1:
|
|
raise util.SMException("Local SR not attached")
|
|
elif len(pbds) > 1:
|
|
raise util.SMException("Local SR multiply attached")
|
|
return pbds[0]["host"] == self._hostRef
|
|
|
|
def getAttachedPBDs(self):
|
|
"""Return PBD records for all PBDs of this SR that are currently
|
|
attached"""
|
|
attachedPBDs = []
|
|
pbds = self.session.xenapi.PBD.get_all_records()
|
|
for pbdRec in pbds.values():
|
|
if pbdRec["SR"] == self._srRef and pbdRec["currently_attached"]:
|
|
attachedPBDs.append(pbdRec)
|
|
return attachedPBDs
|
|
|
|
def getOnlineHosts(self):
|
|
onlineHosts = []
|
|
hosts = self.session.xenapi.host.get_all_records()
|
|
for hostRef, hostRecord in hosts.iteritems():
|
|
metricsRef = hostRecord["metrics"]
|
|
metrics = self.session.xenapi.host_metrics.get_record(metricsRef)
|
|
if metrics["live"]:
|
|
onlineHosts.append(hostRef)
|
|
return onlineHosts
|
|
|
|
def ensureInactive(self, hostRef, args):
|
|
text = self.session.xenapi.host.call_plugin( \
|
|
hostRef, self.PLUGIN_ON_SLAVE, "multi", args)
|
|
Util.log("call-plugin returned: '%s'" % text)
|
|
|
|
def getRefVDI(self, vdi):
|
|
return self.session.xenapi.VDI.get_by_uuid(vdi.uuid)
|
|
|
|
def singleSnapshotVDI(self, vdi):
|
|
return self.session.xenapi.VDI.snapshot(vdi.getRef(), {"type":"single"})
|
|
|
|
def forgetVDI(self, vdiUuid):
|
|
"""Forget the VDI, but handle the case where the VDI has already been
|
|
forgotten (i.e. ignore errors)"""
|
|
try:
|
|
vdiRef = self.session.xenapi.VDI.get_by_uuid(vdiUuid)
|
|
self.session.xenapi.VDI.forget(vdiRef)
|
|
except XenAPI.Failure:
|
|
pass
|
|
|
|
def getConfigVDI(self, kind, vdi):
|
|
if kind == self.CONFIG_SM:
|
|
return self.session.xenapi.VDI.get_sm_config(vdi.getRef())
|
|
elif kind == self.CONFIG_OTHER:
|
|
return self.session.xenapi.VDI.get_other_config(vdi.getRef())
|
|
assert(False)
|
|
|
|
def setConfigVDI(self, kind, vdi, map):
|
|
if kind == self.CONFIG_SM:
|
|
self.session.xenapi.VDI.set_sm_config(vdi.getRef(), map)
|
|
elif kind == self.CONFIG_OTHER:
|
|
self.session.xenapi.VDI.set_other_config(vdi.getRef(), map)
|
|
else:
|
|
assert(False)
|
|
|
|
def srUpdate(self):
|
|
Util.log("Starting asynch srUpdate for SR %s" % self.srRecord["uuid"])
|
|
abortFlag = IPCFlag(self.srRecord["uuid"])
|
|
task = self.session.xenapi.Async.SR.update(self._srRef)
|
|
for i in range(60):
|
|
status = self.session.xenapi.task.get_status(task)
|
|
if not status == "pending":
|
|
Util.log("SR.update_asynch status changed to [%s]" % status)
|
|
return
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
Util.log("Abort signalled during srUpdate, cancelling task...")
|
|
try:
|
|
self.session.xenapi.task.cancel(task)
|
|
Util.log("Task cancelled")
|
|
except:
|
|
pass
|
|
return
|
|
time.sleep(1)
|
|
Util.log("Asynch srUpdate still running, but timeout exceeded.")
|
|
|
|
def atomicOp(self, vdiList, op, args, mustExist = False):
|
|
vdiRefs = []
|
|
for vdi in vdiList:
|
|
Util.log("atomicOp: will pause %s" % vdi.toString())
|
|
try:
|
|
vdiRefs.append(vdi.getRef())
|
|
except XenAPI.Failure:
|
|
Util.log("atomicOp: can't find %s" % vdi.toString())
|
|
if mustExist:
|
|
raise
|
|
if len(vdiRefs) == 0:
|
|
Util.log("atomicOp: no VDIs found in DB, not pausing anything")
|
|
fn = getattr(atomicop, op)
|
|
ret = fn(self.session, args)
|
|
else:
|
|
ret = self.session.xenapi.SR.lvhd_stop_using_these_vdis_and_call_script(\
|
|
vdiRefs, self.PLUGIN_PAUSE_VDIS, op, args)
|
|
Util.log("Plugin returned: %s" % ret)
|
|
if ret == atomicop.RET_EXCEPTION:
|
|
raise util.SMException("Exception in atomic %s" % op)
|
|
if ret == atomicop.RET_SUCCESS:
|
|
return True
|
|
return False
|
|
|
|
|
|
################################################################################
|
|
#
|
|
# VDI
|
|
#
|
|
class VDI:
|
|
"""Object representing a VDI of a VHD-based SR"""
|
|
|
|
POLL_INTERVAL = 1
|
|
POLL_TIMEOUT = 30
|
|
DEVICE_MAJOR = 202
|
|
DRIVER_NAME_VHD = "vhd"
|
|
|
|
# config keys & values
|
|
DB_VHD_PARENT = "vhd-parent"
|
|
DB_VDI_TYPE = "vdi_type"
|
|
DB_VHD_BLOCKS = "vhd-blocks"
|
|
DB_LEAFCLSC = "leaf-coalesce" # config key
|
|
LEAFCLSC_DISABLED = "no" # set by user; means do not leaf-coalesce
|
|
LEAFCLSC_FORCE = "force" # set by user; means skip snap-coalesce
|
|
LEAFCLSC_OFFLINE = "offline" # set here for informational purposes: means
|
|
# no space to snap-coalesce or unable to keep
|
|
# up with VDI
|
|
CONFIG_TYPE = {
|
|
DB_VHD_PARENT: XAPI.CONFIG_SM,
|
|
DB_VDI_TYPE: XAPI.CONFIG_SM,
|
|
DB_VHD_BLOCKS: XAPI.CONFIG_SM,
|
|
DB_LEAFCLSC: XAPI.CONFIG_OTHER }
|
|
|
|
LIVE_LEAF_COALESCE_MAX_SIZE = 100 * 1024 * 1024 # bytes
|
|
LIVE_LEAF_COALESCE_TIMEOUT = 10 # seconds
|
|
|
|
JRN_RELINK = "relink" # journal entry type for relinking children
|
|
JRN_COALESCE = "coalesce" # to communicate which VDI is being coalesced
|
|
JRN_LEAF = "leaf" # used in coalesce-leaf
|
|
|
|
PRINT_INDENTATION = 4
|
|
|
|
def __init__(self, sr, uuid):
|
|
self.sr = sr
|
|
self.scanError = True
|
|
self.uuid = uuid
|
|
self.parentUuid = ""
|
|
self.sizeVirt = -1
|
|
self._sizeVHD = -1
|
|
self.hidden = False
|
|
self.parent = None
|
|
self.children = []
|
|
self._vdiRef = None
|
|
self._config = {}
|
|
self._configDirty = {}
|
|
self._clearRef()
|
|
|
|
def load(self):
|
|
"""Load VDI info"""
|
|
pass # abstract
|
|
|
|
def getDriverName(self):
|
|
return self.DRIVER_NAME_VHD
|
|
|
|
def getRef(self):
|
|
if self._vdiRef == None:
|
|
self._vdiRef = self.sr.xapi.getRefVDI(self)
|
|
return self._vdiRef
|
|
|
|
def getConfig(self, key, default = None):
|
|
kind = self.CONFIG_TYPE[key]
|
|
self._configLazyInit(kind)
|
|
if self._config[kind].get(key):
|
|
return self._config[kind][key]
|
|
return default
|
|
|
|
def setConfig(self, key, val):
|
|
kind = self.CONFIG_TYPE[key]
|
|
self._configLazyInit(kind)
|
|
self._config[kind][key] = val
|
|
self._configDirty[kind] = True
|
|
|
|
def delConfig(self, key):
|
|
kind = self.CONFIG_TYPE[key]
|
|
self._configLazyInit(kind)
|
|
if self._config[kind].get(key):
|
|
del self._config[kind][key]
|
|
self._configDirty[kind] = True
|
|
|
|
def updateConfig(self):
|
|
for kind in self._config.keys():
|
|
if self._configDirty[kind]:
|
|
self.sr.xapi.setConfigVDI(kind, self, self._config[kind])
|
|
self._configDirty[kind] = False
|
|
|
|
def setConfigUpdate(self, key, val):
|
|
self.setConfig(key, val)
|
|
self.updateConfig()
|
|
|
|
def delConfigUpdate(self, key):
|
|
self.delConfig(key)
|
|
self.updateConfig()
|
|
|
|
def getVHDBlocks(self):
|
|
val = self.getConfig(VDI.DB_VHD_BLOCKS)
|
|
if not val:
|
|
self.updateBlockInfo()
|
|
val = self.getConfig(VDI.DB_VHD_BLOCKS)
|
|
bitmap = zlib.decompress(base64.b64decode(val))
|
|
return bitmap
|
|
|
|
def isCoalesceable(self):
|
|
"""A VDI is coalesceable if it has no siblings and is not a leaf"""
|
|
return not self.scanError and \
|
|
self.parent and \
|
|
len(self.parent.children) == 1 and \
|
|
self.hidden and \
|
|
len(self.children) > 0
|
|
|
|
def isLeafCoalesceable(self):
|
|
"""A VDI is leaf-coalesceable if it has no siblings and is a leaf"""
|
|
return not self.scanError and \
|
|
self.parent and \
|
|
len(self.parent.children) == 1 and \
|
|
not self.hidden and \
|
|
len(self.children) == 0
|
|
|
|
def canLiveCoalesce(self):
|
|
"""Can we stop-and-leaf-coalesce this VDI? The VDI must be
|
|
isLeafCoalesceable() already"""
|
|
return self.getSizeVHD() <= self.LIVE_LEAF_COALESCE_MAX_SIZE or \
|
|
self.getConfig(self.DB_LEAFCLSC) == self.LEAFCLSC_FORCE
|
|
|
|
def getAllPrunable(self):
|
|
if len(self.children) == 0: # base case
|
|
# it is possible to have a hidden leaf that was recently coalesced
|
|
# onto its parent, its children already relinked but not yet
|
|
# reloaded - in which case it may not be garbage collected yet:
|
|
# some tapdisks could still be using the file.
|
|
if self.sr.journaler.get(self.JRN_RELINK, self.uuid):
|
|
return []
|
|
if not self.scanError and self.hidden:
|
|
return [self]
|
|
return []
|
|
|
|
thisPrunable = True
|
|
vdiList = []
|
|
for child in self.children:
|
|
childList = child.getAllPrunable()
|
|
vdiList.extend(childList)
|
|
if child not in childList:
|
|
thisPrunable = False
|
|
|
|
if not self.scanError and thisPrunable:
|
|
vdiList.append(self)
|
|
return vdiList
|
|
|
|
def getSizeVHD(self):
|
|
return self._sizeVHD
|
|
|
|
def getTreeRoot(self):
|
|
"Get the root of the tree that self belongs to"
|
|
root = self
|
|
while root.parent:
|
|
root = root.parent
|
|
return root
|
|
|
|
def getTreeHeight(self):
|
|
"Get the height of the subtree rooted at self"
|
|
if len(self.children) == 0:
|
|
return 1
|
|
|
|
maxChildHeight = 0
|
|
for child in self.children:
|
|
childHeight = child.getTreeHeight()
|
|
if childHeight > maxChildHeight:
|
|
maxChildHeight = childHeight
|
|
|
|
return maxChildHeight + 1
|
|
|
|
def updateBlockInfo(self):
|
|
val = base64.b64encode(self._queryVHDBlocks())
|
|
self.setConfigUpdate(VDI.DB_VHD_BLOCKS, val)
|
|
|
|
def rename(self, uuid):
|
|
"Rename the VDI file"
|
|
assert(not self.sr.vdis.get(uuid))
|
|
self._clearRef()
|
|
oldUuid = self.uuid
|
|
self.uuid = uuid
|
|
self.children = []
|
|
# updating the children themselves is the responsiblity of the caller
|
|
del self.sr.vdis[oldUuid]
|
|
self.sr.vdis[self.uuid] = self
|
|
|
|
def delete(self):
|
|
"Physically delete the VDI"
|
|
lock.Lock.cleanup(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
|
|
self._clear()
|
|
|
|
def printTree(self, indentSize = 0):
|
|
indent = " " * indentSize
|
|
Util.log("%s%s" % (indent, self.toString()))
|
|
for child in self.children:
|
|
child.printTree(indentSize + VDI.PRINT_INDENTATION)
|
|
|
|
def toString(self):
|
|
strHidden = ""
|
|
if self.hidden:
|
|
strHidden = "*"
|
|
strSizeVHD = ""
|
|
if self._sizeVHD > 0:
|
|
strSizeVHD = Util.num2str(self._sizeVHD)
|
|
|
|
return "%s%s(%s/%s)" % (strHidden, self.uuid[0:8],
|
|
Util.num2str(self.sizeVirt), strSizeVHD)
|
|
|
|
def validate(self):
|
|
if not vhdutil.check(self.path):
|
|
raise util.SMException("VHD %s corrupted" % self.toString())
|
|
|
|
def _clear(self):
|
|
self.uuid = ""
|
|
self.path = ""
|
|
self.parentUuid = ""
|
|
self.parent = None
|
|
self._clearRef()
|
|
|
|
def _clearRef(self):
|
|
self._vdiRef = None
|
|
for kind in [XAPI.CONFIG_SM, XAPI.CONFIG_OTHER]:
|
|
self._config[kind] = None
|
|
self._configDirty[kind] = False
|
|
|
|
def _configLazyInit(self, kind):
|
|
if self._config[kind] == None:
|
|
self._config[kind] = self.sr.xapi.getConfigVDI(kind, self)
|
|
|
|
def _coalesceBegin(self):
|
|
"""Coalesce self onto parent. Only perform the actual coalescing of
|
|
VHD, but not the subsequent relinking. We'll do that as the next step,
|
|
after reloading the entire SR in case things have changed while we
|
|
were coalescing"""
|
|
self.validate()
|
|
self.parent.validate()
|
|
if self.sr.journaler.get(self.JRN_RELINK, self.uuid):
|
|
# this means we had done the actual coalescing already and just
|
|
# need to finish relinking and/or refreshing the children
|
|
Util.log("==> Coalesce apparently already done: skipping")
|
|
else:
|
|
self.parent._increaseSizeVirt(self.sizeVirt)
|
|
self._coalesceVHD(0)
|
|
self.parent.validate()
|
|
#self._verifyContents(0)
|
|
self.parent.updateBlockInfo()
|
|
|
|
def _verifyContents(self, timeOut):
|
|
Util.log(" Coalesce verification on %s" % self.toString())
|
|
abortTest = lambda:IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
|
|
Util.runAbortable(lambda: self._runTapdiskDiff(), True,
|
|
self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
|
|
Util.log(" Coalesce verification succeeded")
|
|
|
|
def _runTapdiskDiff(self):
|
|
cmd = "tapdisk-diff -n %s:%s -m %s:%s" % \
|
|
(self.getDriverName(), self.path, \
|
|
self.parent.getDriverName(), self.parent.path)
|
|
Util.doexec(cmd, 0)
|
|
return True
|
|
|
|
def _coalesceVHD(self, timeOut):
|
|
Util.log(" Running VHD coalesce on %s" % self.toString())
|
|
abortTest = lambda:IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
|
|
Util.runAbortable(lambda: vhdutil.coalesce(self.path), None,
|
|
self.sr.uuid, abortTest, VDI.POLL_INTERVAL, timeOut)
|
|
util.fistpoint.activate("LVHDRT_coalescing_VHD_data",self.sr.uuid)
|
|
|
|
def _relinkSkip(self):
|
|
"""Relink children of this VDI to point to the parent of this VDI"""
|
|
abortFlag = IPCFlag(self.sr.uuid)
|
|
for child in self.children:
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Aborting due to signal")
|
|
Util.log(" Relinking %s from %s to %s" % (child.toString(), \
|
|
self.toString(), self.parent.toString()))
|
|
util.fistpoint.activate("LVHDRT_relinking_grandchildren",self.sr.uuid)
|
|
child._setParent(self.parent)
|
|
self.children = []
|
|
|
|
def _reloadChildren(self, vdiSkip):
|
|
"""Pause & unpause all VDIs in the subtree to cause blktap to reload
|
|
the VHD metadata for this file in any online VDI"""
|
|
abortFlag = IPCFlag(self.sr.uuid)
|
|
for child in self.children:
|
|
if child == vdiSkip:
|
|
continue
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Aborting due to signal")
|
|
Util.log(" Reloading VDI %s" % child.toString())
|
|
child._reload()
|
|
|
|
def _reload(self):
|
|
"""Pause & unpause to cause blktap to reload the VHD metadata"""
|
|
for child in self.children:
|
|
child._reload()
|
|
|
|
# only leaves can be attached
|
|
if len(self.children) == 0:
|
|
try:
|
|
self.sr.xapi.atomicOp([self], "noop", {})
|
|
except XenAPI.Failure, e:
|
|
if self.sr.xapi.isInvalidHandleError(e):
|
|
Util.log("VDI %s appears to have been deleted, ignoring" % \
|
|
self.toString())
|
|
else:
|
|
raise
|
|
|
|
def _loadInfoParent(self):
|
|
ret = vhdutil.getParent(self.path, lvhdutil.extractUuid)
|
|
if ret:
|
|
self.parentUuid = ret
|
|
|
|
def _setParent(self, parent):
|
|
vhdutil.setParent(self.path, parent.path, False)
|
|
self.parent = parent
|
|
self.parentUuid = parent.uuid
|
|
parent.children.append(self)
|
|
try:
|
|
self.setConfigUpdate(self.DB_VHD_PARENT, self.parentUuid)
|
|
Util.log("Updated the vhd-parent field for child %s with %s" % \
|
|
(self.uuid, self.parentUuid))
|
|
except:
|
|
Util.log("Failed to update %s with vhd-parent field %s" % \
|
|
(self.uuid, self.parentUuid))
|
|
|
|
def _loadInfoHidden(self):
|
|
hidden = vhdutil.getHidden(self.path)
|
|
self.hidden = (hidden != 0)
|
|
|
|
def _setHidden(self, hidden = True):
|
|
vhdutil.setHidden(self.path, hidden)
|
|
self.hidden = hidden
|
|
|
|
def _increaseSizeVirt(self, size, atomic = True):
|
|
"""ensure the virtual size of 'self' is at least 'size'. Note that
|
|
resizing a VHD must always be offline and atomically: the file must
|
|
not be open by anyone and no concurrent operations may take place.
|
|
Thus we use the Agent API call for performing paused atomic
|
|
operations. If the caller is already in the atomic context, it must
|
|
call with atomic = False"""
|
|
if self.sizeVirt >= size:
|
|
return
|
|
Util.log(" Expanding VHD virt size for VDI %s: %s -> %s" % \
|
|
(self.toString(), Util.num2str(self.sizeVirt), \
|
|
Util.num2str(size)))
|
|
|
|
msize = vhdutil.getMaxResizeSize(self.path) * 1024 * 1024
|
|
if (size <= msize):
|
|
vhdutil.setSizeVirtFast(self.path, size)
|
|
else:
|
|
if atomic:
|
|
args = self._resizeArgs(size)
|
|
vdiList = self._getAllSubtree()
|
|
if not self.sr.xapi.atomicOp(vdiList, "resize", args):
|
|
raise util.SMException("Failed to resize atomically")
|
|
else:
|
|
self._setSizeVirt(size)
|
|
|
|
self.sizeVirt = vhdutil.getSizeVirt(self.path)
|
|
|
|
def _setSizeVirt(self, size):
|
|
"""WARNING: do not call this method directly unless all VDIs in the
|
|
subtree are guaranteed to be unplugged (and remain so for the duration
|
|
of the operation): this operation is only safe for offline VHDs"""
|
|
jFile = os.path.join(vhdutil.VHD_JOURNAL_LOCATION, self.uuid)
|
|
vhdutil.setSizeVirt(self.path, size, jFile)
|
|
|
|
def _queryVHDBlocks(self):
|
|
return vhdutil.getBlockBitmap(self.path)
|
|
|
|
def _getCoalescedSizeData(self):
|
|
"""Get the data size of the resulting VHD if we coalesce self onto
|
|
parent. We calculate the actual size by using the VHD block allocation
|
|
information (as opposed to just adding up the two VHD sizes to get an
|
|
upper bound)"""
|
|
# make sure we don't use stale BAT info from vdi_rec since the child
|
|
# was writable all this time
|
|
self.delConfigUpdate(VDI.DB_VHD_BLOCKS)
|
|
blocksChild = self.getVHDBlocks()
|
|
blocksParent = self.parent.getVHDBlocks()
|
|
numBlocks = Util.countBits(blocksChild, blocksParent)
|
|
Util.log("Num combined blocks = %d" % numBlocks)
|
|
sizeData = numBlocks * vhdutil.VHD_BLOCK_SIZE
|
|
assert(sizeData <= self.sizeVirt)
|
|
return sizeData
|
|
|
|
def _calcExtraSpaceForCoalescing(self):
|
|
sizeData = self._getCoalescedSizeData()
|
|
sizeCoalesced = sizeData + vhdutil.calcOverheadBitmap(sizeData) + \
|
|
vhdutil.calcOverheadEmpty(self.sizeVirt)
|
|
Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
|
|
return sizeCoalesced - self.parent.getSizeVHD()
|
|
|
|
def _calcExtraSpaceForLeafCoalescing(self):
|
|
"""How much extra space in the SR will be required to
|
|
[live-]leaf-coalesce this VDI"""
|
|
# the space requirements are the same as for inline coalesce
|
|
return self._calcExtraSpaceForCoalescing()
|
|
|
|
def _calcExtraSpaceForSnapshotCoalescing(self):
|
|
"""How much extra space in the SR will be required to
|
|
snapshot-coalesce this VDI"""
|
|
return self._calcExtraSpaceForCoalescing() + \
|
|
vhdutil.calcOverheadEmpty(self.sizeVirt) # extra snap leaf
|
|
|
|
def _getAllSubtree(self):
|
|
"""Get self and all VDIs in the subtree of self as a flat list"""
|
|
vdiList = [self]
|
|
for child in self.children:
|
|
vdiList.extend(child._getAllSubtree())
|
|
return vdiList
|
|
|
|
def _resizeArgs(self, size):
|
|
args = {
|
|
"type": self.sr.TYPE,
|
|
"uuid": self.uuid,
|
|
"path": self.path,
|
|
"size": str(size),
|
|
"srUuid": self.sr.uuid
|
|
}
|
|
return args
|
|
|
|
|
|
class FileVDI(VDI):
|
|
"""Object representing a VDI in a file-based SR (EXT or NFS)"""
|
|
|
|
FILE_SUFFIX = ".vhd"
|
|
|
|
def extractUuid(path):
|
|
path = os.path.basename(path.strip())
|
|
if not path.endswith(FileVDI.FILE_SUFFIX):
|
|
return None
|
|
uuid = path.replace(FileVDI.FILE_SUFFIX, "")
|
|
# TODO: validate UUID format
|
|
return uuid
|
|
extractUuid = staticmethod(extractUuid)
|
|
|
|
def load(self, info = None):
|
|
if not info:
|
|
if not util.pathexists(self.path):
|
|
raise util.SMException("%s not found" % self.path)
|
|
try:
|
|
info = vhdutil.getVHDInfo(self.path, self.extractUuid)
|
|
except util.SMException:
|
|
Util.log(" [VDI %s: failed to read VHD metadata]" % self.uuid)
|
|
return
|
|
self.parent = None
|
|
self.children = []
|
|
self.parentUuid = info.parentUuid
|
|
self.sizeVirt = info.sizeVirt
|
|
self._sizeVHD = info.sizePhys
|
|
self.hidden = info.hidden
|
|
self.scanError = False
|
|
self.path = os.path.join(self.sr.path, "%s%s" % \
|
|
(self.uuid, self.FILE_SUFFIX))
|
|
|
|
def rename(self, uuid):
|
|
oldPath = self.path
|
|
VDI.rename(self, uuid)
|
|
fileName = "%s%s" % (self.uuid, self.FILE_SUFFIX)
|
|
self.path = os.path.join(self.sr.path, fileName)
|
|
assert(not util.pathexists(self.path))
|
|
Util.log("Renaming %s -> %s" % (oldPath, self.path))
|
|
os.rename(oldPath, self.path)
|
|
|
|
def delete(self):
|
|
if len(self.children) > 0:
|
|
raise util.SMException("VDI %s has children, can't delete" % \
|
|
self.uuid)
|
|
try:
|
|
self.sr.lock()
|
|
try:
|
|
os.unlink(self.path)
|
|
finally:
|
|
self.sr.unlock()
|
|
except OSError:
|
|
raise util.SMException("os.unlink(%s) failed" % self.path)
|
|
self.sr.xapi.forgetVDI(self.uuid)
|
|
VDI.delete(self)
|
|
|
|
|
|
class LVHDVDI(VDI):
|
|
"""Object representing a VDI in an LVHD SR"""
|
|
|
|
JRN_ZERO = "zero" # journal entry type for zeroing out end of parent
|
|
DRIVER_NAME_RAW = "aio"
|
|
|
|
def load(self, vdiInfo):
|
|
self.parent = None
|
|
self.children = []
|
|
self._sizeVHD = -1
|
|
self.scanError = vdiInfo.scanError
|
|
self.raw = vdiInfo.vdiType == lvhdutil.VDI_TYPE_RAW
|
|
self.sizeLV = vdiInfo.sizeLV
|
|
self.sizeVirt = vdiInfo.sizeVirt
|
|
self.lvName = vdiInfo.lvName
|
|
self.lvActive = vdiInfo.lvActive
|
|
self.lvReadonly = vdiInfo.lvReadonly
|
|
self.hidden = vdiInfo.hidden
|
|
self.parentUuid = vdiInfo.parentUuid
|
|
self.path = os.path.join(self.sr.path, self.lvName)
|
|
|
|
def getDriverName(self):
|
|
if self.raw:
|
|
return self.DRIVER_NAME_RAW
|
|
return self.DRIVER_NAME_VHD
|
|
|
|
def inflate(self, size):
|
|
"""inflate the LV containing the VHD to 'size'"""
|
|
if self.raw:
|
|
return
|
|
self._activate()
|
|
self.sr.lock()
|
|
try:
|
|
lvhdutil.inflate(self.sr.journaler, self.sr.uuid, self.uuid, size)
|
|
util.fistpoint.activate("LVHDRT_inflating_the_parent",self.sr.uuid)
|
|
finally:
|
|
self.sr.unlock()
|
|
self.sizeLV = self.sr.lvmCache.getSize(self.lvName)
|
|
self._sizeVHD = -1
|
|
|
|
def deflate(self):
|
|
"""deflate the LV containing the VHD to minimum"""
|
|
if self.raw:
|
|
return
|
|
self._activate()
|
|
self.sr.lock()
|
|
try:
|
|
lvhdutil.deflate(self.sr.lvmCache, self.lvName, self.getSizeVHD())
|
|
finally:
|
|
self.sr.unlock()
|
|
self.sizeLV = self.sr.lvmCache.getSize(self.lvName)
|
|
self._sizeVHD = -1
|
|
|
|
def inflateFully(self):
|
|
self.inflate(lvhdutil.calcSizeVHDLV(self.sizeVirt))
|
|
|
|
def inflateParentForCoalesce(self):
|
|
"""Inflate the parent only as much as needed for the purposes of
|
|
coalescing"""
|
|
if self.parent.raw:
|
|
return
|
|
inc = self._calcExtraSpaceForCoalescing()
|
|
if inc > 0:
|
|
util.fistpoint.activate("LVHDRT_coalescing_before_inflate_grandparent",self.sr.uuid)
|
|
self.parent.inflate(self.parent.sizeLV + inc)
|
|
|
|
def updateBlockInfo(self):
|
|
if not self.raw:
|
|
VDI.updateBlockInfo(self)
|
|
|
|
def rename(self, uuid):
|
|
oldUuid = self.uuid
|
|
oldLVName = self.lvName
|
|
VDI.rename(self, uuid)
|
|
self.lvName = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_VHD] + self.uuid
|
|
if self.raw:
|
|
self.lvName = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_RAW] + self.uuid
|
|
self.path = os.path.join(self.sr.path, self.lvName)
|
|
assert(not self.sr.lvmCache.checkLV(self.lvName))
|
|
|
|
self.sr.lvmCache.rename(oldLVName, self.lvName)
|
|
if self.sr.lvActivator.get(oldUuid, False):
|
|
self.sr.lvActivator.replace(oldUuid, self.uuid, self.lvName, False)
|
|
|
|
ns = lvhdutil.NS_PREFIX_LVM + self.sr.uuid
|
|
(cnt, bcnt) = RefCounter.check(oldUuid, ns)
|
|
RefCounter.set(self.uuid, cnt, bcnt, ns)
|
|
RefCounter.reset(oldUuid, ns)
|
|
|
|
def delete(self):
|
|
if len(self.children) > 0:
|
|
raise util.SMException("VDI %s has children, can't delete" % \
|
|
self.uuid)
|
|
self.sr.lock()
|
|
try:
|
|
self.sr.lvmCache.remove(self.lvName)
|
|
finally:
|
|
self.sr.unlock()
|
|
RefCounter.reset(self.uuid, lvhdutil.NS_PREFIX_LVM + self.sr.uuid)
|
|
self.sr.xapi.forgetVDI(self.uuid)
|
|
VDI.delete(self)
|
|
|
|
def getSizeVHD(self):
|
|
if self._sizeVHD == -1:
|
|
self._loadInfoSizeVHD()
|
|
return self._sizeVHD
|
|
|
|
def _loadInfoSizeVHD(self):
|
|
"""Get the physical utilization of the VHD file. We do it individually
|
|
(and not using the VHD batch scanner) as an optimization: this info is
|
|
relatively expensive and we need it only for VDI's involved in
|
|
coalescing."""
|
|
if self.raw:
|
|
return
|
|
self._activate()
|
|
self._sizeVHD = vhdutil.getSizePhys(self.path)
|
|
if self._sizeVHD <= 0:
|
|
raise util.SMException("phys size of %s = %d" % \
|
|
(self.toString(), self._sizeVHD))
|
|
|
|
def _loadInfoHidden(self):
|
|
if self.raw:
|
|
self.hidden = self.sr.lvmCache.getHidden(self.lvName)
|
|
else:
|
|
VDI._loadInfoHidden(self)
|
|
|
|
def _setHidden(self, hidden = True):
|
|
if self.raw:
|
|
self.sr.lvmCache.setHidden(self.lvName, hidden)
|
|
self.hidden = hidden
|
|
else:
|
|
VDI._setHidden(self, hidden)
|
|
|
|
def toString(self):
|
|
strType = "VHD"
|
|
if self.raw:
|
|
strType = "RAW"
|
|
strHidden = ""
|
|
if self.hidden:
|
|
strHidden = "*"
|
|
strSizeVHD = ""
|
|
if self._sizeVHD > 0:
|
|
strSizeVHD = Util.num2str(self._sizeVHD)
|
|
strActive = "n"
|
|
if self.lvActive:
|
|
strActive = "a"
|
|
return "%s%s[%s](%s/%s/%s|%s)" % (strHidden, self.uuid[0:8], strType,
|
|
Util.num2str(self.sizeVirt), strSizeVHD,
|
|
Util.num2str(self.sizeLV), strActive)
|
|
|
|
def validate(self):
|
|
if not self.raw:
|
|
VDI.validate(self)
|
|
|
|
def _coalesceBegin(self):
|
|
"""LVHD parents must first be activated, inflated, and made writable"""
|
|
try:
|
|
self._activateChain()
|
|
self.sr.lvmCache.setReadonly(self.parent.lvName, False)
|
|
self.parent.validate()
|
|
self.inflateParentForCoalesce()
|
|
VDI._coalesceBegin(self)
|
|
finally:
|
|
self.parent._loadInfoSizeVHD()
|
|
self.parent.deflate()
|
|
self.sr.lvmCache.setReadonly(self.lvName, True)
|
|
|
|
def _setParent(self, parent):
|
|
self._activate()
|
|
if self.lvReadonly:
|
|
self.sr.lvmCache.setReadonly(self.lvName, False)
|
|
|
|
try:
|
|
vhdutil.setParent(self.path, parent.path, parent.raw)
|
|
finally:
|
|
if self.lvReadonly:
|
|
self.sr.lvmCache.setReadonly(self.lvName, True)
|
|
self._deactivate()
|
|
self.parent = parent
|
|
self.parentUuid = parent.uuid
|
|
parent.children.append(self)
|
|
try:
|
|
self.setConfigUpdate(self.DB_VHD_PARENT, self.parentUuid)
|
|
Util.log("Updated the vhd-parent field for child %s with %s" % \
|
|
(self.uuid, self.parentUuid))
|
|
except:
|
|
Util.log("Failed to update the vhd-parent with %s for child %s" % \
|
|
(self.parentUuid, self.uuid))
|
|
|
|
def _activate(self):
|
|
self.sr.lvActivator.activate(self.uuid, self.lvName, False)
|
|
|
|
def _activateChain(self):
|
|
vdi = self
|
|
while vdi:
|
|
vdi._activate()
|
|
vdi = vdi.parent
|
|
|
|
def _deactivate(self):
|
|
self.sr.lvActivator.deactivate(self.uuid, False)
|
|
|
|
def _increaseSizeVirt(self, size, atomic = True):
|
|
"ensure the virtual size of 'self' is at least 'size'"
|
|
self._activate()
|
|
if not self.raw:
|
|
VDI._increaseSizeVirt(self, size, atomic)
|
|
return
|
|
|
|
# raw VDI case
|
|
offset = self.sizeLV
|
|
if self.sizeVirt < size:
|
|
oldSize = self.sizeLV
|
|
self.sizeLV = util.roundup(lvutil.LVM_SIZE_INCREMENT, size)
|
|
Util.log(" Growing %s: %d->%d" % (self.path, oldSize, self.sizeLV))
|
|
self.sr.lvmCache.setSize(self.lvName, self.sizeLV)
|
|
offset = oldSize
|
|
unfinishedZero = False
|
|
jval = self.sr.journaler.get(self.JRN_ZERO, self.uuid)
|
|
if jval:
|
|
unfinishedZero = True
|
|
offset = int(jval)
|
|
length = self.sizeLV - offset
|
|
if not length:
|
|
return
|
|
|
|
if unfinishedZero:
|
|
Util.log(" ==> Redoing unfinished zeroing out")
|
|
else:
|
|
self.sr.journaler.create(self.JRN_ZERO, self.uuid, \
|
|
str(offset))
|
|
Util.log(" Zeroing %s: from %d, %dB" % (self.path, offset, length))
|
|
abortTest = lambda:IPCFlag(self.sr.uuid).test(FLAG_TYPE_ABORT)
|
|
func = lambda: util.zeroOut(self.path, offset, length)
|
|
Util.runAbortable(func, True, self.sr.uuid, abortTest,
|
|
VDI.POLL_INTERVAL, 0)
|
|
self.sr.journaler.remove(self.JRN_ZERO, self.uuid)
|
|
|
|
def _setSizeVirt(self, size):
|
|
"""WARNING: do not call this method directly unless all VDIs in the
|
|
subtree are guaranteed to be unplugged (and remain so for the duration
|
|
of the operation): this operation is only safe for offline VHDs"""
|
|
self._activate()
|
|
jFile = lvhdutil.createVHDJournalLV(self.sr.lvmCache, self.uuid,
|
|
vhdutil.MAX_VHD_JOURNAL_SIZE)
|
|
try:
|
|
lvhdutil.setSizeVirt(self.sr.journaler, self.sr.uuid, self.uuid,
|
|
size, jFile)
|
|
finally:
|
|
lvhdutil.deleteVHDJournalLV(self.sr.lvmCache, self.uuid)
|
|
|
|
def _queryVHDBlocks(self):
|
|
self._activate()
|
|
return VDI._queryVHDBlocks(self)
|
|
|
|
def _calcExtraSpaceForCoalescing(self):
|
|
if self.parent.raw:
|
|
return 0 # raw parents are never deflated in the first place
|
|
sizeCoalesced = lvhdutil.calcSizeVHDLV(self._getCoalescedSizeData())
|
|
Util.log("Coalesced size = %s" % Util.num2str(sizeCoalesced))
|
|
return sizeCoalesced - self.parent.sizeLV
|
|
|
|
def _calcExtraSpaceForLeafCoalescing(self):
|
|
"""How much extra space in the SR will be required to
|
|
[live-]leaf-coalesce this VDI"""
|
|
# we can deflate the leaf to minimize the space requirements
|
|
deflateDiff = self.sizeLV - lvhdutil.calcSizeLV(self.getSizeVHD())
|
|
return self._calcExtraSpaceForCoalescing() - deflateDiff
|
|
|
|
def _calcExtraSpaceForSnapshotCoalescing(self):
|
|
return self._calcExtraSpaceForCoalescing() + \
|
|
lvhdutil.calcSizeLV(self.getSizeVHD())
|
|
|
|
def _resizeArgs(self, size):
|
|
args = VDI._resizeArgs(self, size)
|
|
args["vgName"] = self.sr.vgName
|
|
args["lvSize"] = str(self.sizeLV)
|
|
return args
|
|
|
|
|
|
|
|
################################################################################
|
|
#
|
|
# SR
|
|
#
|
|
class SR:
|
|
TYPE_FILE = "file"
|
|
TYPE_LVHD = "lvhd"
|
|
TYPES = [TYPE_LVHD, TYPE_FILE]
|
|
|
|
LOCK_RETRY_INTERVAL = 3
|
|
LOCK_RETRY_ATTEMPTS = 20
|
|
LOCK_RETRY_ATTEMPTS_LOCK = 100
|
|
|
|
SCAN_RETRY_ATTEMPTS = 3
|
|
|
|
JRN_CLONE = "clone" # journal entry type for the clone operation (from SM)
|
|
TMP_RENAME_PREFIX = "OLD_"
|
|
|
|
KEY_OFFLINE_COALESCE_NEEDED = "leaf_coalesce_need_offline"
|
|
KEY_OFFLINE_COALESCE_OVERRIDE = "leaf_coalesce_offline_override"
|
|
|
|
def getInstance(uuid, xapiSession):
|
|
xapi = XAPI(xapiSession, uuid)
|
|
type = normalizeType(xapi.srRecord["type"])
|
|
if type == SR.TYPE_FILE:
|
|
return FileSR(uuid, xapi)
|
|
elif type == SR.TYPE_LVHD:
|
|
return LVHDSR(uuid, xapi)
|
|
raise util.SMException("SR type %s not recognized" % type)
|
|
getInstance = staticmethod(getInstance)
|
|
|
|
def __init__(self, uuid, xapi):
|
|
self.uuid = uuid
|
|
self.path = ""
|
|
self.name = ""
|
|
self.vdis = {}
|
|
self.vdiTrees = []
|
|
self.journaler = None
|
|
self.xapi = xapi
|
|
self._locked = 0
|
|
self._srLock = lock.Lock(vhdutil.LOCK_TYPE_SR, self.uuid)
|
|
self.name = unicode(self.xapi.srRecord["name_label"]).encode("utf-8", "replace")
|
|
self._failedCoalesceTargets = []
|
|
|
|
if not self.xapi.isPluggedHere():
|
|
raise util.SMException("SR %s not attached on this host" % uuid)
|
|
|
|
if not self.xapi.isMaster():
|
|
raise util.SMException("This host is NOT master, will not run")
|
|
|
|
def scan(self, force = False):
|
|
"""Scan the SR and load VDI info for each VDI. If called repeatedly,
|
|
update VDI objects if they already exist"""
|
|
pass # abstract
|
|
|
|
def scanLocked(self, force = False):
|
|
self.lock()
|
|
try:
|
|
self.scan(force)
|
|
finally:
|
|
self.unlock()
|
|
|
|
def getVDI(self, uuid):
|
|
return self.vdis.get(uuid)
|
|
|
|
def hasWork(self):
|
|
if len(self.findGarbage()) > 0:
|
|
return True
|
|
if self.findCoalesceable():
|
|
return True
|
|
if self.findLeafCoalesceable():
|
|
return True
|
|
if self.needUpdateBlockInfo():
|
|
return True
|
|
return False
|
|
|
|
def findCoalesceable(self):
|
|
"""Find a coalesceable VDI. Return a vdi that should be coalesced
|
|
(choosing one among all coalesceable candidates according to some
|
|
criteria) or None if there is no VDI that could be coalesced"""
|
|
# finish any VDI for which a relink journal entry exists first
|
|
journals = self.journaler.getAll(VDI.JRN_RELINK)
|
|
for uuid in journals.iterkeys():
|
|
vdi = self.getVDI(uuid)
|
|
if vdi and vdi not in self._failedCoalesceTargets:
|
|
return vdi
|
|
|
|
candidates = []
|
|
for vdi in self.vdis.values():
|
|
if vdi.isCoalesceable() and vdi not in self._failedCoalesceTargets:
|
|
candidates.append(vdi)
|
|
|
|
# pick one in the tallest tree
|
|
treeHeight = dict()
|
|
for c in candidates:
|
|
height = c.getTreeRoot().getTreeHeight()
|
|
if treeHeight.get(height):
|
|
treeHeight[height].append(c)
|
|
else:
|
|
treeHeight[height] = [c]
|
|
|
|
freeSpace = self.getFreeSpace()
|
|
heights = treeHeight.keys()
|
|
heights.sort(reverse=True)
|
|
for h in heights:
|
|
for c in treeHeight[h]:
|
|
spaceNeeded = c._calcExtraSpaceForCoalescing()
|
|
if spaceNeeded <= freeSpace:
|
|
Util.log("Coalesce candidate: %s (tree height %d)" % \
|
|
(c.toString(), h))
|
|
return c
|
|
else:
|
|
Util.log("No space to coalesce %s (free space: %d)" % \
|
|
(c.toString(), freeSpace))
|
|
return None
|
|
|
|
def findLeafCoalesceable(self):
|
|
"""Find leaf-coalesceable VDIs in each VHD tree"""
|
|
candidates = []
|
|
for vdi in self.vdis.values():
|
|
if not vdi.isLeafCoalesceable():
|
|
continue
|
|
if vdi in self._failedCoalesceTargets:
|
|
continue
|
|
if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_DISABLED:
|
|
Util.log("Leaf-coalesce disabled for %s" % vdi.toString())
|
|
continue
|
|
if not (AUTO_ONLINE_LEAF_COALESCE_ENABLED or \
|
|
vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE):
|
|
continue
|
|
candidates.append(vdi)
|
|
|
|
freeSpace = self.getFreeSpace()
|
|
for candidate in candidates:
|
|
# check the space constraints to see if leaf-coalesce is actually
|
|
# feasible for this candidate
|
|
spaceNeeded = candidate._calcExtraSpaceForSnapshotCoalescing()
|
|
spaceNeededLive = spaceNeeded
|
|
if spaceNeeded > freeSpace:
|
|
spaceNeededLive = candidate._calcExtraSpaceForLeafCoalescing()
|
|
if candidate.canLiveCoalesce():
|
|
spaceNeeded = spaceNeededLive
|
|
if spaceNeeded <= freeSpace:
|
|
Util.log("Leaf-coalesce candidate: %s" % candidate.toString())
|
|
return candidate
|
|
else:
|
|
Util.log("No space to leaf-coalesce %s (free space: %d)" % \
|
|
(candidate.toString(), freeSpace))
|
|
if spaceNeededLive <= freeSpace:
|
|
Util.log("...but enough space if skip snap-coalesce")
|
|
candidate.setConfigUpdate(VDI.DB_LEAFCLSC,
|
|
VDI.LEAFCLSC_OFFLINE)
|
|
|
|
return None
|
|
|
|
def coalesce(self, vdi, dryRun):
|
|
"""Coalesce vdi onto parent"""
|
|
Util.log("Coalescing %s -> %s" % \
|
|
(vdi.toString(), vdi.parent.toString()))
|
|
if dryRun:
|
|
return
|
|
|
|
try:
|
|
self._coalesce(vdi)
|
|
except util.SMException, e:
|
|
if isinstance(e, AbortException):
|
|
self.cleanup()
|
|
raise
|
|
else:
|
|
self._failedCoalesceTargets.append(vdi)
|
|
Util.logException("coalesce")
|
|
Util.log("Coalesce failed, skipping")
|
|
self.cleanup()
|
|
|
|
def coalesceLeaf(self, vdi, dryRun):
|
|
"""Leaf-coalesce vdi onto parent"""
|
|
Util.log("Leaf-coalescing %s -> %s" % \
|
|
(vdi.toString(), vdi.parent.toString()))
|
|
if dryRun:
|
|
return
|
|
|
|
try:
|
|
try:
|
|
self._coalesceLeaf(vdi)
|
|
finally:
|
|
vdi.delConfigUpdate(vdi.DB_LEAFCLSC)
|
|
except (util.SMException, XenAPI.Failure), e:
|
|
if isinstance(e, AbortException):
|
|
self.cleanup()
|
|
raise
|
|
else:
|
|
self._failedCoalesceTargets.append(vdi)
|
|
Util.logException("leaf-coalesce")
|
|
Util.log("Leaf-coalesce failed, skipping")
|
|
self.cleanup()
|
|
|
|
def garbageCollect(self, dryRun = False):
|
|
vdiList = self.findGarbage()
|
|
Util.log("Found %d VDIs for deletion:" % len(vdiList))
|
|
for vdi in vdiList:
|
|
Util.log(" %s" % vdi.toString())
|
|
if not dryRun:
|
|
self.deleteVDIs(vdiList)
|
|
self.cleanupJournals(dryRun)
|
|
|
|
def findGarbage(self):
|
|
vdiList = []
|
|
for vdi in self.vdiTrees:
|
|
vdiList.extend(vdi.getAllPrunable())
|
|
return vdiList
|
|
|
|
def deleteVDIs(self, vdiList):
|
|
for vdi in vdiList:
|
|
if IPCFlag(self.uuid).test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Aborting due to signal")
|
|
Util.log("Deleting unlinked VDI %s" % vdi.toString())
|
|
self.deleteVDI(vdi)
|
|
|
|
def deleteVDI(self, vdi):
|
|
assert(len(vdi.children) == 0)
|
|
del self.vdis[vdi.uuid]
|
|
if vdi.parent:
|
|
vdi.parent.children.remove(vdi)
|
|
if vdi in self.vdiTrees:
|
|
self.vdiTrees.remove(vdi)
|
|
vdi.delete()
|
|
|
|
def getFreeSpace(self):
|
|
return 0
|
|
|
|
def cleanup(self):
|
|
Util.log("In cleanup")
|
|
return
|
|
|
|
def toString(self):
|
|
if self.name:
|
|
ret = "%s ('%s')" % (self.uuid[0:4], self.name)
|
|
else:
|
|
ret = "%s" % self.uuid
|
|
return ret
|
|
|
|
def printVDIs(self):
|
|
Util.log("-- SR %s has %d VDIs (%d VHD trees) --" % \
|
|
(self.toString(), len(self.vdis), len(self.vdiTrees)))
|
|
for vdi in self.vdiTrees:
|
|
vdi.printTree()
|
|
|
|
def lock(self):
|
|
"""Acquire the SR lock. Nested acquire()'s are ok. Check for Abort
|
|
signal to avoid deadlocking (trying to acquire the SR lock while the
|
|
lock is held by a process that is trying to abort us)"""
|
|
self._locked += 1
|
|
if self._locked > 1:
|
|
return
|
|
|
|
abortFlag = IPCFlag(self.uuid)
|
|
for i in range(SR.LOCK_RETRY_ATTEMPTS_LOCK):
|
|
if self._srLock.acquireNoblock():
|
|
return
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Abort requested")
|
|
time.sleep(SR.LOCK_RETRY_INTERVAL)
|
|
raise util.SMException("Unable to acquire the SR lock")
|
|
|
|
def unlock(self):
|
|
assert(self._locked > 0)
|
|
self._locked -= 1
|
|
if self._locked == 0:
|
|
self._srLock.release()
|
|
|
|
def needUpdateBlockInfo(self):
|
|
for vdi in self.vdis.values():
|
|
if vdi.scanError or len(vdi.children) == 0:
|
|
continue
|
|
if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
|
|
return True
|
|
return False
|
|
|
|
def updateBlockInfo(self):
|
|
for vdi in self.vdis.values():
|
|
if vdi.scanError or len(vdi.children) == 0:
|
|
continue
|
|
if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
|
|
vdi.updateBlockInfo()
|
|
|
|
def cleanupCoalesceJournals(self):
|
|
"""Remove stale coalesce VDI indicators"""
|
|
entries = self.journaler.getAll(VDI.JRN_COALESCE)
|
|
for uuid, jval in entries.iteritems():
|
|
self.journaler.remove(VDI.JRN_COALESCE, uuid)
|
|
|
|
def cleanupJournals(self, dryRun):
|
|
"""delete journal entries for non-existing VDIs"""
|
|
for t in [LVHDVDI.JRN_ZERO, VDI.JRN_RELINK, SR.JRN_CLONE]:
|
|
entries = self.journaler.getAll(t)
|
|
for uuid, jval in entries.iteritems():
|
|
if self.getVDI(uuid):
|
|
continue
|
|
if t == SR.JRN_CLONE:
|
|
baseUuid, clonUuid = jval.split("_")
|
|
if self.getVDI(baseUuid):
|
|
continue
|
|
Util.log(" Deleting stale '%s' journal entry for %s "
|
|
"(%s)" % (t, uuid, jval))
|
|
if not dryRun:
|
|
self.journaler.remove(t, uuid)
|
|
|
|
def _coalesce(self, vdi):
|
|
# JRN_COALESCE is used to check which VDI is being coalesced in order
|
|
# to decide whether to abort the coalesce. We remove the journal as
|
|
# soon as the VHD coalesce step is done, because we don't expect the
|
|
# rest of the process to take long
|
|
self.journaler.create(vdi.JRN_COALESCE, vdi.uuid, "1")
|
|
vdi._coalesceBegin()
|
|
self.journaler.remove(vdi.JRN_COALESCE, vdi.uuid)
|
|
|
|
util.fistpoint.activate("LVHDRT_before_create_relink_journal",self.uuid)
|
|
|
|
# we now need to relink the children: lock the SR to prevent ops like
|
|
# SM.clone from manipulating the VDIs we'll be relinking and rescan the
|
|
# SR first in case the children changed since the last scan
|
|
if not self.journaler.get(vdi.JRN_RELINK, vdi.uuid):
|
|
self.journaler.create(vdi.JRN_RELINK, vdi.uuid, "1")
|
|
|
|
self.lock()
|
|
try:
|
|
self.scan()
|
|
vdi._relinkSkip()
|
|
finally:
|
|
self.unlock()
|
|
|
|
vdi.parent._reloadChildren(vdi)
|
|
self.journaler.remove(vdi.JRN_RELINK, vdi.uuid)
|
|
|
|
def _coalesceLeaf(self, vdi):
|
|
"""Leaf-coalesce VDI vdi. Return true if we succeed, false if we cannot
|
|
complete due to external changes, namely vdi_delete and vdi_snapshot
|
|
that alter leaf-coalescibility of vdi"""
|
|
while not vdi.canLiveCoalesce():
|
|
prevSizeVHD = vdi.getSizeVHD()
|
|
if not self._snapshotCoalesce(vdi):
|
|
return False
|
|
if vdi.getSizeVHD() >= prevSizeVHD:
|
|
Util.log("Snapshot-coalesce did not help, abandoning attempts")
|
|
vdi.setConfigUpdate(vdi.DB_LEAFCLSC, vdi.LEAFCLSC_OFFLINE)
|
|
break
|
|
return self._liveLeafCoalesce(vdi)
|
|
|
|
def _snapshotCoalesce(self, vdi):
|
|
# Note that because we are not holding any locks here, concurrent SM
|
|
# operations may change this tree under our feet. In particular, vdi
|
|
# can be deleted, or it can be snapshotted.
|
|
assert(AUTO_ONLINE_LEAF_COALESCE_ENABLED)
|
|
Util.log("Single-snapshotting %s" % vdi.toString())
|
|
util.fistpoint.activate("LVHDRT_coaleaf_delay_1", self.uuid)
|
|
try:
|
|
ret = self.xapi.singleSnapshotVDI(vdi)
|
|
Util.log("Single-snapshot returned: %s" % ret)
|
|
except XenAPI.Failure, e:
|
|
if self.xapi.isInvalidHandleError(e):
|
|
Util.log("The VDI appears to have been concurrently deleted")
|
|
return False
|
|
raise
|
|
self.scanLocked()
|
|
tempSnap = vdi.parent
|
|
if not tempSnap.isCoalesceable():
|
|
Util.log("The VDI appears to have been concurrently snapshotted")
|
|
return False
|
|
Util.log("Coalescing parent %s" % tempSnap.toString())
|
|
util.fistpoint.activate("LVHDRT_coaleaf_delay_2", self.uuid)
|
|
self._coalesce(tempSnap)
|
|
self.deleteVDI(tempSnap)
|
|
if not vdi.isLeafCoalesceable():
|
|
Util.log("The VDI tree appears to have been altered since")
|
|
return False
|
|
return True
|
|
|
|
def _liveLeafCoalesce(self, vdi):
|
|
args = {"srUuid": self.uuid, "vdiUuid": vdi.uuid}
|
|
util.fistpoint.activate("LVHDRT_coaleaf_delay_3", self.uuid)
|
|
try:
|
|
if not self.xapi.atomicOp([vdi], "coalesce_leaf", args, True):
|
|
Util.log("%s is no longer leaf-coalesceable" % vdi.toString())
|
|
return False
|
|
except XenAPI.Failure, e:
|
|
if self.xapi.isInvalidHandleError(e):
|
|
Util.log("The VDI appears to have been deleted meanwhile")
|
|
return False
|
|
self.scanLocked()
|
|
return True
|
|
|
|
def _doCoalesceLeaf(self, vdi):
|
|
pass # abstract
|
|
|
|
def _removeStaleVDIs(self, uuidsPresent):
|
|
for uuid in self.vdis.keys():
|
|
if not uuid in uuidsPresent:
|
|
Util.log("VDI %s disappeared since last scan" % \
|
|
self.vdis[uuid].toString())
|
|
del self.vdis[uuid]
|
|
|
|
def _buildTree(self, force):
|
|
self.vdiTrees = []
|
|
for vdi in self.vdis.values():
|
|
if vdi.parentUuid:
|
|
parent = self.getVDI(vdi.parentUuid)
|
|
if not parent:
|
|
if vdi.uuid.startswith(self.TMP_RENAME_PREFIX):
|
|
self.vdiTrees.append(vdi)
|
|
continue
|
|
if force:
|
|
Util.log("ERROR: Parent VDI %s not found! (for %s)" % \
|
|
(vdi.parentUuid, vdi.uuid))
|
|
self.vdiTrees.append(vdi)
|
|
continue
|
|
else:
|
|
raise util.SMException("Parent VDI %s of %s not " \
|
|
"found" % (vdi.parentUuid, vdi.uuid))
|
|
vdi.parent = parent
|
|
parent.children.append(vdi)
|
|
else:
|
|
self.vdiTrees.append(vdi)
|
|
|
|
|
|
class FileSR(SR):
|
|
TYPE = SR.TYPE_FILE
|
|
|
|
def __init__(self, uuid, xapi):
|
|
SR.__init__(self, uuid, xapi)
|
|
self.path = "/var/run/sr-mount/%s" % self.uuid
|
|
self.journaler = fjournaler.Journaler(self.path)
|
|
|
|
def scan(self, force = False):
|
|
if not util.pathexists(self.path):
|
|
raise util.SMException("directory %s not found!" % self.uuid)
|
|
vhds = self._scan(force)
|
|
for uuid, vhdInfo in vhds.iteritems():
|
|
vdi = self.getVDI(uuid)
|
|
if not vdi:
|
|
Util.log("Found new VDI when scanning: %s" % uuid)
|
|
vdi = FileVDI(self, uuid)
|
|
self.vdis[uuid] = vdi
|
|
vdi.load(vhdInfo)
|
|
self._removeStaleVDIs(vhds.keys())
|
|
self._buildTree(force)
|
|
self.printVDIs()
|
|
|
|
def getFreeSpace(self):
|
|
return util.get_fs_size(self.path) - util.get_fs_utilisation(self.path)
|
|
|
|
def findLeafCoalesceable(self):
|
|
return None # not implemented for FileSR
|
|
|
|
def _scan(self, force):
|
|
for i in range(SR.SCAN_RETRY_ATTEMPTS):
|
|
error = False
|
|
pattern = os.path.join(self.path, "*%s" % FileVDI.FILE_SUFFIX)
|
|
vhds = vhdutil.getAllVHDs(pattern, FileVDI.extractUuid)
|
|
for uuid, vhdInfo in vhds.iteritems():
|
|
if vhdInfo.error:
|
|
error = True
|
|
break
|
|
if not error:
|
|
return vhds
|
|
Util.log("Scan error on attempt %d" % i)
|
|
if force:
|
|
return vhds
|
|
raise util.SMException("Scan error")
|
|
|
|
def deleteVDI(self, vdi):
|
|
self._checkSlaves(vdi)
|
|
SR.deleteVDI(self, vdi)
|
|
|
|
def _checkSlaves(self, vdi):
|
|
onlineHosts = self.xapi.getOnlineHosts()
|
|
abortFlag = IPCFlag(self.uuid)
|
|
for pbdRecord in self.xapi.getAttachedPBDs():
|
|
hostRef = pbdRecord["host"]
|
|
if hostRef == self.xapi._hostRef:
|
|
continue
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Aborting due to signal")
|
|
try:
|
|
self._checkSlave(hostRef, vdi)
|
|
except util.CommandException:
|
|
if onlineHosts.__contains__(hostRef):
|
|
raise
|
|
|
|
def _checkSlave(self, hostRef, vdi):
|
|
call = (hostRef, "nfs-on-slave", "check", { 'path': vdi.path })
|
|
Util.log("Checking with slave: %s" % repr(call))
|
|
_host = self.xapi.session.xenapi.host
|
|
text = _host.call_plugin(*call)
|
|
|
|
class LVHDSR(SR):
|
|
TYPE = SR.TYPE_LVHD
|
|
SUBTYPES = ["lvhdoiscsi", "lvhdohba"]
|
|
|
|
def __init__(self, uuid, xapi):
|
|
SR.__init__(self, uuid, xapi)
|
|
self.vgName = "%s%s" % (lvhdutil.VG_PREFIX, self.uuid)
|
|
self.path = os.path.join(lvhdutil.VG_LOCATION, self.vgName)
|
|
self.lvmCache = lvmcache.LVMCache(self.vgName)
|
|
self.lvActivator = LVActivator(self.uuid, self.lvmCache)
|
|
self.journaler = journaler.Journaler(self.lvmCache)
|
|
|
|
def deleteVDI(self, vdi):
|
|
if self.lvActivator.get(vdi.uuid, False):
|
|
self.lvActivator.deactivate(vdi.uuid, False)
|
|
self._checkSlaves(vdi)
|
|
SR.deleteVDI(self, vdi)
|
|
|
|
def getFreeSpace(self):
|
|
stats = lvutil._getVGstats(self.vgName)
|
|
return stats['physical_size'] - stats['physical_utilisation']
|
|
|
|
def cleanup(self):
|
|
if not self.lvActivator.deactivateAll():
|
|
Util.log("ERROR deactivating LVs while cleaning up")
|
|
|
|
def needUpdateBlockInfo(self):
|
|
for vdi in self.vdis.values():
|
|
if vdi.scanError or vdi.raw or len(vdi.children) == 0:
|
|
continue
|
|
if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
|
|
return True
|
|
return False
|
|
|
|
def updateBlockInfo(self):
|
|
for vdi in self.vdis.values():
|
|
if vdi.scanError or vdi.raw or len(vdi.children) == 0:
|
|
continue
|
|
if not vdi.getConfig(vdi.DB_VHD_BLOCKS):
|
|
vdi.updateBlockInfo()
|
|
|
|
def scan(self, force = False):
|
|
vdis = self._scan(force)
|
|
for uuid, vdiInfo in vdis.iteritems():
|
|
vdi = self.getVDI(uuid)
|
|
if not vdi:
|
|
Util.log("Found new VDI when scanning: %s" % uuid)
|
|
vdi = LVHDVDI(self, uuid)
|
|
self.vdis[uuid] = vdi
|
|
vdi.load(vdiInfo)
|
|
self._removeStaleVDIs(vdis.keys())
|
|
self._buildTree(force)
|
|
self.printVDIs()
|
|
self._handleInterruptedCoalesceLeaf()
|
|
|
|
def _scan(self, force):
|
|
for i in range(SR.SCAN_RETRY_ATTEMPTS):
|
|
error = False
|
|
self.lvmCache.refresh()
|
|
vdis = lvhdutil.getVDIInfo(self.lvmCache)
|
|
for uuid, vdiInfo in vdis.iteritems():
|
|
if vdiInfo.scanError:
|
|
error = True
|
|
break
|
|
if not error:
|
|
return vdis
|
|
Util.log("Scan error, retrying (%d)" % i)
|
|
if force:
|
|
return vdis
|
|
raise util.SMException("Scan error")
|
|
|
|
def _liveLeafCoalesce(self, vdi):
|
|
"""If the parent is raw and the child was resized (virt. size), then
|
|
we'll need to resize the parent, which can take a while due to zeroing
|
|
out of the extended portion of the LV. Do it before pausing the child
|
|
to avoid a protracted downtime"""
|
|
if vdi.parent.raw and vdi.sizeVirt > vdi.parent.sizeVirt:
|
|
self.lvmCache.setReadonly(vdi.parent.lvName, False)
|
|
vdi.parent._increaseSizeVirt(vdi.sizeVirt)
|
|
|
|
parentUuid = vdi.parent.uuid
|
|
if not SR._liveLeafCoalesce(self, vdi):
|
|
return False
|
|
|
|
# fix the activation records after the UUIDs have been changed
|
|
if self.lvActivator.get(parentUuid, False):
|
|
self.lvActivator.replace(parentUuid, vdi.uuid, vdi.lvName, False)
|
|
else:
|
|
self.lvActivator.remove(vdi.uuid, False)
|
|
return True
|
|
|
|
def _doCoalesceLeaf(self, vdi):
|
|
"""Actual coalescing of a leaf VDI onto parent. Must be called in an
|
|
offline/atomic context"""
|
|
vdi._activateChain()
|
|
self.journaler.create(VDI.JRN_LEAF, vdi.uuid, vdi.parent.uuid)
|
|
self.lvmCache.setReadonly(vdi.parent.lvName, False)
|
|
vdi.parent._setHidden(False)
|
|
vdi.deflate()
|
|
vdi.inflateParentForCoalesce()
|
|
vdi.parent._increaseSizeVirt(vdi.sizeVirt, False)
|
|
vdi.validate()
|
|
vdi.parent.validate()
|
|
util.fistpoint.activate("LVHDRT_coaleaf_before_coalesce", self.uuid)
|
|
timeout = vdi.LIVE_LEAF_COALESCE_TIMEOUT
|
|
if vdi.getConfig(vdi.DB_LEAFCLSC) == vdi.LEAFCLSC_FORCE:
|
|
Util.log("Leaf-coalesce forced, will not use timeout")
|
|
timeout = 0
|
|
vdi._coalesceVHD(timeout)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_after_coalesce", self.uuid)
|
|
vdi.parent.validate()
|
|
#vdi._verifyContents(timeout / 2)
|
|
|
|
# rename
|
|
vdiUuid = vdi.uuid
|
|
oldNameLV = vdi.lvName
|
|
origParentUuid = vdi.parent.uuid
|
|
vdi.rename(self.TMP_RENAME_PREFIX + vdiUuid)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_one_renamed", self.uuid)
|
|
vdi.parent.rename(vdiUuid)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_both_renamed", self.uuid)
|
|
self._updateSlavesOnRename(vdi.parent, oldNameLV)
|
|
|
|
# Note that "vdi.parent" is now the single remaining leaf and "vdi" is
|
|
# garbage
|
|
|
|
# update the VDI record
|
|
vdi.parent.delConfig(VDI.DB_VHD_PARENT)
|
|
if vdi.parent.raw:
|
|
vdi.parent.setConfig(VDI.DB_VDI_TYPE, lvhdutil.VDI_TYPE_RAW)
|
|
vdi.parent.delConfig(VDI.DB_VHD_BLOCKS)
|
|
vdi.parent.updateConfig()
|
|
util.fistpoint.activate("LVHDRT_coaleaf_after_vdirec", self.uuid)
|
|
|
|
# fix the refcounts: the remaining node should inherit the binary
|
|
# refcount from the leaf (because if it was online, it should remain
|
|
# refcounted as such), but the normal refcount from the parent (because
|
|
# this node is really the parent node) - minus 1 if it is online (since
|
|
# non-leaf nodes increment their normal counts when they are online and
|
|
# we are now a leaf, storing that 1 in the binary refcount).
|
|
ns = lvhdutil.NS_PREFIX_LVM + self.uuid
|
|
cCnt, cBcnt = RefCounter.check(vdi.uuid, ns)
|
|
pCnt, pBcnt = RefCounter.check(vdi.parent.uuid, ns)
|
|
pCnt = pCnt - cBcnt
|
|
assert(pCnt >= 0)
|
|
RefCounter.set(vdi.parent.uuid, pCnt, cBcnt, ns)
|
|
|
|
# delete the obsolete leaf & inflate the parent (in that order, to
|
|
# minimize free space requirements)
|
|
parent = vdi.parent
|
|
vdi._setHidden(True)
|
|
vdi.parent.children = []
|
|
vdi.parent = None
|
|
util.fistpoint.activate("LVHDRT_coaleaf_before_delete", self.uuid)
|
|
self.deleteVDI(vdi)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_after_delete", self.uuid)
|
|
self.xapi.forgetVDI(origParentUuid)
|
|
parent.inflateFully()
|
|
|
|
util.fistpoint.activate("LVHDRT_coaleaf_before_remove_j", self.uuid)
|
|
self.journaler.remove(VDI.JRN_LEAF, vdiUuid)
|
|
|
|
def _handleInterruptedCoalesceLeaf(self):
|
|
"""An interrupted leaf-coalesce operation may leave the VHD tree in an
|
|
inconsistent state. If the old-leaf VDI is still present, we revert the
|
|
operation (in case the original error is persistent); otherwise we must
|
|
finish the operation"""
|
|
entries = self.journaler.getAll(VDI.JRN_LEAF)
|
|
for uuid, parentUuid in entries.iteritems():
|
|
childLV = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_VHD] + uuid
|
|
tmpChildLV = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_VHD] + \
|
|
self.TMP_RENAME_PREFIX + uuid
|
|
parentLV1 = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_VHD] + parentUuid
|
|
parentLV2 = lvhdutil.LV_PREFIX[lvhdutil.VDI_TYPE_RAW] + parentUuid
|
|
parentPresent = (self.lvmCache.checkLV(parentLV1) or \
|
|
self.lvmCache.checkLV(parentLV2))
|
|
if parentPresent or self.lvmCache.checkLV(tmpChildLV):
|
|
self._undoInterruptedCoalesceLeaf(uuid, parentUuid)
|
|
else:
|
|
self._finishInterruptedCoalesceLeaf(uuid, parentUuid)
|
|
self.journaler.remove(VDI.JRN_LEAF, uuid)
|
|
|
|
def _undoInterruptedCoalesceLeaf(self, childUuid, parentUuid):
|
|
Util.log("*** UNDO LEAF-COALESCE")
|
|
parent = self.getVDI(parentUuid)
|
|
if not parent:
|
|
parent = self.getVDI(childUuid)
|
|
if not parent:
|
|
raise util.SMException("Neither %s nor %s found" % \
|
|
(parentUuid, childUuid))
|
|
Util.log("Renaming parent back: %s -> %s" % (childUuid, parentUuid))
|
|
parent.rename(parentUuid)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename", self.uuid)
|
|
|
|
child = self.getVDI(childUuid)
|
|
if not child:
|
|
child = self.getVDI(self.TMP_RENAME_PREFIX + childUuid)
|
|
if not child:
|
|
raise util.SMException("Neither %s nor %s found" % \
|
|
(childUuid, self.TMP_RENAME_PREFIX + childUuid))
|
|
Util.log("Renaming child back to %s" % childUuid)
|
|
child.rename(childUuid)
|
|
Util.log("Updating the VDI record")
|
|
child.setConfig(VDI.DB_VHD_PARENT, parentUuid)
|
|
child.setConfig(VDI.DB_VDI_TYPE, lvhdutil.VDI_TYPE_VHD)
|
|
child.updateConfig()
|
|
util.fistpoint.activate("LVHDRT_coaleaf_undo_after_rename2", self.uuid)
|
|
|
|
# refcount (best effort - assume that it had succeeded if the
|
|
# second rename succeeded; if not, this adjustment will be wrong,
|
|
# leading to a non-deactivation of the LV)
|
|
ns = lvhdutil.NS_PREFIX_LVM + self.uuid
|
|
cCnt, cBcnt = RefCounter.check(child.uuid, ns)
|
|
pCnt, pBcnt = RefCounter.check(parent.uuid, ns)
|
|
pCnt = pCnt + cBcnt
|
|
RefCounter.set(parent.uuid, pCnt, 0, ns)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_undo_after_refcount", self.uuid)
|
|
|
|
parent.deflate()
|
|
child.inflateFully()
|
|
util.fistpoint.activate("LVHDRT_coaleaf_undo_after_deflate", self.uuid)
|
|
if child.hidden:
|
|
child._setHidden(False)
|
|
if not parent.hidden:
|
|
parent._setHidden(True)
|
|
if not parent.lvReadonly:
|
|
self.lvmCache.setReadonly(parent.lvName, True)
|
|
util.fistpoint.activate("LVHDRT_coaleaf_undo_end", self.uuid)
|
|
Util.log("*** leaf-coalesce undo successful")
|
|
if util.fistpoint.is_active("LVHDRT_coaleaf_stop_after_recovery"):
|
|
child.setConfigUpdate(VDI.DB_LEAFCLSC, VDI.LEAFCLSC_DISABLED)
|
|
|
|
def _finishInterruptedCoalesceLeaf(self, childUuid, parentUuid):
|
|
Util.log("*** FINISH LEAF-COALESCE")
|
|
vdi = self.getVDI(childUuid)
|
|
if not vdi:
|
|
raise util.SMException("VDI %s not found" % childUuid)
|
|
vdi.inflateFully()
|
|
util.fistpoint.activate("LVHDRT_coaleaf_finish_after_inflate", self.uuid)
|
|
try:
|
|
self.xapi.forgetVDI(parentUuid)
|
|
except XenAPI.Failure:
|
|
pass
|
|
util.fistpoint.activate("LVHDRT_coaleaf_finish_end", self.uuid)
|
|
Util.log("*** finished leaf-coalesce successfully")
|
|
|
|
def _checkSlaves(self, vdi):
|
|
"""Confirm with all slaves in the pool that 'vdi' is not in use. We
|
|
try to check all slaves, including those that the Agent believes are
|
|
offline, but ignore failures for offline hosts. This is to avoid cases
|
|
where the Agent thinks a host is offline but the host is up."""
|
|
args = {"vgName" : self.vgName,
|
|
"action1": "deactivateNoRefcount",
|
|
"lvName1": vdi.lvName,
|
|
"action2": "cleanupLock",
|
|
"uuid2" : vdi.uuid,
|
|
"ns2" : lvhdutil.NS_PREFIX_LVM + self.uuid}
|
|
onlineHosts = self.xapi.getOnlineHosts()
|
|
abortFlag = IPCFlag(self.uuid)
|
|
for pbdRecord in self.xapi.getAttachedPBDs():
|
|
hostRef = pbdRecord["host"]
|
|
if hostRef == self.xapi._hostRef:
|
|
continue
|
|
if abortFlag.test(FLAG_TYPE_ABORT):
|
|
raise AbortException("Aborting due to signal")
|
|
Util.log("Checking with slave %s (path %s)" % (hostRef, vdi.path))
|
|
try:
|
|
self.xapi.ensureInactive(hostRef, args)
|
|
except XenAPI.Failure:
|
|
if onlineHosts.__contains__(hostRef):
|
|
raise
|
|
|
|
def _updateSlavesOnRename(self, vdi, oldNameLV):
|
|
activeVBDs = util.get_attached_VBDs(self.xapi.session, vdi.uuid)
|
|
slaves = util.get_hosts(self.xapi.session, activeVBDs)
|
|
pool = self.xapi.session.xenapi.pool.get_all()[0]
|
|
master = self.xapi.session.xenapi.pool.get_master(pool)
|
|
if master in slaves:
|
|
slaves.remove(master)
|
|
if len(slaves) == 0:
|
|
Util.log("VDI %s not attached on any slave" % vdi.toString())
|
|
return
|
|
|
|
util.SMlog("Updating %s to %s on %d slaves:" % \
|
|
(oldNameLV, vdi.lvName, len(slaves)))
|
|
for slave in slaves:
|
|
util.SMlog("Updating slave %s" % slave)
|
|
args = {"vgName" : self.vgName,
|
|
"action1": "deactivateNoRefcount",
|
|
"lvName1": oldNameLV,
|
|
"action2": "refresh",
|
|
"lvName2": vdi.lvName}
|
|
text = self.xapi.session.xenapi.host.call_plugin( \
|
|
slave, self.xapi.PLUGIN_ON_SLAVE, "multi", args)
|
|
util.SMlog("call-plugin returned: '%s'" % text)
|
|
|
|
|
|
################################################################################
|
|
#
|
|
# Helpers
|
|
#
|
|
def daemonize():
|
|
pid = os.fork()
|
|
if pid:
|
|
os.waitpid(pid, 0)
|
|
Util.log("New PID [%d]" % pid)
|
|
return False
|
|
os.chdir("/")
|
|
os.setsid()
|
|
pid = os.fork()
|
|
if pid:
|
|
Util.log("Will finish as PID [%d]" % pid)
|
|
os._exit(0)
|
|
for fd in [0, 1, 2]:
|
|
try:
|
|
os.close(fd)
|
|
except OSError:
|
|
pass
|
|
# we need to fill those special fd numbers or pread won't work
|
|
sys.stdin = open("/dev/null", 'r')
|
|
sys.stderr = open("/dev/null", 'w')
|
|
sys.stdout = open("/dev/null", 'w')
|
|
return True
|
|
|
|
def normalizeType(type):
|
|
if type in LVHDSR.SUBTYPES:
|
|
type = SR.TYPE_LVHD
|
|
if type in ["lvm", "lvmoiscsi", "lvmohba"]:
|
|
# temporary while LVHD is symlinked as LVM
|
|
type = SR.TYPE_LVHD
|
|
if type in ["ext", "nfs"]:
|
|
type = SR.TYPE_FILE
|
|
if not type in SR.TYPES:
|
|
raise util.SMException("Unsupported SR type: %s" % type)
|
|
return type
|
|
|
|
def _gcLoop(sr, dryRun):
|
|
failedCandidates = []
|
|
while True:
|
|
if not sr.xapi.isPluggedHere():
|
|
Util.log("SR no longer attached, exiting")
|
|
break
|
|
sr.scanLocked()
|
|
if not sr.hasWork():
|
|
Util.log("No work, exiting")
|
|
break
|
|
|
|
if not lockRunning.acquireNoblock():
|
|
Util.log("Another instance already running, exiting")
|
|
break
|
|
try:
|
|
sr.cleanupCoalesceJournals()
|
|
sr.scanLocked()
|
|
sr.updateBlockInfo()
|
|
|
|
if len(sr.findGarbage()) > 0:
|
|
sr.garbageCollect(dryRun)
|
|
sr.xapi.srUpdate()
|
|
continue
|
|
|
|
candidate = sr.findCoalesceable()
|
|
if candidate:
|
|
util.fistpoint.activate("LVHDRT_finding_a_suitable_pair",sr.uuid)
|
|
sr.coalesce(candidate, dryRun)
|
|
sr.xapi.srUpdate()
|
|
continue
|
|
|
|
candidate = sr.findLeafCoalesceable()
|
|
if candidate:
|
|
sr.coalesceLeaf(candidate, dryRun)
|
|
sr.xapi.srUpdate()
|
|
continue
|
|
|
|
Util.log("No work left")
|
|
finally:
|
|
lockRunning.release()
|
|
|
|
def _gc(session, srUuid, dryRun):
|
|
init(srUuid)
|
|
sr = SR.getInstance(srUuid, session)
|
|
|
|
try:
|
|
_gcLoop(sr, dryRun)
|
|
finally:
|
|
sr.cleanup()
|
|
Util.log("Final SR state:")
|
|
Util.log(sr.toString())
|
|
sr.printVDIs()
|
|
|
|
def _abort(srUuid):
|
|
"""If successful, we return holding lockRunning; otherwise exception
|
|
raised."""
|
|
Util.log("=== SR %s: abort ===" % (srUuid))
|
|
init(srUuid)
|
|
if not lockRunning.acquireNoblock():
|
|
gotLock = False
|
|
Util.log("Aborting currently-running instance (SR %s)" % srUuid)
|
|
abortFlag = IPCFlag(srUuid)
|
|
abortFlag.set(FLAG_TYPE_ABORT)
|
|
for i in range(SR.LOCK_RETRY_ATTEMPTS):
|
|
gotLock = lockRunning.acquireNoblock()
|
|
if gotLock:
|
|
break
|
|
time.sleep(SR.LOCK_RETRY_INTERVAL)
|
|
abortFlag.clear(FLAG_TYPE_ABORT)
|
|
if not gotLock:
|
|
raise util.SMException("SR %s: error aborting existing process" % \
|
|
srUuid)
|
|
|
|
def coalesceLeafAtomic(session, srUuid, vdiUuid):
|
|
"""Coalesce a leaf node onto its parent. This should be invoked in the
|
|
stop_using_these_vdis_() context to ensure that the leaf is offline. It is
|
|
dangerous to invoke this function otherwise. Return True on success, False
|
|
if the target VDI is no longer leaf-coalesceable"""
|
|
Util.log("=== SR %s: coalesceLeafAtomic for VDI %s ===" % (srUuid, vdiUuid))
|
|
init(srUuid)
|
|
sr = SR.getInstance(srUuid, session)
|
|
sr.lock()
|
|
try:
|
|
sr.scan()
|
|
vdi = sr.getVDI(vdiUuid)
|
|
if not vdi.isLeafCoalesceable():
|
|
return False
|
|
try:
|
|
sr._doCoalesceLeaf(vdi)
|
|
except:
|
|
Util.logException("_doCoalesceLeaf")
|
|
sr._handleInterruptedCoalesceLeaf()
|
|
raise
|
|
finally:
|
|
sr.cleanup()
|
|
sr.unlock()
|
|
Util.log("final SR state:")
|
|
Util.log(sr.toString())
|
|
sr.printVDIs()
|
|
return True
|
|
|
|
def init(srUuid):
|
|
global lockRunning
|
|
if not lockRunning:
|
|
lockRunning = lock.Lock(LOCK_TYPE_RUNNING, srUuid)
|
|
|
|
def usage():
|
|
output = """Garbage collect and/or coalesce VHDs in a VHD-based SR
|
|
|
|
Parameters:
|
|
-u --uuid UUID SR UUID
|
|
and one of:
|
|
-g --gc garbage collect, coalesce, and repeat while there is work
|
|
-G --gc_force garbage collect once, aborting any current operations
|
|
-a --abort abort any currently running operation (GC or coalesce)
|
|
-q --query query the current state (GC'ing, coalescing or not running)
|
|
-x --disable disable GC/coalesce (will be in effect until you exit)
|
|
|
|
Options:
|
|
-b --background run in background (return immediately) (valid for -g only)
|
|
-f --force continue in the presence of VHDs with errors (when doing
|
|
GC, this might cause removal of any such VHDs) (only valid
|
|
for -G) (DANGEROUS)
|
|
"""
|
|
#-d --dry-run don't actually perform any SR-modifying operations
|
|
#-t perform a custom operation (for testing)
|
|
print output
|
|
Util.log("(Invalid usage)")
|
|
sys.exit(1)
|
|
|
|
|
|
##############################################################################
|
|
#
|
|
# API
|
|
#
|
|
def abort(srUuid):
|
|
"""Abort GC/coalesce if we are currently GC'ing or coalescing a VDI pair.
|
|
"""
|
|
_abort(srUuid)
|
|
Util.log("abort: releasing the process lock")
|
|
lockRunning.release()
|
|
|
|
def gc(session, srUuid, inBackground, dryRun = False):
|
|
"""Garbage collect all deleted VDIs in SR "srUuid". Fork & return
|
|
immediately if inBackground=True.
|
|
|
|
The following algorithm is used:
|
|
1. If we are already GC'ing in this SR, return
|
|
2. If we are already coalescing a VDI pair:
|
|
a. Scan the SR and determine if the VDI pair is GC'able
|
|
b. If the pair is not GC'able, return
|
|
c. If the pair is GC'able, abort coalesce
|
|
3. Scan the SR
|
|
4. If there is nothing to collect, nor to coalesce, return
|
|
5. If there is something to collect, GC all, then goto 3
|
|
6. If there is something to coalesce, coalesce one pair, then goto 3
|
|
"""
|
|
Util.log("=== SR %s: gc ===" % srUuid)
|
|
if inBackground:
|
|
if daemonize():
|
|
# we are now running in the background. Catch & log any errors
|
|
# because there is no other way to propagate them back at this
|
|
# point
|
|
|
|
try:
|
|
_gc(None, srUuid, dryRun)
|
|
except AbortException:
|
|
Util.log("Aborted")
|
|
except Exception:
|
|
Util.logException("gc")
|
|
Util.log("* * * * * SR %s: ERROR\n" % srUuid)
|
|
os._exit(0)
|
|
else:
|
|
_gc(session, srUuid, dryRun)
|
|
|
|
def gc_force(session, srUuid, force = False, dryRun = False):
|
|
"""Garbage collect all deleted VDIs in SR "srUuid".
|
|
The following algorithm is used:
|
|
1. If we are already GC'ing or coalescing a VDI pair, abort GC/coalesce
|
|
2. Scan the SR
|
|
3. GC
|
|
4. return
|
|
"""
|
|
Util.log("=== SR %s: gc_force ===" % srUuid)
|
|
init(srUuid)
|
|
sr = SR.getInstance(srUuid, session)
|
|
if not lockRunning.acquireNoblock():
|
|
_abort(srUuid)
|
|
else:
|
|
Util.log("Nothing was running, clear to proceed")
|
|
|
|
if force:
|
|
Util.log("FORCED: will continue even if there are VHD errors")
|
|
sr.scanLocked(force)
|
|
sr.cleanupCoalesceJournals()
|
|
|
|
try:
|
|
sr.garbageCollect(dryRun)
|
|
finally:
|
|
sr.cleanup()
|
|
Util.log("final SR state:")
|
|
Util.log(sr.toString())
|
|
sr.printVDIs()
|
|
lockRunning.release()
|
|
|
|
def get_state(srUuid):
|
|
"""Return whether GC/coalesce is currently running or not. The information
|
|
is not guaranteed for any length of time if the call is not protected by
|
|
locking.
|
|
"""
|
|
init(srUuid)
|
|
if lockRunning.acquireNoblock():
|
|
lockRunning.release()
|
|
return False
|
|
return True
|
|
|
|
def should_preempt(session, srUuid):
|
|
sr = SR.getInstance(srUuid, session)
|
|
entries = sr.journaler.getAll(VDI.JRN_COALESCE)
|
|
if len(entries) == 0:
|
|
return False
|
|
elif len(entries) > 1:
|
|
raise util.SMException("More than one coalesce entry: " + entries)
|
|
sr.scanLocked()
|
|
coalescedUuid = entries.popitem()[0]
|
|
garbage = sr.findGarbage()
|
|
for vdi in garbage:
|
|
if vdi.uuid == coalescedUuid:
|
|
return True
|
|
return False
|
|
|
|
def get_coalesceable_leaves(session, srUuid, vdiUuids):
|
|
coalesceable = []
|
|
sr = SR.getInstance(srUuid, session)
|
|
sr.scanLocked()
|
|
for uuid in vdiUuids:
|
|
vdi = sr.getVDI(uuid)
|
|
if not vdi:
|
|
raise util.SMException("VDI %s not found" % uuid)
|
|
if vdi.isLeafCoalesceable():
|
|
coalesceable.append(uuid)
|
|
return coalesceable
|
|
|
|
##############################################################################
|
|
#
|
|
# CLI
|
|
#
|
|
def main():
|
|
action = ""
|
|
uuid = ""
|
|
background = False
|
|
force = False
|
|
dryRun = False
|
|
test = False
|
|
shortArgs = "gGaqxu:bfdt"
|
|
longArgs = ["gc", "gc_force", "abort", "query", "disable",
|
|
"uuid", "background", "force", "dry-run", "test"]
|
|
|
|
try:
|
|
opts, args = getopt.getopt(sys.argv[1:], shortArgs, longArgs)
|
|
except getopt.GetoptError:
|
|
usage()
|
|
for o, a in opts:
|
|
if o in ("-g", "--gc"):
|
|
action = "gc"
|
|
if o in ("-G", "--gc_force"):
|
|
action = "gc_force"
|
|
if o in ("-a", "--abort"):
|
|
action = "abort"
|
|
if o in ("-q", "--query"):
|
|
action = "query"
|
|
if o in ("-x", "--disable"):
|
|
action = "disable"
|
|
if o in ("-u", "--uuid"):
|
|
uuid = a
|
|
if o in ("-b", "--background"):
|
|
background = True
|
|
if o in ("-f", "--force"):
|
|
force = True
|
|
if o in ("-d", "--dry-run"):
|
|
Util.log("Dry run mode")
|
|
dryRun = True
|
|
if o in ("-t"):
|
|
action = "test"
|
|
|
|
if not action or not uuid:
|
|
usage()
|
|
elif action != "query":
|
|
print "All output goes in %s" % LOG_FILE
|
|
|
|
if action == "gc":
|
|
gc(None, uuid, background, dryRun)
|
|
elif action == "gc_force":
|
|
gc_force(None, uuid, force, dryRun)
|
|
elif action == "abort":
|
|
abort(uuid)
|
|
elif action == "query":
|
|
print "Currently running: %s" % get_state(uuid)
|
|
elif action == "disable":
|
|
print "Disabling GC/coalesce for %s" % uuid
|
|
_abort(uuid)
|
|
raw_input("Press enter to re-enable...")
|
|
print "GC/coalesce re-enabled"
|
|
lockRunning.release()
|
|
elif action == "test":
|
|
Util.log("Test operation")
|
|
pass
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|