#
# Copyright (c) 2018 Red Hat, Inc.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# 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 General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.
#
"""
VDOService - manages the VDO service on the local node
$Id: //eng/vdo-releases/magnesium/src/python/vdo/vdomgmnt/VDOService.py#35 $
"""
from . import ArgumentError
from . import Constants
from . import Defaults
from . import MgmntLogger, MgmntUtils
from . import Service, ServiceError
from . import SizeString
from . import VDOKernelModuleService
from . import DeveloperExitStatus, StateExitStatus
from . import SystemExitStatus, UserExitStatus
from utils import Command, CommandError, runCommand
from utils import Transaction, transactional
import functools
import locale
import math
import os
import re
from socket import gethostbyname
import stat
import time
import yaml
########################################################################
class VDOServiceError(ServiceError):
"""Base class for VDO service exceptions.
"""
######################################################################
# Overriden methods
######################################################################
def __init__(self, msg = _("VDO volume error"), *args, **kwargs):
super(VDOServiceError, self).__init__(msg, *args, **kwargs)
########################################################################
class VDODeviceAlreadyConfiguredError(UserExitStatus, VDOServiceError):
"""The specified device is already configured for a VDO.
"""
######################################################################
# Overriden methods
######################################################################
def __init__(self, msg = _("Device already configured"), *args, **kwargs):
super(VDODeviceAlreadyConfiguredError, self).__init__(msg, *args, **kwargs)
########################################################################
class VDOServiceExistsError(UserExitStatus, VDOServiceError):
"""VDO service exists exception.
"""
######################################################################
# Overriden methods
######################################################################
def __init__(self, msg = _("VDO volume exists"), *args, **kwargs):
super(VDOServiceExistsError, self).__init__(msg, *args, **kwargs)
########################################################################
class VDOMissingDeviceError(StateExitStatus, VDOServiceError):
"""VDO underlying device does not exist exception.
"""
######################################################################
# Overriden methods
######################################################################
def __init__(self, msg = _("Underlying device does not exist"),
*args, **kwargs):
super(VDOMissingDeviceError, self).__init__(msg, *args, **kwargs)
########################################################################
class VDOServicePreviousOperationError(StateExitStatus, VDOServiceError):
"""VDO volume previous operation was not completed.
"""
######################################################################
# Overriden methods
######################################################################
def __init__(self, msg = _("VDO volume previous operation is incomplete"),
*args, **kwargs):
super(VDOServicePreviousOperationError, self).__init__(msg,
*args, **kwargs)
########################################################################
class VDOService(Service):
"""VDOService manages a vdo device mapper target on the local node.
Attributes:
ackThreads (int): Number of threads dedicated to performing I/O
operation acknowledgement calls.
activated (bool): If True, should be started by the `start` method.
bioRotationInterval (int): Number of I/O operations to enqueue for
one bio submission thread in a batch before moving on to enqueue for
the next.
bioThreads (int): Number of threads used to submit I/O operations to
the storage device.
blockMapCacheSize (sizeString): Memory allocated for block map pages.
blockMapPeriod (int): Block map period.
cpuThreads (int): Number of threads used for various CPU-intensive tasks
such as hashing.
device (path): The device used for storage for this VDO volume.
enableCompression (bool): If True, compression should be
enabled on this volume the next time the `start` method is run.
enableDeduplication (bool): If True, deduplication should be
enabled on this volume the next time the `start` method is run.
enableReadCache (bool): If True, enables the VDO device's read cache.
hashZoneThreads (int): Number of threads across which to subdivide parts
of VDO processing based on the hash value computed from the block data
indexCfreq (int): The Index checkpoint frequency.
indexMemory (str): The Index memory setting.
indexSparse (bool): If True, creates a sparse Index.
indexThreads (int): The Index thread count. If 0, use a thread per core
logicalSize (SizeString): The logical size of this VDO volume.
logicalThreads (int): Number of threads across which to subdivide parts
of the VDO processing based on logical block addresses.
physicalSize (SizeString): The physical size of this VDO volume.
physicalThreads (int): Number of threads across which to subdivide parts
of the VDO processing based on physical block addresses.
readCacheSize (SizeString): The size of the read cache, in addition
to a minimum set by the VDO software.
slabSize (SizeString): The size increment by which a VDO is grown. Using
a smaller size constrains the maximum physical size that can be
accomodated. Must be a power of two between 128M and 32G.
writePolicy (str): sync, async or auto.
"""
log = MgmntLogger.getLogger(MgmntLogger.myname + '.Service.VDOService')
yaml_tag = u"!VDOService"
# Key values to use accessing a dictionary created via yaml-loading the
# output of vdo status.
# Access the VDO list.
vdosKey = "VDOs"
# Access the per-VDO info.
readCacheKey = _("Read cache")
readCacheSizeKey = _("Read cache size")
vdoAckThreadsKey = _("Acknowledgement threads")
vdoBioSubmitThreadsKey = _("Bio submission threads")
vdoBlockMapCacheSizeKey = _("Block map cache size")
vdoBlockMapPeriodKey = _("Block map period")
vdoBlockSizeKey = _("Block size")
vdoCompressionEnabledKey = _("Compression")
vdoCpuThreadsKey = _("CPU-work threads")
vdoDeduplicationEnabledKey = _("Deduplication")
vdoHashZoneThreadsKey = _("Hash zone threads")
vdoLogicalSizeKey = _("Logical size")
vdoLogicalThreadsKey = _("Logical threads")
vdoMdRaid5ModeKey = _("MD RAID5 mode")
vdoPhysicalSizeKey = _("Physical size")
vdoPhysicalThreadsKey = _("Physical threads")
vdoStatisticsKey = _("VDO statistics")
vdoWritePolicyKey = _("Write policy")
# Options that cannot be changed for an already-created VDO device.
# Specified as used by the command-line.
fixedOptions = [ 'device' ]
# Options that can be changed for an already-created VDO device,
# though not necessarily while the device is running. The
# command-line options and VDOService use different names,
# hence the mapping.
modifiableOptions = {
'blockMapCacheSize' : 'blockMapCacheSize',
'blockMapPeriod' : 'blockMapPeriod',
'readCache' : 'readCache',
'readCacheSize' : 'readCacheSize',
'vdoAckThreads' : 'ackThreads',
'vdoBioRotationInterval': 'bioRotationInterval',
'vdoBioThreads' : 'bioThreads',
'vdoCpuThreads' : 'cpuThreads',
'vdoHashZoneThreads' : 'hashZoneThreads',
'vdoLogicalThreads' : 'logicalThreads',
'vdoPhysicalThreads' : 'physicalThreads',
}
# States in the process of constructing a vdo.
class OperationState(object):
beginCreate = 'beginCreate'
beginGrowLogical = 'beginGrowLogical'
beginGrowPhysical = 'beginGrowPhysical'
beginRunningSetWritePolicy = 'beginRunningSetWritePolicy'
finished = 'finished'
unknown = 'unknown'
####################################################################
@classmethod
def specificOperationStates(cls):
"""Return a list of the possible specific operation states.
"Specific operation state" means a state that is specifically set
via normal processing.
"""
return [cls.beginCreate, cls.beginGrowLogical, cls.beginGrowPhysical,
cls.beginRunningSetWritePolicy, cls.finished]
######################################################################
# Public methods
######################################################################
@classmethod
def validateModifiableOptions(cls, args):
"""Validates that any options specified in the arguments are solely
those which are modifiable.
Argument:
args - arguments passed from the user
"""
for option in cls.fixedOptions:
if getattr(args, option, None) is not None:
msg = _("Cannot change option {0} after VDO creation").format(
option)
raise ServiceError(msg, exitStatus = UserExitStatus)
######################################################################
def activate(self):
"""Marks the VDO device as activated, updating the configuration.
"""
self._handlePreviousOperationFailure()
if self.activated:
msg = _("{0} already activated").format(self.getName())
self.log.announce(msg)
return
self.log.announce(_("Activating VDO {0}").format(self.getName()))
self.activated = True
self.config.addVdo(self.getName(), self, True)
######################################################################
def announceReady(self, wasCreated=True):
"""Logs the VDO volume state during create/start."""
if self.running():
self.log.announce(_("VDO instance {0} volume is ready at {1}").format(
self.getInstanceNumber(), self.getPath()))
elif wasCreated:
self.log.announce(_("VDO volume created at {0}").format(self.getPath()))
elif not self.activated:
self.log.announce(_("VDO volume cannot be started (not activated)"))
######################################################################
def connect(self):
"""Connect to index."""
self.log.announce(_("Attempting to get {0} to connect").format(
self.getName()))
self._handlePreviousOperationFailure()
runCommand(['dmsetup', 'message', self.getName(), '0', 'index-enable'])
self.log.announce(_("{0} connect succeeded").format(self.getName()))
######################################################################
@transactional
def create(self, force = False):
"""Creates and starts a VDO target."""
self.log.announce(_("Creating VDO {0}").format(self.getName()))
self.log.debug("confFile is {0}".format(self.config.filepath))
self._handlePreviousOperationFailure()
if self.isConstructed:
msg = _("VDO volume {0} already exists").format(self.getName())
raise VDOServiceExistsError(msg)
# Check that there isn't already a vdo using the device we were given.
if self.config.isDeviceConfigured(self.device):
msg = _("Device {0} already configured for VDO use").format(
self.device)
raise VDODeviceAlreadyConfiguredError(msg)
# Check that there isn't an already extant dm target with the VDO's name.
if self._mapperDeviceExists():
msg = _("Name conflict with extant device mapper target {0}").format(
self.getName())
raise VDOServiceError(msg, exitStatus = StateExitStatus)
# Check that we have enough kernel memory to at least create the index.
self._validateAvailableMemory(self.indexMemory);
# Check that the hash zone, logical and physical threads are consistent.
self._validateModifiableThreadCounts(self.hashZoneThreads,
self.logicalThreads,
self.physicalThreads)
# Perform a verification that the storage device doesn't already
# have something on it.
if not force:
self._createCheckCleanDevice()
# Find a stable name for the real device, that won't change from
# one boot cycle to the next.
realpath = os.path.realpath(self.device)
idDir = '/dev/disk/by-id'
aliases = []
if os.path.isdir(idDir):
aliases = [absname
for absname in (os.path.join(idDir, name)
for name in os.listdir(idDir))
if os.path.realpath(absname) == realpath]
if realpath is not None:
deviceUUID = self._getDeviceUUID(realpath)
if deviceUUID is not None:
self.log.debug("pruning {uuid} from aliases".format(
uuid=deviceUUID))
aliases = [a for a in aliases if not deviceUUID in a]
if len(aliases) > 0:
self.log.debug("found aliases for {original}: {aliases}"
.format(original = realpath, aliases = aliases))
# A device can have multiple names; dm-name-*, dm-uuid-*, ata-*,
# wwn-*, etc. Do we have a way to prioritize them?
#
# LVM volumes and MD arrays can be renamed; prioritize dm-name-*
# below dm-uuid-* and likewise for md-*.
#
# Otherwise, just sort and take the first name; that'll at least
# be consistent from run to run.
uuidAliases = [a
for a in aliases
if re.match(r".*/[dm][dm]-uuid-", a) is not None]
if len(uuidAliases) > 0:
aliases = uuidAliases
aliases.sort()
self.device = aliases[0]
self.log.debug("using {new}".format(new = self.device))
else:
self.log.debug("no aliases for {original} found in {idDir}!"
.format(original = realpath, idDir = idDir))
# Make certain the kernel module is installed.
self._installKernelModule(self.vdoLogLevel)
# Do the create.
self._setOperationState(self.OperationState.beginCreate)
# As setting the operation state updates (and persists) the config file
# we need to be certain to remove this instance if something goes wrong.
transaction = Transaction.transaction()
transaction.addUndoStage(self.config.persist)
transaction.addUndoStage(functools.partial(self.config.removeVdo,
self.getName()))
self._constructVdoFormat(force)
self._constructServiceStart()
# The create is done.
self._setOperationState(self.OperationState.finished)
######################################################################
def deactivate(self):
"""Marks the VDO device as not activated, updating the configuration.
"""
self._handlePreviousOperationFailure()
if not self.activated:
msg = _("{0} already deactivated").format(self.getName())
self.log.announce(msg)
return
self.log.announce(_("Deactivating VDO {0}").format(self.getName()))
self.activated = False
self.config.addVdo(self.getName(), self, True)
######################################################################
def disconnect(self):
"""Disables deduplication on this VDO device."""
self._handlePreviousOperationFailure()
try:
runCommand(["dmsetup", "message", self.getName(), "0", "index-disable"])
except Exception:
self.log.error(_("Cannot stop deduplication on VDO {0}").format(
self.getName()))
raise
######################################################################
def getInstanceNumber(self):
"""Returns the instance number of a vdo if running, or zero."""
self._handlePreviousOperationFailure()
if not self.instanceNumber and self.running():
self._determineInstanceNumber()
return self.instanceNumber
######################################################################
def getPath(self):
"""Returns the full path to this VDO device."""
return os.path.join("/dev/mapper", self.getName())
######################################################################
@transactional
def growLogical(self, newLogicalSize):
"""Grows the logical size of this VDO volume.
Arguments:
newLogicalSize (SizeString): The new size.
"""
self._handlePreviousOperationFailure()
if not self.running():
msg = _("VDO volume {0} must be running").format(self.getName())
raise ServiceError(msg, exitStatus = StateExitStatus)
newLogicalSize.roundToBlock()
if newLogicalSize < self.logicalSize:
msg = _("Can't shrink a VDO volume (old size {0})").format(
self.logicalSize)
raise ServiceError(msg, exitStatus = UserExitStatus)
elif newLogicalSize == self.logicalSize:
msg = _("Can't grow a VDO volume by less than {0} bytes").format(
Constants.VDO_BLOCK_SIZE)
raise ServiceError(msg, exitStatus = UserExitStatus)
# Do the grow.
self._setOperationState(self.OperationState.beginGrowLogical)
self.log.info(_("Preparing to increase logical size of VDO {0}").format(
self.getName()))
transaction = Transaction.transaction()
transaction.setMessage(self.log.error,
_("Cannot prepare to grow logical on VDO {0}").format(
self.getName()))
runCommand(["dmsetup", "message", self.getName(), "0",
"prepareToGrowLogical", str(newLogicalSize.toBlocks())])
transaction.setMessage(None)
self._suspend()
transaction.addUndoStage(self._resume)
self.log.info(_("Increasing logical size of VDO volume {0}").format(
self.getName()))
transaction.setMessage(self.log.error,
_("Device {0} could not be changed").format(
self.getName()))
numSectors = newLogicalSize.toSectors()
vdoConf = self._generateModifiedDmTable(numSectors = str(numSectors))
runCommand(["dmsetup", "reload", self._name, "--table", vdoConf])
transaction.setMessage(None)
self.log.info(_("Increased logical size of VDO volume {0}").format(
self.getName()))
self.logicalSize = newLogicalSize
self._resume()
# The grow is done.
self._setOperationState(self.OperationState.finished)
######################################################################
@transactional
def growPhysical(self):
"""Grows the physical size of this VDO volume.
Arguments:
newPhysicalSize (SizeString): The new size. If None, use all the
remaining free space in the volume
group.
"""
self._handlePreviousOperationFailure()
if not self.running():
msg = _("VDO volume {0} must be running").format(self.getName())
raise ServiceError(msg, exitStatus = StateExitStatus)
# Do the grow.
self._setOperationState(self.OperationState.beginGrowPhysical)
self.log.info(_("Preparing to increase physical size of VDO {0}").format(
self.getName()))
transaction = Transaction.transaction()
transaction.setMessage(self.log.error,
_("Cannot prepare to grow physical on VDO {0}").format(
self.getName()))
runCommand(["dmsetup", "message", self.getName(), "0",
"prepareToGrowPhysical"])
transaction.setMessage(None)
self._suspend()
transaction.addUndoStage(self._resume)
transaction.setMessage(self.log.error,
_("Cannot grow physical on VDO {0}").format(
self.getName()))
runCommand(['dmsetup', 'message', self.getName(), '0',
'growPhysical'])
transaction.setMessage(None)
# Get the new physical size
vdoConfig = self._getConfigFromVDO()
sectorsPerBlock = vdoConfig["blockSize"] / Constants.SECTOR_SIZE
physicalSize = vdoConfig["physicalBlocks"] * sectorsPerBlock
self.physicalSize = SizeString("{0}s".format(physicalSize))
self._resume()
# The grow is done.
self._setOperationState(self.OperationState.finished)
######################################################################
def reconnect(self):
"""Enables deduplication on this VDO device."""
self._handlePreviousOperationFailure()
try:
runCommand(["dmsetup", "message", self.getName(), "0", "index-enable"])
except Exception:
self.log.error(_("Cannot start deduplication on VDO {0}").format(
self.getName()))
raise
######################################################################
def remove(self, force=False, removeSteps=None):
"""Removes a VDO target.
If removeSteps is not None it as an empty list to which the processing
commands for removal will be appended.
If force was not specified and the instance previous operation failure
is not recoverable VDOServicePreviousOperationError will be raised.
"""
self.log.announce(_("Removing VDO {0}").format(self.getName()))
# Fail if the device does not exist and --force is not specified. If
# this remove is being run to undo a failed create, the device will
# exist.
try:
os.stat(self.device)
except OSError:
if not force:
msg = _("Device {0} not found. Remove VDO with --force.").format(
self.device)
raise VDOMissingDeviceError(msg)
localRemoveSteps = []
try:
self.stop(force, localRemoveSteps)
# If we're not forcing handle any previous operation failure (which will
# raise an exception if it's not handled).
if not force:
self._handlePreviousOperationFailure()
except VDOServicePreviousOperationError:
if (removeSteps is not None) and (len(localRemoveSteps) > 0):
removeSteps.append(
_("Steps to clean up VDO {0}:").format(self.getName()))
removeSteps.extend([" {0}".format(s) for s in localRemoveSteps])
raise
self.config.removeVdo(self.getName())
# We delete the metadata after we remove the entry from the config
# file because if we do it before and the removal from the config
# fails, we will end up with a valid looking entry in the config
# that has no valid metadata.
#
# Having gotten this far we know that either no one is holding on to the
# underlying device or we skipped that check because we weren't running.
# We could be in one of a number of clean up conditions and only if the
# underlying device isn't in use can we clear the metadata.
#
# This really isn't sufficient but we cannot completely determine that no
# one has data of any import on the device. For example, if the device was
# being used raw we have no way of determining that. We do what we can.
if not self._hasHolders():
self._clearMetadata()
######################################################################
def running(self):
"""Returns True if the VDO service is available."""
try:
result = runCommand(["dmsetup", "status", "--target",
Defaults.vdoTargetName, self.getName()])
# dmsetup does not error as long as the device exists even if it's not
# of the specified target type. However, if it's not of the specified
# target type there is no returned info.
return result.strip() != ""
except Exception:
return False
######################################################################
def start(self, forceRebuild=False):
"""Starts the VDO target mapper. In noRun mode, we always assume
the service is not yet running.
Raises:
ServiceError
"""
self.log.announce(_("Starting VDO {0}").format(self.getName()))
self._handlePreviousOperationFailure()
if not self.activated:
self.log.info(_("VDO service {0} not activated").format(self.getName()))
return
if self.running() and not Command.noRunMode():
self.log.info(_("VDO service {0} already started").format(
self.getName()))
return
# Check that we have enough kernel memory to at least create the index.
self._validateAvailableMemory(self.indexMemory);
self._installKernelModule()
self._checkConfiguration()
try:
if forceRebuild:
try:
self._forceRebuild()
except Exception:
self.log.error(_("Device {0} not read-only").format(self.getName()))
raise
runCommand(["dmsetup", "create", self._name, "--uuid", self._getUUID(),
"--table", self._generateDeviceMapperTable()])
if not self.enableDeduplication:
try:
self.disconnect()
except Exception:
pass
self._determineInstanceNumber()
if self.instanceNumber:
self.log.info(_("started VDO service {0} instance {1}").format(
self.getName(), self.instanceNumber))
try:
if self.enableCompression:
self._startCompression()
except Exception:
self.log.error(_("Could not enable compression for {0}").format(
self.getName()))
raise
self._startFullnessMonitoring()
except Exception:
self.log.error(_("Could not set up device mapper for {0}").format(
self.getName()))
raise
######################################################################
def status(self):
"""Returns a dictionary representing the status of this object.
"""
self._handlePreviousOperationFailure()
status = {}
status[_("Storage device")] = self.device
status[self.vdoBlockMapCacheSizeKey] = str(self.blockMapCacheSize)
status[self.vdoBlockMapPeriodKey] = self.blockMapPeriod
status[self.vdoBlockSizeKey] = Constants.VDO_BLOCK_SIZE
status[_("Emulate 512 byte")] = Constants.enableString(
self.logicalBlockSize == 512)
status[_("Activate")] = Constants.enableString(self.activated)
status[self.readCacheKey] = Constants.enableString(self.enableReadCache)
status[self.readCacheSizeKey] = str(self.readCacheSize)
status[self.vdoCompressionEnabledKey] = Constants.enableString(
self.enableCompression)
status[self.vdoDeduplicationEnabledKey] = Constants.enableString(
self.enableDeduplication)
status[self.vdoLogicalSizeKey] = str(self.logicalSize)
status[self.vdoPhysicalSizeKey] = str(self.physicalSize)
status[self.vdoAckThreadsKey] = self.ackThreads
status[self.vdoBioSubmitThreadsKey] = self.bioThreads
status[_("Bio rotation interval")] = self.bioRotationInterval
status[self.vdoCpuThreadsKey] = self.cpuThreads
status[self.vdoHashZoneThreadsKey] = self.hashZoneThreads
status[self.vdoLogicalThreadsKey] = self.logicalThreads
status[self.vdoPhysicalThreadsKey] = self.physicalThreads
status[_("Slab size")] = str(self.slabSize)
status[_("Configured write policy")] = self.writePolicy
status[_("Index checkpoint frequency")] = self.indexCfreq
status[_("Index memory setting")] = self.indexMemory
status[_("Index parallel factor")] = self.indexThreads
status[_("Index sparse")] = Constants.enableString(self.indexSparse)
status[_("Index status")] = self._getDeduplicationStatus()
if os.getuid() == 0:
status[_("Device mapper status")] = MgmntUtils.statusHelper(
['dmsetup', 'status',
self.getName()])
try:
result = runCommand(['vdostats', '--verbose', self.getPath()])
status[self.vdoStatisticsKey] = yaml.safe_load(result)
except Exception:
status[self.vdoStatisticsKey] = _("not available")
return status
######################################################################
def stop(self, force=False, removeSteps=None):
"""Stops the VDO target mapper. In noRun mode, assumes the service
is already running.
If removeSteps is not None it is a list to which the processing
commands will be appended.
If force was not specified and the instance previous operation failed
VDOServicePreviousOperationError will be raised.
Raises:
ServiceError
VDOServicePreviousOperationError
"""
self.log.announce(_("Stopping VDO {0}").format(self.getName()))
execute = force
if not execute:
try:
self._handlePreviousOperationFailure()
execute = True
except VDOServicePreviousOperationError:
pass
if execute:
if ((not self.running()) and (not Command.noRunMode())
and (not self.previousOperationFailure)):
self.log.info(_("VDO service {0} already stopped").format(
self.getName()))
return
running = self.running()
if running and self._hasHolders():
msg = _("cannot stop VDO volume {0}: in use").format(self.getName())
raise ServiceError(msg, exitStatus = StateExitStatus)
if (running and self._hasMounts()) or (not execute):
command = ["umount", "-f", self.getPath()]
if removeSteps is not None:
removeSteps.append(" ".join(command))
if execute:
if force:
runCommand(command, noThrow=True)
else:
msg = _("cannot stop VDO volume with mounts {0}").format(
self.getName())
raise ServiceError(msg, exitStatus = StateExitStatus)
# The udevd daemon can wake up at any time and use the blkid command on our
# vdo device. In fact, it can be triggered to do so by the unmount command
# we might have just done. Wait for udevd to process its event queue.
command = ["udevadm", "settle"]
if removeSteps is not None:
removeSteps.append(" ".join(command))
if running and execute:
runCommand(command, noThrow=True)
if running:
self._stopFullnessMonitoring(execute, removeSteps)
# In a modern Linux, we would use "dmsetup remove --retry".
# But SQUEEZE does not have the --retry option.
command = ["dmsetup", "remove", self.getName()]
if removeSteps is not None:
removeSteps.append(" ".join(command))
inUse = True
if running and execute:
for unused_i in range(10):
try:
runCommand(command)
return
except Exception as ex:
if "Device or resource busy" not in str(ex):
inUse = False
break
time.sleep(1)
# If we're not executing we're in a previous operation failure situation
# and want to report that and go no further.
if not execute:
self._generatePreviousOperationFailureResponse()
# Because we may removed the instance above we have to check again to see
# if it's running rather than using the value we got above.
if self.running():
if inUse:
msg = _("cannot stop VDO service {0}: device in use").format(
self.getName())
else:
msg = _("cannot stop VDO service {0}").format(self.getName())
raise ServiceError(msg, exitStatus = SystemExitStatus)
######################################################################
def setCompression(self, enable):
"""Changes the compression setting on a VDO. If the VDO is running
the setting takes effect immediately.
"""
self._announce("Enabling" if enable else "Disabling", "compression")
self._handlePreviousOperationFailure()
if ((enable and self.enableCompression) or
((not enable) and (not self.enableCompression))):
message = "compression already {0} on VDO ".format(
"enabled" if enable else "disabled")
self.log.info(_(message) + self.getName())
return
self.enableCompression = enable
self.config.addVdo(self.getName(), self, True)
if self.enableCompression:
self._startCompression()
else:
self._stopCompression()
######################################################################
def setConfig(self, config):
"""Sets the configuration reference and other attributes dependent on
the configuration.
This method must tolerate the possibility that the configuration is None
to handle instantiation from YAML representation. At present, there is
nothing for which we attempt to use the configuration.
"""
self._config = config
self._configUpgraded = False
######################################################################
def setDeduplication(self, enable):
"""Changes the deduplication setting on a VDO. If the VDO is running
the setting takes effect immediately.
"""
self._announce("Enabling" if enable else "Disabling", "deduplication")
self._handlePreviousOperationFailure()
if ((enable and self.enableDeduplication) or
((not enable) and (not self.enableDeduplication))):
message = "deduplication already {0} on VDO ".format(
"enabled" if enable else "disabled")
self.log.info(_(message) + self.getName())
return
self.enableDeduplication = enable
self.config.addVdo(self.getName(), self, True)
if self.running():
if self.enableDeduplication:
self.reconnect()
status = None
for _i in range(Constants.DEDUPLICATION_TIMEOUT):
status = self._getDeduplicationStatus()
if status == Constants.deduplicationStatusOpening:
time.sleep(1)
else:
break
if status == Constants.deduplicationStatusOnline:
pass
elif status == Constants.deduplicationStatusError:
raise ServiceError(_("Error enabling deduplication for {0}")
.format(self.getName()),
exitStatus = SystemExitStatus)
elif status == Constants.deduplicationStatusOpening:
message = (_("Timeout enabling deduplication for {0}, continuing")
.format(self.getName()))
self.log.warn(message)
else:
message = (_("Unexpected kernel status {0} enabling deduplication for {0}")
.format(status, self.getName()))
raise ServiceError(message, exitStatus = SystemExitStatus)
else:
self.disconnect()
######################################################################
def setModifiableOptions(self, args):
"""Sets any of the modifiable options that are specified in the arguments.
Argument:
args - arguments passed from the user
Raises:
ArgumentError
"""
self._handlePreviousOperationFailure()
# Check that any modification to hash zone, logical and physical threads
# maintain consistency.
self._validateModifiableThreadCounts(getattr(args, "vdoHashZoneThreads",
self.hashZoneThreads),
getattr(args, "vdoLogicalThreads",
self.logicalThreads),
getattr(args, "vdoPhysicalThreads",
self.physicalThreads))
modified = False
for option in self.modifiableOptions:
value = getattr(args, option, None)
if value is not None:
setattr(self, self.modifiableOptions[option], value)
modified = True
if modified:
self.config.addVdo(self.getName(), self, True)
if self.running():
self.log.announce(
_("Note: Changes will not apply until VDO {0} is restarted").format(
self.getName()))
######################################################################
def setWritePolicy(self, policy):
"""Changes the write policy on a VDO. If the VDO is running it is
restarted with the new policy"""
self._handlePreviousOperationFailure()
#pylint: disable=E0203
if policy != self.writePolicy:
self.writePolicy = policy
if not self.running():
self.config.addVdo(self.getName(), self, True)
else:
# Because the vdo is running we need to be able to handle recovery
# should the user interrupt processing.
# Setting the operation state will update the configuration thus
# saving the specified state.
self._setOperationState(self.OperationState.beginRunningSetWritePolicy)
self._performRunningSetWritePolicy()
# The setting of the write policy is finished.
self._setOperationState(self.OperationState.finished)
######################################################################
# Overridden methods
######################################################################
@staticmethod
def getKeys():
"""Returns the list of standard attributes for this object."""
return ["ackThreads",
"activated",
"bioRotationInterval",
"bioThreads",
"blockMapCacheSize",
"blockMapPeriod",
"cpuThreads",
"compression",
"deduplication",
"device",
"hashZoneThreads",
"indexCfreq",
"indexMemory",
"indexSparse",
"indexThreads",
"logicalBlockSize",
"logicalSize",
"logicalThreads",
"_operationState",
"physicalSize",
"physicalThreads",
"readCache",
"readCacheSize",
"slabSize",
"writePolicy"]
######################################################################
@classmethod
def _yamlMakeInstance(cls):
return cls("YAMLInstance", None)
######################################################################
@property
def _yamlData(self):
data = super(VDOService, self)._yamlData
data["activated"] = Constants.enableString(self.activated)
data["blockMapCacheSize"] = str(self.blockMapCacheSize)
data["compression"] = Constants.enableString(self.enableCompression)
data["deduplication"] = Constants.enableString(self.enableDeduplication)
data["indexSparse"] = Constants.enableString(self.indexSparse)
data["logicalSize"] = str(self.logicalSize)
data["physicalSize"] = str(self.physicalSize)
data["readCache"] = Constants.enableString(self.enableReadCache)
data["readCacheSize"] = str(self.readCacheSize)
data["slabSize"] = str(self.slabSize)
data["writePolicy"] = self.writePolicy
return data
######################################################################
def _yamlSetAttributes(self, attributes):
super(VDOService, self)._yamlSetAttributes(attributes)
# If an expected attribute does not exist in the specified dictionary the
# current value is used. This requires that the attribute be given an
# appropriate default when the object is instantiated.
self.activated = (
self._defaultIfNone(attributes, "activated",
Constants.enableString(self.activated))
!= Constants.disabled)
self.blockMapCacheSize = SizeString(
self._defaultIfNone(attributes, "blockMapCacheSize",
self.blockMapCacheSize.toBytes))
self.enableCompression = (
self._defaultIfNone(attributes, "compression",
Constants.enableString(self.enableCompression))
!= Constants.disabled)
self.enableDeduplication = (
self._defaultIfNone(attributes, "deduplication",
Constants.enableString(self.enableDeduplication))
!= Constants.disabled)
self.indexSparse = (
self._defaultIfNone(attributes, "indexSparse",
Constants.enableString(self.indexSparse))
!= Constants.disabled)
self.logicalSize = SizeString(
self._defaultIfNone(attributes, "logicalSize", self.logicalSize.toBytes))
self.physicalSize = SizeString(
self._defaultIfNone(attributes, "physicalSize",
self.physicalSize.toBytes))
self.enableReadCache = (
self._defaultIfNone(attributes, "readCache",
Constants.enableString(self.enableReadCache))
!= Constants.disabled)
self.readCacheSize = SizeString(
self._defaultIfNone(attributes, "readCacheSize",
self.readCacheSize.toBytes))
self.slabSize = SizeString(
self._defaultIfNone(attributes, "slabSize", self.slabSize.toBytes))
# writePolicy is handled differently as it is a computed property which
# depends on the config being set which is not the case when the instance
# is instantiated from YAML.
if "writePolicy" in attributes:
self.writePolicy = attributes["writePolicy"]
######################################################################
@property
def _yamlSpeciallyHandledAttributes(self):
specials = super(VDOService, self)._yamlSpeciallyHandledAttributes
specials.extend(["activated",
"blockMapCacheSize",
"compression",
"deduplication",
"indexSparse",
"logicalSize",
"physicalSize",
"readCache",
"readCacheSize",
"slabSize",
"writePolicy"])
return specials
######################################################################
def __getattr__(self, name):
# Fake this attribute so we don't have to make incompatible
# changes to the configuration file format.
if name == "config":
return self._computedConfig()
elif name == "isConstructed":
return self._computedIsConstructed()
elif name == "operationState":
return self._computedOperationState()
elif name == "previousOperationFailure":
return self._computedPreviousOperationFailure()
elif name == "indexMemory":
#pylint: disable=E1101
return super(VDOService, self).__getattr__(name)
elif name == "unrecoverablePreviousOperationFailure":
return self._computedUnrecoverablePreviousOperationFailure()
elif name == "writePolicy":
return self._computedWritePolicy()
else:
raise AttributeError("'{obj}' object has no attribute '{attr}'".format(
obj=type(self).__name__, attr=name))
######################################################################
def __init__(self, name, conf, **kw):
super(VDOService, self).__init__(name)
self.setConfig(conf)
# The state of operation of this instance is unknown.
# It will either be updated from the config file as part of accessing
# known vdos or it will be set during actual operation.
self._operationState = self.OperationState.unknown
self._previousOperationFailure = None
# required value
self.device = kw.get('device')
self.ackThreads = self._defaultIfNone(kw, 'vdoAckThreads',
Defaults.ackThreads)
self.bioRotationInterval = self._defaultIfNone(
kw,
'vdoBioRotationInterval',
Defaults.bioRotationInterval)
self.bioThreads = self._defaultIfNone(kw, 'vdoBioThreads',
Defaults.bioThreads)
self.blockMapCacheSize = self._defaultIfNone(kw, 'blockMapCacheSize',
Defaults.blockMapCacheSize)
self.blockMapPeriod = self._defaultIfNone(kw, 'blockMapPeriod',
Defaults.blockMapPeriod)
self.cpuThreads = self._defaultIfNone(kw, 'vdoCpuThreads',
Defaults.cpuThreads)
self.logicalBlockSize = Constants.VDO_BLOCK_SIZE
emulate512 = self._defaultIfNone(kw, 'emulate512', Defaults.emulate512)
if emulate512 != Constants.disabled:
self.logicalBlockSize = 512
compression = self._defaultIfNone(kw, 'compression',
Defaults.compression)
self.enableCompression = (compression != Constants.disabled)
deduplication = self._defaultIfNone(kw, 'deduplication',
Defaults.deduplication)
self.enableDeduplication = (deduplication != Constants.disabled)
activate = self._defaultIfNone(kw, 'activate', Defaults.activate)
self.activated = (activate != Constants.disabled)
self.hashZoneThreads = self._defaultIfNone(kw, 'vdoHashZoneThreads',
Defaults.hashZoneThreads)
self.logicalSize = kw.get('vdoLogicalSize', SizeString("0"))
self.logicalThreads = self._defaultIfNone(kw, 'vdoLogicalThreads',
Defaults.logicalThreads)
self.mdRaid5Mode = Defaults.mdRaid5Mode
self.physicalSize = SizeString("0")
self.physicalThreads = self._defaultIfNone(kw, 'vdoPhysicalThreads',
Defaults.physicalThreads)
readCache = self._defaultIfNone(kw, 'readCache', Defaults.readCache)
self.enableReadCache = (readCache != Constants.disabled)
self.readCacheSize = self._defaultIfNone(kw, 'readCacheSize',
Defaults.readCacheSize)
self.slabSize = self._defaultIfNone(kw, 'vdoSlabSize', Defaults.slabSize)
self._writePolicy = self._defaultIfNone(kw, 'writePolicy',
Defaults.writePolicy)
self._writePolicySet = False # track if the policy is explicitly set
self.instanceNumber = 0
self.vdoLogLevel = kw.get('vdoLogLevel')
self.indexCfreq = self._defaultIfNone(kw, 'cfreq', Defaults.cfreq)
self._setMemoryAttr(self._defaultIfNone(kw, 'indexMem',
Defaults.indexMem))
sparse = self._defaultIfNone(kw, 'sparseIndex', Defaults.sparseIndex)
self.indexSparse = (sparse != Constants.disabled)
self.indexThreads = self._defaultIfNone(kw, 'udsParallelFactor',
Defaults.udsParallelFactor)
######################################################################
def __setattr__(self, name, value):
if name == "readCache":
self.enableReadCache = value != Constants.disabled
elif name == 'indexMemory':
self._setMemoryAttr(value)
elif name == "writePolicy":
self._writePolicy = value
self._writePolicySet = True
elif name == 'identifier':
# Setting the identifier must work, since we might have an old config
# file with the identifier set, but we don't use it anymore so just
# drop it. (It's deprecated.)
pass
else:
super(VDOService, self).__setattr__(name, value)
# We need to round all logical sizes and physical sizes.
if name in ['logicalSize', 'physicalSize']:
getattr(self, name).roundToBlock()
######################################################################
# Protected methods
######################################################################
@staticmethod
def _defaultIfNone(args, name, default):
value = args.get(name)
return default if value is None else value
######################################################################
def _announce(self, action, option):
message = "{0} {1} on VDO ".format(action, option)
self.log.announce(_(message) + self.getName())
######################################################################
def _checkConfiguration(self):
"""Check and fix the configuration of this VDO.
Raises:
ServiceError
"""
cachePages = self.blockMapCacheSize.toBlocks()
if cachePages < 2 * 2048 * self.logicalThreads:
msg = _("Insufficient block map cache for {0}").format(self.getName())
raise ServiceError(msg, exitStatus = UserExitStatus)
# Adjust the block map period to be in its acceptable range.
self.blockMapPeriod = max(self.blockMapPeriod, Defaults.blockMapPeriodMin)
self.blockMapPeriod = min(self.blockMapPeriod, Defaults.blockMapPeriodMax)
######################################################################
def _clearMetadata(self):
"""Clear the VDO metadata from the storage device"""
try:
mode = os.stat(self.device).st_mode
if not stat.S_ISBLK(mode):
self.log.debug("Not clearing {devicePath}, not a block device".format(
devicePath=self.device))
return
except OSError as ex:
self.log.debug("Not clearing {devicePath}, cannot stat: {ex}".format(
devicePath=self.device, ex=ex))
return
command = ["dd",
"if=/dev/zero",
"of={devicePath}".format(devicePath=self.device),
"oflag=direct",
"bs=4096",
"count=1"]
runCommand(command)
######################################################################
def _computedConfig(self):
"""Update the instance properties as necessary and return the
configuration instance.
"""
# TODO: rework organization to avoid having to do local import
from . import Configuration
if not self._configUpgraded:
# There may be an older existing entry in the config which needs to be
# upgraded to account for attribute changes.
service = None
try:
service = self._config.getVdo(self.getName())
except ArgumentError:
pass
else:
# Getting here means we found an entry in the configuration.
# Upgrade the entry as necessary.
#
# If the entry is in the 'unknown' state that indicates that it is
# a pre-existing instance before we added the operation state to
# the configuration. These are to be treated as finished, but we
# don't have to force the update to disk.
if service._operationState == self.OperationState.unknown:
service._setOperationState(self.OperationState.finished,
persist=False)
self._configUpgraded = True
return self._config
######################################################################
def _computedIsConstructed(self):
"""Returns a boolean indicating if the instance represents a fully
constructed vdo.
"""
return self.operationState == self.OperationState.finished
######################################################################
def _computedOperationState(self):
"""Return the operation state of the instance.
If there is an instance in the configuration the state reported is from
that instance else it's from this instance.
"""
service = self
try:
service = self.config.getVdo(self.getName())
except ArgumentError:
pass
return service._operationState
######################################################################
def _computedPreviousOperationFailure(self):
"""Returns a boolean indicating if the instance operation failed.
"""
if self._previousOperationFailure is None:
# We access the operation state property in order to determine
# the operation failure status as that will give us the actual
# operation state of the existing (if so) entry in the config which
# represents the real operation state.
self._previousOperationFailure = ((self.operationState !=
self.OperationState.unknown)
and (self.operationState !=
self.OperationState.finished))
return self._previousOperationFailure
######################################################################
def _computedUnrecoverablePreviousOperationFailure(self):
"""Returns a boolean indicating if a previous operation failure cannot be
automatically recovered.
"""
return (self.previousOperationFailure
and (self.operationState == self.OperationState.beginCreate))
######################################################################
def _computedWritePolicy(self):
"""Return the write policy of the instance.
If this instance's write policy was not explicitly set and there is an
instance in the configuration the write policy reported is from that
instance else it's from this instance.
"""
service = self
if not self._writePolicySet:
try:
service = self.config.getVdo(self.getName())
except ArgumentError:
pass
return service._writePolicy
######################################################################
def _computeSlabBits(self):
"""Compute the --slab-bits parameter value for the slabSize attribute."""
# add some fudge because of imprecision in long arithmetic
blocks = self.slabSize.toBlocks()
return int(math.log(blocks, 2) + 0.05)
######################################################################
def _constructServiceStart(self):
self.log.debug("construction - starting; vdo {0}".format(self.getName()))
transaction = Transaction.transaction()
self.start()
transaction.addUndoStage(self.stop)
######################################################################
def _constructVdoFormat(self, force = False):
self.log.debug("construction - formatting logical volume; vdo {0}"
.format(self.getName()))
transaction = Transaction.transaction()
self._formatTarget(force)
transaction.addUndoStage(self.remove)
vdoConfig = self._getConfigFromVDO()
sectorsPerBlock = vdoConfig["blockSize"] / Constants.SECTOR_SIZE
physicalSize = vdoConfig["physicalBlocks"] * sectorsPerBlock
self.physicalSize = SizeString("{0}s".format(physicalSize))
logicalSize = vdoConfig["logicalBlocks"] * sectorsPerBlock
self.logicalSize = SizeString("{0}s".format(logicalSize))
######################################################################
def _createCheckCleanDevice(self):
"""Performs a verification for create that the storage device doesn't
already have something on it.
Raises:
VDOServiceError
"""
# Perform the same checks that LVM does (which doesn't yet include checking
# for an already-formatted VDO volume, but vdoformat does that), so we do
# it by...actually making LVM do it for us!
try:
runCommand(['pvcreate', '--config', 'devices/scan_lvs=1',
'-qq', '--test', self.device])
except CommandError as e:
# Messages from pvcreate aren't localized, so we can look at
# the message generated and pick it apart. This will need
# fixing if the message format changes or it gets localized.
lines = [line.strip() for line in e.getStandardError().splitlines()]
lineCount = len(lines)
if lineCount > 0:
for i in range(lineCount):
if (re.match(r"^TEST MODE", lines[i]) is not None):
for line in lines[i+1:]:
detectionMatch = re.match(r"WARNING: (.* detected .*)"
"\.\s+Wipe it\?", line)
if detectionMatch is not None:
raise VDOServiceError('{0}; use --force to override'
.format(detectionMatch.group(1)),
exitStatus = StateExitStatus)
break
# Use the last line from the test output.
# This will be the human-useful description of the problem.
e.setMessage(lines[-1])
# No TEST MODE message, just keep going.
raise e
# If this is a physical volume that's not in use by any logical
# volumes the above check won't trigger an error. So do a second
# check that catches that.
try:
runCommand(['blkid', '-p', self.device])
except CommandError as e:
if e.getExitCode() == 2:
return
raise VDOServiceError('device is a physical volume;'
+ ' use --force to override',
exitStatus = StateExitStatus)
######################################################################
def _determineInstanceNumber(self):
"""Determine the instance number of a running VDO using sysfs."""
path = "/sys/kvdo/{0}/instance".format(self.getName())
try:
with open(path, "r") as f:
self.instanceNumber = int(f.read().strip())
except Exception as err:
self.log.warning(
_("unable to determine VDO service {0} instance number: {1}").format(
self.getName(), err))
######################################################################
def _forceRebuild(self):
"""Calls vdoforcerebuild to exit read-only mode and force a metadata
rebuild at next start.
"""
runCommand(['vdoforcerebuild', self.device])
######################################################################
def _formatTarget(self, force):
"""Formats the VDO target."""
commandLine = ['vdoformat']
commandLine.append("--uds-checkpoint-frequency=" + str(self.indexCfreq))
memVal = self.indexMemory
if memVal == 0.0:
memVal = Defaults.indexMem
if not self.slabSize:
self.slabSize = Defaults.slabSize
commandLine.append("--uds-memory-size=" + str(memVal))
if self.indexSparse:
commandLine.append("--uds-sparse")
if self.logicalSize.toBytes() > 0:
commandLine.append("--logical-size=" + self.logicalSize.asLvmText())
if self.slabSize != Defaults.slabSize:
commandLine.append("--slab-bits=" + str(self._computeSlabBits()))
if force:
commandLine.append("--force")
commandLine.append(self.device)
runCommand(commandLine)
######################################################################
def _generateDeviceMapperTable(self):
"""Generate the device mapper table line from the properties of this
object.
"""
numSectors = self.logicalSize.toSectors()
cachePages = self.blockMapCacheSize.toBlocks()
threadCountConfig = ",".join(["ack=" + str(self.ackThreads),
"bio=" + str(self.bioThreads),
("bioRotationInterval="
+ str(self.bioRotationInterval)),
"cpu=" + str(self.cpuThreads),
"hash=" + str(self.hashZoneThreads),
"logical=" + str(self.logicalThreads),
"physical=" + str(self.physicalThreads)])
vdoConf = " ".join(["0", str(numSectors), Defaults.vdoTargetName,
self.device,
str(self.logicalBlockSize),
Constants.enableString(self.enableReadCache),
str(self.readCacheSize.toBlocks()),
str(cachePages), str(self.blockMapPeriod),
self.mdRaid5Mode, self.writePolicy,
self._name,
threadCountConfig])
return vdoConf
######################################################################
def _generateModifiedDmTable(self, **kwargs):
"""Changes the specified parameters in the dmsetup table to the specified
values.
Raises:
CommandError if the current table cannot be obtained
Returns:
a valid new dmsetup table.
"""
table = runCommand(["dmsetup", "table", self._name]).rstrip()
self.log.info(table)
# Parse the existing table.
tableOrder = ("logicalStart numSectors targetName storagePath blockSize"
+ " readCache readCacheBlocks cacheBlocks blockMapPeriod"
+ " mdRaid5Mode writePolicy poolName"
+ " threadCountConfig")
dmTable = dict(zip(tableOrder.split(" "), table.split(" ")))
# Apply new values
for (key, val) in kwargs.iteritems():
dmTable[key] = val
# Create and return the new table
return " ".join([dmTable[key] for key in tableOrder.split(" ")])
######################################################################
def _generatePreviousOperationFailureResponse(self, operation = "create"):
"""Generates the required response to a previous operation failure.
Logs a message indicating that the previous operation failed and raises the
VDOServicePreviousOperationError exception with the same message.
Arguments:
operation (str) - the operation that failed; default to "create" as that
is currently the only operation that is not
automatically recovered
Raises:
VDOServicePreviousOperationError
"""
msg = _("VDO volume {0} previous operation ({1}) is incomplete{2}").format(
self.getName(), operation,
"; recover by performing 'remove --force'"
if operation == "create" else "")
raise VDOServicePreviousOperationError(msg)
######################################################################
def _handlePreviousOperationFailure(self):
"""Handles a previous operation failure.
If the failure can be corrected automatically it is.
If not, the method logs a message indicating that the previous operation
failed and raises the VDOServicePreviousOperationError exception with
the same message.
Raises:
VDOServicePreviousOperationError if the previous operation failure
is a non-recoverable error.
VDOServiceError if unexpected/unhandled operation state is
encountered. Excepting corruption or incorrect setting of state this
indicates that the developer augmented the code with an operation (new or
old) which can experience a catastrophic failure requiring some form of
recovery but failed to update specificOperationStates() and/or failed to
add a clause to this method to address the new failure.
"""
if not self.previousOperationFailure:
self.log.debug(
_("No failure requiring recovery for VDO volume {0}").format(
self.getName()))
return
if (self.operationState
not in self.OperationState.specificOperationStates()):
msg = _("VDO volume {0} in unknown operation state: {1}").format(
self.getName(), self.operationState)
raise VDOServiceError(msg, exitStatus = DeveloperExitStatus)
elif self.operationState == self.OperationState.beginCreate:
# Create is not automatically recovered.
self._generatePreviousOperationFailureResponse()
elif self.operationState == self.OperationState.beginGrowLogical:
self._recoverGrowLogical()
elif self.operationState == self.OperationState.beginGrowPhysical:
self._recoverGrowPhysical()
elif self.operationState == self.OperationState.beginRunningSetWritePolicy:
self._recoverRunningSetWritePolicy()
else:
msg = _("Missing handler for recover from operation state: {0}").format(
self.operationState)
raise VDOServiceError(msg, exitStatus = DeveloperExitStatus)
self._previousOperationFailure = False
######################################################################
def _getBaseDevice(self, devicePath):
"""Take the server name and convert it to a device name that
can be opened from the kernel
Arguments:
devicePath (path): path to a device.
Raises:
ArgumentError
"""
resolvedPath = devicePath
if not os.access(resolvedPath, os.F_OK):
raise ArgumentError(
_("{path} does not exist").format(path = resolvedPath))
if stat.S_ISLNK(os.lstat(resolvedPath).st_mode):
cmd = Command(["readlink", "-f", resolvedPath])
resolvedPath = cmd.run().strip()
if resolvedPath == "":
raise ArgumentError(
_("{path} could not be resolved").format(path = resolvedPath))
return resolvedPath
######################################################################
def _getConfigFromVDO(self):
"""Returns a dictionary of the configuration values as reported from
the actual vdo storage.
"""
config = yaml.safe_load(runCommand(["vdodumpconfig",
self.device]))
return config["VDOConfig"]
######################################################################
def _getUUID(self):
"""Returns the uuid as reported from the actual vdo storage.
"""
config = yaml.safe_load(runCommand(["vdodumpconfig",
self.device]))
return "VDO-" + config["UUID"]
######################################################################
def _getDeduplicationStatus(self):
status = _("not available")
try:
output = runCommand(["dmsetup", "status", self.getName()])
fields = output.split(" ")
status = fields[Constants.dmsetupStatusFields.deduplicationStatus]
except Exception:
pass
return status
######################################################################
def _getDeviceUUID(self, devicePath):
"""Get the UUID of the device passed in,
Arguments:
devicePath (path): path to a device.
Returns:
UUID as a string, or None if none found
"""
try:
output = runCommand(["blkid", "-s", "UUID", "-o", "value",
devicePath]).strip()
except CommandError as ex:
self.log.info("blkid failed: " + str(ex))
return None
if output == "":
return None
else:
return output
######################################################################
def _hasHolders(self):
"""Tests whether other devices are holding the VDO device open. This
handles the case where there are LVM entities stacked on top of us.
Returns:
True iff the VDO device has something holding it open.
"""
try:
st = os.stat(self.getPath())
major = os.major(st.st_rdev)
minor = os.minor(st.st_rdev)
except OSError:
return False
holdersDirectory = "/sys/dev/block/{major}:{minor}/holders".format(
major=major, minor=minor)
if os.path.isdir(holdersDirectory):
holders = os.listdir(holdersDirectory)
if len(holders) > 0:
self.log.info("{path} is being held open by {holders}".format(
path=self.getPath(), holders=" ".join(holders)))
return True
return False
######################################################################
def _hasMounts(self):
"""Tests whether filesystems are mounted on the VDO device.
Returns:
True iff the VDO device has something mounted on it.
"""
mountList = runCommand(['mount'], noThrow=True)
if mountList:
matcher = re.compile(r'(\A|\s+)' + re.escape(self.getPath()) + r'\s+')
for line in mountList.splitlines():
if matcher.search(line):
return True
return False
######################################################################
def _installKernelModule(self, logLevel = None):
"""Install the kernel module providing VDO support.
Arguments:
logLevel: the level of logging to use; if None, do not set level
"""
kms = VDOKernelModuleService()
try:
kms.start()
except Exception:
self.log.error(_("Kernel module {0} not installed").format(
kms.getName()))
raise
if logLevel is not None:
kms.setLogLevel(logLevel)
######################################################################
def _mapperDeviceExists(self):
"""Returns True if there already exists a dm target with the name of
this VDO."""
try:
os.stat(self.getPath())
return True
except OSError:
return False
######################################################################
@transactional
def _performRunningSetWritePolicy(self):
"""Peforms the changing of the write policy on a running vdo instance.
"""
transaction = Transaction.transaction()
self._suspend()
transaction.addUndoStage(self._resume)
transaction.setMessage(self.log.error,
_("Device {0} could not be read").format(
self.getName()))
vdoConf = self._generateModifiedDmTable(writePolicy = self.writePolicy)
transaction.setMessage(self.log.error,
_("Device {0} could not be changed").format(
self.getName()))
runCommand(["dmsetup", "reload", self._name, "--table", vdoConf])
transaction.setMessage(None)
self._resume()
######################################################################
def _recoverGrowLogical(self):
"""Recovers a VDO target from a previous grow logical failure.
Raises:
VDOServiceError
"""
if not self.previousOperationFailure:
self.log.debug(
_("No grow logical recovery necessary for VDO volume {0}").format(
self.getName()))
elif self.operationState != self.OperationState.beginGrowLogical:
msg = _("Previous operation failure for VDO volume {0} not from"
" grow logical").format(self.getName())
raise VDOServiceError(msg, exitStatus = DeveloperExitStatus)
else:
# Get the correct logical size from vdo.
vdoConfig = self._getConfigFromVDO()
logicalSize = (vdoConfig["logicalBlocks"]
* (vdoConfig["blockSize"] / Constants.SECTOR_SIZE))
self.logicalSize = SizeString("{0}s".format(logicalSize))
# If vdo is running it's possible the failure came from the user
# interrupting the original command so we issue a resume.
# This is safe even if not necessary.
if self.running():
self._resume()
# Mark the operation as finished (which also updates and persists the
# configuration.
self._setOperationState(self.OperationState.finished)
######################################################################
def _recoverGrowPhysical(self):
"""Recovers a VDO target from a previous grow physical failure.
Raises:
VDOServiceError
"""
if not self.previousOperationFailure:
self.log.debug(
_("No grow physical recovery necessary for VDO volume {0}").format(
self.getName()))
elif self.operationState != self.OperationState.beginGrowPhysical:
msg = _("Previous operation failure for VDO volume {0} not from"
" grow physical").format(self.getName())
raise VDOServiceError(msg, exitStatus = DeveloperExitStatus)
else:
# Get the correct physical size from vdo.
vdoConfig = self._getConfigFromVDO()
physicalSize = (vdoConfig["physicalBlocks"]
* (vdoConfig["blockSize"] / Constants.SECTOR_SIZE))
self.physicalSize = SizeString("{0}s".format(physicalSize))
# If vdo is running it's possible the failure came from the user
# interrupting the original command so we issue a resume.
# This is safe even if not necessary.
if self.running():
self._resume()
# Mark the operation as finished (which also updates and persists the
# configuration.
self._setOperationState(self.OperationState.finished)
######################################################################
def _recoverRunningSetWritePolicy(self):
"""Recovers a VDO target from a previous setting of write policy against
a running VDO.
Raises:
VDOServiceError
"""
if not self.previousOperationFailure:
self.log.debug(
_("No set write policy recovery necessary for VDO volume {0}").format(
self.getName()))
elif self.operationState != self.OperationState.beginRunningSetWritePolicy:
msg = _("Previous operation failure for VDO volume {0} not from"
" set write policy").format(self.getName())
raise VDOServiceError(msg, exitStatus = DeveloperExitStatus)
else:
# Perform the recovery only if the vdo is actually running (indicating
# the user aborted the command).
# If the vdo is not running the value stored in the configuration is what
# we want to use and it will be used when starting the vdo.
# In both cases we can go ahead and mark the operation as finished.
if self.running():
self._performRunningSetWritePolicy()
# Mark the operation as finished (which also updates and persists the
# configuration.
self._setOperationState(self.OperationState.finished)
######################################################################
def _resume(self):
"""Resumes a suspended VDO."""
self.log.info(_("Resuming VDO volume {0}").format(self.getName()))
try:
runCommand(["dmsetup", "resume", self.getName()])
except Exception as ex:
self.log.error(_("Can't resume VDO volume {0}; {1!s}").format(
self.getName(), ex))
raise
self._startFullnessMonitoring()
self.log.info(_("Resumed VDO volume {0}").format(self.getName()))
######################################################################
def _setMemoryAttr(self, value):
memory = float(value)
if memory >= 1.0:
memory = int(memory)
# Call the superclass __setattr__ as this method is called from
# our __setattr__ and we need to avoid an infinite recursion.
super(VDOService, self).__setattr__("indexMemory", memory)
######################################################################
def _setOperationState(self, state, persist=True):
self._operationState = state
if persist:
self.config.addVdo(self.getName(), self, replace = True)
self.config.persist()
######################################################################
def _startCompression(self):
"""Starts compression on a VDO volume if it is running.
"""
self._toggleCompression(True)
######################################################################
def _startFullnessMonitoring(self):
try:
runCommand(["vdodmeventd", "-r", self.getName()])
except Exception:
self.log.info(_("Could not register {0}"
" with dmeventd").format(self.getName()))
pass
######################################################################
def _stopCompression(self):
"""Stops compression on a VDO volume if it is running.
"""
self._toggleCompression(False)
######################################################################
def _stopFullnessMonitoring(self, execute, removeSteps):
command = ["vdodmeventd", "-u", self.getName()]
if removeSteps is not None:
removeSteps.append(" ".join(command))
if execute:
runCommand(command, noThrow=True)
######################################################################
def _suspend(self):
"""Suspends a running VDO."""
self.log.info(_("Suspending VDO volume {0}").format(self.getName()))
self._stopFullnessMonitoring(True, None)
try:
runCommand(["dmsetup", "suspend", self.getName()])
except Exception as ex:
self.log.error(_("Can't suspend VDO volume {0}; {1!s}").format(
self.getName(), ex))
raise
self.log.info(_("Suspended VDO volume {0}").format(self.getName()))
######################################################################
def _toggleCompression(self, enable):
"""Turns compression on or off if the VDO is running.
Arguments:
enable (boolean): True if compression should be enabled
"""
if not self.running():
return
self.log.announce(_("{0} compression on VDO {1}").format(
"Starting" if enable else "Stopping", self.getName()))
runCommand(["dmsetup", "message", self.getName(), "0",
"compression", "on" if enable else "off"])
######################################################################
def _validateAvailableMemory(self, indexMemory):
"""Validates whether there is likely enough kernel memory to at least
create the index. If there is an error getting the info, don't
fail the create, just let the real check be done in vdoformat.
Arguments:
indexMemory - the amount of memory requested or default.
Raises:
ArgumentError
"""
# SizeString respects locale, so convert to localized representation
memoryNeeded = SizeString("{0}g".format(locale.str(float(indexMemory))))
memoryAvailable = None
try:
result = runCommand(['grep', 'MemAvailable', '/proc/meminfo'])
for line in result.splitlines():
memory = re.match(r"MemAvailable:\s*(\d+)", line)
if memory is not None:
available = memory.group(1)
memoryAvailable = SizeString("{0}k".format(available))
except Exception:
pass
if memoryAvailable is None:
self.log.info("Unable to validate available memory")
return;
if (memoryNeeded.toBytes() >= memoryAvailable.toBytes()):
raise ArgumentError(_("Not enough available memory in system"
" for index requirement of {needed}".format(
needed = memoryNeeded)));
######################################################################
def _validateModifiableThreadCounts(self, hashZone, logical, physical):
"""Validates that the hash zone, logical and physical thread counts
are consistent (all zero or all non-zero).
Arguments:
hashZone - hash zone thread count to use, may be None
logical - logical thread count to use, may be None
physical - physical thread count to use, may be None
Raises:
ArgumentError
"""
if hashZone is None:
hashZone = self.hashZoneThreads
if logical is None:
logical = self.logicalThreads
if physical is None:
physical = self.physicalThreads
if (((hashZone == 0) or (logical == 0) or (physical == 0))
and (not ((hashZone == 0) and (logical == 0) and (physical == 0)))):
raise ArgumentError(_("hash zone, logical and physical threads must"
" either all be zero or all be non-zero"))