#
#
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2010, 2011, 2012, 2013 Google 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
import re
import time
import errno
+import shlex
+import stat
import pyparsing as pyp
+import os
+import logging
+import math
from ganeti import utils
-from ganeti import logger
from ganeti import errors
from ganeti import constants
+from ganeti import objects
+from ganeti import compat
+from ganeti import netutils
+from ganeti import pathutils
+from ganeti import serializer
+
+
+# Size of reads in _CanReadDevice
+_DEVICE_READ_SIZE = 128 * 1024
+
+
+class RbdShowmappedJsonError(Exception):
+ """`rbd showmmapped' JSON formatting error Exception class.
+
+ """
+ pass
+
+
+def _IgnoreError(fn, *args, **kwargs):
+ """Executes the given function, ignoring BlockDeviceErrors.
+
+ This is used in order to simplify the execution of cleanup or
+ rollback functions.
+
+ @rtype: boolean
+ @return: True when fn didn't raise an exception, False otherwise
+
+ """
+ try:
+ fn(*args, **kwargs)
+ return True
+ except errors.BlockDeviceError, err:
+ logging.warning("Caught BlockDeviceError but ignoring: %s", str(err))
+ return False
+
+
+def _ThrowError(msg, *args):
+ """Log an error to the node daemon and the raise an exception.
+
+ @type msg: string
+ @param msg: the text of the exception
+ @raise errors.BlockDeviceError
+
+ """
+ if args:
+ msg = msg % args
+ logging.error(msg)
+ raise errors.BlockDeviceError(msg)
+
+
+def _CheckResult(result):
+ """Throws an error if the given result is a failed one.
+
+ @param result: result from RunCmd
+
+ """
+ if result.failed:
+ _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
+ result.output)
+
+
+def _CanReadDevice(path):
+ """Check if we can read from the given device.
+
+ This tries to read the first 128k of the device.
+
+ """
+ try:
+ utils.ReadFile(path, size=_DEVICE_READ_SIZE)
+ return True
+ except EnvironmentError:
+ logging.warning("Can't read from device %s", path, exc_info=True)
+ return False
+
+
+def _GetForbiddenFileStoragePaths():
+ """Builds a list of path prefixes which shouldn't be used for file storage.
+
+ @rtype: frozenset
+
+ """
+ paths = set([
+ "/boot",
+ "/dev",
+ "/etc",
+ "/home",
+ "/proc",
+ "/root",
+ "/sys",
+ ])
+
+ for prefix in ["", "/usr", "/usr/local"]:
+ paths.update(map(lambda s: "%s/%s" % (prefix, s),
+ ["bin", "lib", "lib32", "lib64", "sbin"]))
+
+ return compat.UniqueFrozenset(map(os.path.normpath, paths))
+
+
+def _ComputeWrongFileStoragePaths(paths,
+ _forbidden=_GetForbiddenFileStoragePaths()):
+ """Cross-checks a list of paths for prefixes considered bad.
+
+ Some paths, e.g. "/bin", should not be used for file storage.
+
+ @type paths: list
+ @param paths: List of paths to be checked
+ @rtype: list
+ @return: Sorted list of paths for which the user should be warned
+
+ """
+ def _Check(path):
+ return (not os.path.isabs(path) or
+ path in _forbidden or
+ filter(lambda p: utils.IsBelowDir(p, path), _forbidden))
+
+ return utils.NiceSort(filter(_Check, map(os.path.normpath, paths)))
+
+
+def ComputeWrongFileStoragePaths(_filename=pathutils.FILE_STORAGE_PATHS_FILE):
+ """Returns a list of file storage paths whose prefix is considered bad.
+
+ See L{_ComputeWrongFileStoragePaths}.
+
+ """
+ return _ComputeWrongFileStoragePaths(_LoadAllowedFileStoragePaths(_filename))
+
+
+def _CheckFileStoragePath(path, allowed):
+ """Checks if a path is in a list of allowed paths for file storage.
+
+ @type path: string
+ @param path: Path to check
+ @type allowed: list
+ @param allowed: List of allowed paths
+ @raise errors.FileStoragePathError: If the path is not allowed
+
+ """
+ if not os.path.isabs(path):
+ raise errors.FileStoragePathError("File storage path must be absolute,"
+ " got '%s'" % path)
+
+ for i in allowed:
+ if not os.path.isabs(i):
+ logging.info("Ignoring relative path '%s' for file storage", i)
+ continue
+
+ if utils.IsBelowDir(i, path):
+ break
+ else:
+ raise errors.FileStoragePathError("Path '%s' is not acceptable for file"
+ " storage" % path)
+
+
+def _LoadAllowedFileStoragePaths(filename):
+ """Loads file containing allowed file storage paths.
+
+ @rtype: list
+ @return: List of allowed paths (can be an empty list)
+
+ """
+ try:
+ contents = utils.ReadFile(filename)
+ except EnvironmentError:
+ return []
+ else:
+ return utils.FilterEmptyLinesAndComments(contents)
+
+
+def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE):
+ """Checks if a path is allowed for file storage.
+
+ @type path: string
+ @param path: Path to check
+ @raise errors.FileStoragePathError: If the path is not allowed
+
+ """
+ allowed = _LoadAllowedFileStoragePaths(_filename)
+
+ if _ComputeWrongFileStoragePaths([path]):
+ raise errors.FileStoragePathError("Path '%s' uses a forbidden prefix" %
+ path)
+
+ _CheckFileStoragePath(path, allowed)
class BlockDev(object):
- online (=used, or ready for use)
A device can also be online but read-only, however we are not using
- the readonly state (MD and LV have it, if needed in the future)
- and we are usually looking at this like at a stack, so it's easier
- to conceptualise the transition from not-existing to online and back
+ the readonly state (LV has it, if needed in the future) and we are
+ usually looking at this like at a stack, so it's easier to
+ conceptualise the transition from not-existing to online and back
like a linear one.
The many different states of the device are due to the fact that we
need to cover many device types:
- logical volumes are created, lvchange -a y $lv, and used
- - md arrays are created or assembled and used
- drbd devices are attached to a local disk/remote peer and made primary
- The status of the device can be examined by `GetStatus()`, which
- returns a numerical value, depending on the position in the
- transition stack of the device.
-
A block device is identified by three items:
- the /dev path of the device (dynamic)
- a unique ID of the device (static)
Not all devices implement both the first two as distinct items. LVM
logical volumes have their unique ID (the pair volume group, logical
- volume name) in a 1-to-1 relation to the dev path. For MD devices,
- the /dev path is dynamic and the unique ID is the UUID generated at
- array creation plus the slave list. For DRBD devices, the /dev path
- is again dynamic and the unique id is the pair (host1, dev1),
- (host2, dev2).
+ volume name) in a 1-to-1 relation to the dev path. For DRBD devices,
+ the /dev path is again dynamic and the unique id is the pair (host1,
+ dev1), (host2, dev2).
You can get to a device in two ways:
- creating the (real) device, which returns you
- an attached instance (lvcreate, mdadm --create)
+ an attached instance (lvcreate)
- attaching of a python instance to an existing (real) device
- The second point, the attachement to a device, is different
+ The second point, the attachment to a device, is different
depending on whether the device is assembled or not. At init() time,
we search for a device with the same unique_id as us. If found,
good. It also means that the device is already assembled. If not,
after assembly we'll have our correct major/minor.
"""
- STATUS_UNKNOWN = 0
- STATUS_EXISTING = 1
- STATUS_STANDBY = 2
- STATUS_ONLINE = 3
-
- STATUS_MAP = {
- STATUS_UNKNOWN: "unknown",
- STATUS_EXISTING: "existing",
- STATUS_STANDBY: "ready for use",
- STATUS_ONLINE: "online",
- }
-
- def __init__(self, unique_id, children):
+ def __init__(self, unique_id, children, size, params):
self._children = children
self.dev_path = None
self.unique_id = unique_id
self.major = None
self.minor = None
+ self.attached = False
+ self.size = size
+ self.params = params
def Assemble(self):
"""Assemble the device from its components.
- If this is a plain block device (e.g. LVM) than assemble does
- nothing, as the LVM has no children and we don't put logical
- volumes offline.
-
- One guarantee is that after the device has been assembled, it
- knows its major/minor numbers. This allows other devices (usually
- parents) to probe correctly for their children.
+ Implementations of this method by child classes must ensure that:
+ - after the device has been assembled, it knows its major/minor
+ numbers; this allows other devices (usually parents) to probe
+ correctly for their children
+ - calling this method on an existing, in-use device is safe
+ - if the device is already configured (and in an OK state),
+ this method is idempotent
"""
- status = True
- for child in self._children:
- if not isinstance(child, BlockDev):
- raise TypeError("Invalid child passed of type '%s'" % type(child))
- if not status:
- break
- status = status and child.Assemble()
- if not status:
- break
- status = status and child.Open()
-
- if not status:
- for child in self._children:
- child.Shutdown()
- return status
+ pass
def Attach(self):
"""Find a device which matches our config and attach to it.
raise NotImplementedError
@classmethod
- def Create(cls, unique_id, children, size):
+ def Create(cls, unique_id, children, size, params, excl_stor):
"""Create the device.
If the device cannot be created, it will return None
def Remove(self):
"""Remove this device.
- This makes sense only for some of the device types: LV and to a
- lesser degree, md devices. Also note that if the device can't
- attach, the removal can't be completed.
+ This makes sense only for some of the device types: LV and file
+ storage. Also note that if the device can't attach, the removal
+ can't be completed.
"""
raise NotImplementedError
"""
raise NotImplementedError
- def GetStatus(self):
- """Return the status of the device.
-
- """
- raise NotImplementedError
-
def Open(self, force=False):
"""Make the device ready for use.
"""
raise NotImplementedError
- def SetSyncSpeed(self, speed):
- """Adjust the sync speed of the mirror.
+ def SetSyncParams(self, params):
+ """Adjust the synchronization parameters of the mirror.
+
+ In case this is not a mirroring device, this is no-op.
+
+ @param params: dictionary of LD level disk parameters related to the
+ synchronization.
+ @rtype: list
+ @return: a list of error messages, emitted both by the current node and by
+ children. An empty list means no errors.
+
+ """
+ result = []
+ if self._children:
+ for child in self._children:
+ result.extend(child.SetSyncParams(params))
+ return result
+
+ def PauseResumeSync(self, pause):
+ """Pause/Resume the sync of the mirror.
In case this is not a mirroring device, this is no-op.
+ @param pause: Whether to pause or resume
+
"""
result = True
if self._children:
for child in self._children:
- result = result and child.SetSyncSpeed(speed)
+ result = result and child.PauseResumeSync(pause)
return result
def GetSyncStatus(self):
If this device is a mirroring device, this function returns the
status of the mirror.
- Returns:
- (sync_percent, estimated_time, is_degraded, ldisk)
-
If sync_percent is None, it means the device is not syncing.
If estimated_time is None, it means we can't estimate
data. This is only valid for some devices, the rest will always
return False (not degraded).
- """
- return None, None, False, False
+ @rtype: objects.BlockDevStatus
+ """
+ return objects.BlockDevStatus(dev_path=self.dev_path,
+ major=self.major,
+ minor=self.minor,
+ sync_percent=None,
+ estimated_time=None,
+ is_degraded=False,
+ ldisk_status=constants.LDS_OKAY)
def CombinedSyncStatus(self):
"""Calculate the mirror status recursively for our children.
minimum percent and maximum time are calculated across our
children.
+ @rtype: objects.BlockDevStatus
+
"""
- min_percent, max_time, is_degraded, ldisk = self.GetSyncStatus()
+ status = self.GetSyncStatus()
+
+ min_percent = status.sync_percent
+ max_time = status.estimated_time
+ is_degraded = status.is_degraded
+ ldisk_status = status.ldisk_status
+
if self._children:
for child in self._children:
- c_percent, c_time, c_degraded, c_ldisk = child.GetSyncStatus()
+ child_status = child.GetSyncStatus()
+
if min_percent is None:
- min_percent = c_percent
- elif c_percent is not None:
- min_percent = min(min_percent, c_percent)
+ min_percent = child_status.sync_percent
+ elif child_status.sync_percent is not None:
+ min_percent = min(min_percent, child_status.sync_percent)
+
if max_time is None:
- max_time = c_time
- elif c_time is not None:
- max_time = max(max_time, c_time)
- is_degraded = is_degraded or c_degraded
- ldisk = ldisk or c_ldisk
- return min_percent, max_time, is_degraded, ldisk
+ max_time = child_status.estimated_time
+ elif child_status.estimated_time is not None:
+ max_time = max(max_time, child_status.estimated_time)
+
+ is_degraded = is_degraded or child_status.is_degraded
+ if ldisk_status is None:
+ ldisk_status = child_status.ldisk_status
+ elif child_status.ldisk_status is not None:
+ ldisk_status = max(ldisk_status, child_status.ldisk_status)
+
+ return objects.BlockDevStatus(dev_path=self.dev_path,
+ major=self.major,
+ minor=self.minor,
+ sync_percent=min_percent,
+ estimated_time=max_time,
+ is_degraded=is_degraded,
+ ldisk_status=ldisk_status)
def SetInfo(self, text):
"""Update metadata with info text.
for child in self._children:
child.SetInfo(text)
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the block device.
+
+ @type amount: integer
+ @param amount: the amount (in mebibytes) to grow with
+ @type dryrun: boolean
+ @param dryrun: whether to execute the operation in simulation mode
+ only, without actually increasing the size
+ @param backingstore: whether to execute the operation on backing storage
+ only, or on "logical" storage only; e.g. DRBD is logical storage,
+ whereas LVM, file, RBD are backing storage
+
+ """
+ raise NotImplementedError
+
+ def GetActualSize(self):
+ """Return the actual disk size.
+
+ @note: the device needs to be active when this is called
+
+ """
+ assert self.attached, "BlockDevice not attached in GetActualSize()"
+ result = utils.RunCmd(["blockdev", "--getsize64", self.dev_path])
+ if result.failed:
+ _ThrowError("blockdev failed (%s): %s",
+ result.fail_reason, result.output)
+ try:
+ sz = int(result.output.strip())
+ except (ValueError, TypeError), err:
+ _ThrowError("Failed to parse blockdev output: %s", str(err))
+ return sz
def __repr__(self):
return ("<%s: unique_id: %s, children: %s, %s:%s, %s>" %
"""Logical Volume block device.
"""
- def __init__(self, unique_id, children):
+ _VALID_NAME_RE = re.compile("^[a-zA-Z0-9+_.-]*$")
+ _INVALID_NAMES = compat.UniqueFrozenset([".", "..", "snapshot", "pvmove"])
+ _INVALID_SUBSTRINGS = compat.UniqueFrozenset(["_mlog", "_mimage"])
+
+ def __init__(self, unique_id, children, size, params):
"""Attaches to a LV device.
The unique_id is a tuple (vg_name, lv_name)
"""
- super(LogicalVolume, self).__init__(unique_id, children)
+ super(LogicalVolume, self).__init__(unique_id, children, size, params)
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
raise ValueError("Invalid configuration data %s" % str(unique_id))
self._vg_name, self._lv_name = unique_id
- self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
+ self._ValidateName(self._vg_name)
+ self._ValidateName(self._lv_name)
+ self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
+ self._degraded = True
+ self.major = self.minor = self.pe_size = self.stripe_count = None
self.Attach()
+ @staticmethod
+ def _GetStdPvSize(pvs_info):
+ """Return the the standard PV size (used with exclusive storage).
+
+ @param pvs_info: list of objects.LvmPvInfo, cannot be empty
+ @rtype: float
+ @return: size in MiB
+
+ """
+ assert len(pvs_info) > 0
+ smallest = min([pv.size for pv in pvs_info])
+ return smallest / (1 + constants.PART_MARGIN + constants.PART_RESERVED)
+
+ @staticmethod
+ def _ComputeNumPvs(size, pvs_info):
+ """Compute the number of PVs needed for an LV (with exclusive storage).
+
+ @type size: float
+ @param size: LV size in MiB
+ @param pvs_info: list of objects.LvmPvInfo, cannot be empty
+ @rtype: integer
+ @return: number of PVs needed
+ """
+ assert len(pvs_info) > 0
+ pv_size = float(LogicalVolume._GetStdPvSize(pvs_info))
+ return int(math.ceil(float(size) / pv_size))
+
+ @staticmethod
+ def _GetEmptyPvNames(pvs_info, max_pvs=None):
+ """Return a list of empty PVs, by name.
+
+ """
+ empty_pvs = filter(objects.LvmPvInfo.IsEmpty, pvs_info)
+ if max_pvs is not None:
+ empty_pvs = empty_pvs[:max_pvs]
+ return map((lambda pv: pv.name), empty_pvs)
+
@classmethod
- def Create(cls, unique_id, children, size):
+ def Create(cls, unique_id, children, size, params, excl_stor):
"""Create a new logical volume.
"""
if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
- raise ValueError("Invalid configuration data %s" % str(unique_id))
+ raise errors.ProgrammerError("Invalid configuration data %s" %
+ str(unique_id))
vg_name, lv_name = unique_id
- pvs_info = cls.GetPVInfo(vg_name)
+ cls._ValidateName(vg_name)
+ cls._ValidateName(lv_name)
+ pvs_info = cls.GetPVInfo([vg_name])
if not pvs_info:
- raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
- vg_name)
- pvs_info.sort()
- pvs_info.reverse()
-
- pvlist = [ pv[1] for pv in pvs_info ]
- free_size = sum([ pv[0] for pv in pvs_info ])
+ if excl_stor:
+ msg = "No (empty) PVs found"
+ else:
+ msg = "Can't compute PV info for vg %s" % vg_name
+ _ThrowError(msg)
+ pvs_info.sort(key=(lambda pv: pv.free), reverse=True)
+
+ pvlist = [pv.name for pv in pvs_info]
+ if compat.any(":" in v for v in pvlist):
+ _ThrowError("Some of your PVs have the invalid character ':' in their"
+ " name, this is not supported - please filter them out"
+ " in lvm.conf using either 'filter' or 'preferred_names'")
+
+ current_pvs = len(pvlist)
+ desired_stripes = params[constants.LDP_STRIPES]
+ stripes = min(current_pvs, desired_stripes)
+
+ if excl_stor:
+ (err_msgs, _) = utils.LvmExclusiveCheckNodePvs(pvs_info)
+ if err_msgs:
+ for m in err_msgs:
+ logging.warning(m)
+ req_pvs = cls._ComputeNumPvs(size, pvs_info)
+ pvlist = cls._GetEmptyPvNames(pvs_info, req_pvs)
+ current_pvs = len(pvlist)
+ if current_pvs < req_pvs:
+ _ThrowError("Not enough empty PVs to create a disk of %d MB:"
+ " %d available, %d needed", size, current_pvs, req_pvs)
+ assert current_pvs == len(pvlist)
+ if stripes > current_pvs:
+ # No warning issued for this, as it's no surprise
+ stripes = current_pvs
- # The size constraint should have been checked from the master before
- # calling the create function.
- if free_size < size:
- raise errors.BlockDeviceError("Not enough free space: required %s,"
- " available %s" % (size, free_size))
- result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-n%s" % lv_name,
- vg_name] + pvlist)
+ else:
+ if stripes < desired_stripes:
+ logging.warning("Could not use %d stripes for VG %s, as only %d PVs are"
+ " available.", desired_stripes, vg_name, current_pvs)
+ free_size = sum([pv.free for pv in pvs_info])
+ # The size constraint should have been checked from the master before
+ # calling the create function.
+ if free_size < size:
+ _ThrowError("Not enough free space: required %s,"
+ " available %s", size, free_size)
+
+ # If the free space is not well distributed, we won't be able to
+ # create an optimally-striped volume; in that case, we want to try
+ # with N, N-1, ..., 2, and finally 1 (non-stripped) number of
+ # stripes
+ cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
+ for stripes_arg in range(stripes, 0, -1):
+ result = utils.RunCmd(cmd + ["-i%d" % stripes_arg] + [vg_name] + pvlist)
+ if not result.failed:
+ break
if result.failed:
- raise errors.BlockDeviceError(result.fail_reason)
- return LogicalVolume(unique_id, children)
+ _ThrowError("LV create failed (%s): %s",
+ result.fail_reason, result.output)
+ return LogicalVolume(unique_id, children, size, params)
@staticmethod
- def GetPVInfo(vg_name):
+ def _GetVolumeInfo(lvm_cmd, fields):
+ """Returns LVM Volume infos using lvm_cmd
+
+ @param lvm_cmd: Should be one of "pvs", "vgs" or "lvs"
+ @param fields: Fields to return
+ @return: A list of dicts each with the parsed fields
+
+ """
+ if not fields:
+ raise errors.ProgrammerError("No fields specified")
+
+ sep = "|"
+ cmd = [lvm_cmd, "--noheadings", "--nosuffix", "--units=m", "--unbuffered",
+ "--separator=%s" % sep, "-o%s" % ",".join(fields)]
+
+ result = utils.RunCmd(cmd)
+ if result.failed:
+ raise errors.CommandError("Can't get the volume information: %s - %s" %
+ (result.fail_reason, result.output))
+
+ data = []
+ for line in result.stdout.splitlines():
+ splitted_fields = line.strip().split(sep)
+
+ if len(fields) != len(splitted_fields):
+ raise errors.CommandError("Can't parse %s output: line '%s'" %
+ (lvm_cmd, line))
+
+ data.append(splitted_fields)
+
+ return data
+
+ @classmethod
+ def GetPVInfo(cls, vg_names, filter_allocatable=True, include_lvs=False):
"""Get the free space info for PVs in a volume group.
- Args:
- vg_name: the volume group name
+ @param vg_names: list of volume group names, if empty all will be returned
+ @param filter_allocatable: whether to skip over unallocatable PVs
+ @param include_lvs: whether to include a list of LVs hosted on each PV
- Returns:
- list of (free_space, name) with free_space in mebibytes
+ @rtype: list
+ @return: list of objects.LvmPvInfo objects
"""
- command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
- "-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
- "--separator=:"]
- result = utils.RunCmd(command)
- if result.failed:
- logger.Error("Can't get the PV information: %s" % result.fail_reason)
+ # We request "lv_name" field only if we care about LVs, so we don't get
+ # a long list of entries with many duplicates unless we really have to.
+ # The duplicate "pv_name" field will be ignored.
+ if include_lvs:
+ lvfield = "lv_name"
+ else:
+ lvfield = "pv_name"
+ try:
+ info = cls._GetVolumeInfo("pvs", ["pv_name", "vg_name", "pv_free",
+ "pv_attr", "pv_size", lvfield])
+ except errors.GenericError, err:
+ logging.error("Can't get PV information: %s", err)
return None
+
+ # When asked for LVs, "pvs" may return multiple entries for the same PV-LV
+ # pair. We sort entries by PV name and then LV name, so it's easy to weed
+ # out duplicates.
+ if include_lvs:
+ info.sort(key=(lambda i: (i[0], i[5])))
data = []
- for line in result.stdout.splitlines():
- fields = line.strip().split(':')
- if len(fields) != 4:
- logger.Error("Can't parse pvs output: line '%s'" % line)
- return None
- # skip over pvs from another vg or ones which are not allocatable
- if fields[1] != vg_name or fields[3][0] != 'a':
+ lastpvi = None
+ for (pv_name, vg_name, pv_free, pv_attr, pv_size, lv_name) in info:
+ # (possibly) skip over pvs which are not allocatable
+ if filter_allocatable and pv_attr[0] != "a":
+ continue
+ # (possibly) skip over pvs which are not in the right volume group(s)
+ if vg_names and vg_name not in vg_names:
+ continue
+ # Beware of duplicates (check before inserting)
+ if lastpvi and lastpvi.name == pv_name:
+ if include_lvs and lv_name:
+ if not lastpvi.lv_list or lastpvi.lv_list[-1] != lv_name:
+ lastpvi.lv_list.append(lv_name)
+ else:
+ if include_lvs and lv_name:
+ lvl = [lv_name]
+ else:
+ lvl = []
+ lastpvi = objects.LvmPvInfo(name=pv_name, vg_name=vg_name,
+ size=float(pv_size), free=float(pv_free),
+ attributes=pv_attr, lv_list=lvl)
+ data.append(lastpvi)
+
+ return data
+
+ @classmethod
+ def _GetExclusiveStorageVgFree(cls, vg_name):
+ """Return the free disk space in the given VG, in exclusive storage mode.
+
+ @type vg_name: string
+ @param vg_name: VG name
+ @rtype: float
+ @return: free space in MiB
+ """
+ pvs_info = cls.GetPVInfo([vg_name])
+ if not pvs_info:
+ return 0.0
+ pv_size = cls._GetStdPvSize(pvs_info)
+ num_pvs = len(cls._GetEmptyPvNames(pvs_info))
+ return pv_size * num_pvs
+
+ @classmethod
+ def GetVGInfo(cls, vg_names, excl_stor, filter_readonly=True):
+ """Get the free space info for specific VGs.
+
+ @param vg_names: list of volume group names, if empty all will be returned
+ @param excl_stor: whether exclusive_storage is enabled
+ @param filter_readonly: whether to skip over readonly VGs
+
+ @rtype: list
+ @return: list of tuples (free_space, total_size, name) with free_space in
+ MiB
+
+ """
+ try:
+ info = cls._GetVolumeInfo("vgs", ["vg_name", "vg_free", "vg_attr",
+ "vg_size"])
+ except errors.GenericError, err:
+ logging.error("Can't get VG information: %s", err)
+ return None
+
+ data = []
+ for vg_name, vg_free, vg_attr, vg_size in info:
+ # (possibly) skip over vgs which are not writable
+ if filter_readonly and vg_attr[0] == "r":
+ continue
+ # (possibly) skip over vgs which are not in the right volume group(s)
+ if vg_names and vg_name not in vg_names:
continue
- data.append((float(fields[2]), fields[0]))
+ # Exclusive storage needs a different concept of free space
+ if excl_stor:
+ es_free = cls._GetExclusiveStorageVgFree(vg_name)
+ assert es_free <= vg_free
+ vg_free = es_free
+ data.append((float(vg_free), float(vg_size), vg_name))
return data
+ @classmethod
+ def _ValidateName(cls, name):
+ """Validates that a given name is valid as VG or LV name.
+
+ The list of valid characters and restricted names is taken out of
+ the lvm(8) manpage, with the simplification that we enforce both
+ VG and LV restrictions on the names.
+
+ """
+ if (not cls._VALID_NAME_RE.match(name) or
+ name in cls._INVALID_NAMES or
+ compat.any(substring in name for substring in cls._INVALID_SUBSTRINGS)):
+ _ThrowError("Invalid LVM name '%s'", name)
+
def Remove(self):
"""Remove this logical volume.
"""
if not self.minor and not self.Attach():
# the LV does not exist
- return True
+ return
result = utils.RunCmd(["lvremove", "-f", "%s/%s" %
(self._vg_name, self._lv_name)])
if result.failed:
- logger.Error("Can't lvremove: %s" % result.fail_reason)
-
- return not result.failed
+ _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output)
def Rename(self, new_id):
"""Rename this logical volume.
(self._vg_name, new_vg))
result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name])
if result.failed:
- raise errors.BlockDeviceError("Failed to rename the logical volume: %s" %
- result.output)
+ _ThrowError("Failed to rename the logical volume: %s", result.output)
self._lv_name = new_name
- self.dev_path = "/dev/%s/%s" % (self._vg_name, self._lv_name)
-
+ self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name)
def Attach(self):
"""Attach to an existing LV.
This method will try to see if an existing and active LV exists
- which matches the our name. If so, its major/minor will be
+ which matches our name. If so, its major/minor will be
recorded.
"""
- result = utils.RunCmd(["lvdisplay", self.dev_path])
+ self.attached = False
+ result = utils.RunCmd(["lvs", "--noheadings", "--separator=,",
+ "--units=k", "--nosuffix",
+ "-olv_attr,lv_kernel_major,lv_kernel_minor,"
+ "vg_extent_size,stripes", self.dev_path])
if result.failed:
- logger.Error("Can't find LV %s: %s, %s" %
- (self.dev_path, result.fail_reason, result.output))
+ logging.error("Can't find LV %s: %s, %s",
+ self.dev_path, result.fail_reason, result.output)
return False
- match = re.compile("^ *Block device *([0-9]+):([0-9]+).*$")
- for line in result.stdout.splitlines():
- match_result = match.match(line)
- if match_result:
- self.major = int(match_result.group(1))
- self.minor = int(match_result.group(2))
- return True
- return False
+ # the output can (and will) have multiple lines for multi-segment
+ # LVs, as the 'stripes' parameter is a segment one, so we take
+ # only the last entry, which is the one we're interested in; note
+ # that with LVM2 anyway the 'stripes' value must be constant
+ # across segments, so this is a no-op actually
+ out = result.stdout.splitlines()
+ if not out: # totally empty result? splitlines() returns at least
+ # one line for any non-empty string
+ logging.error("Can't parse LVS output, no lines? Got '%s'", str(out))
+ return False
+ out = out[-1].strip().rstrip(",")
+ out = out.split(",")
+ if len(out) != 5:
+ logging.error("Can't parse LVS output, len(%s) != 5", str(out))
+ return False
+
+ status, major, minor, pe_size, stripes = out
+ if len(status) < 6:
+ logging.error("lvs lv_attr is not at least 6 characters (%s)", status)
+ return False
+
+ try:
+ major = int(major)
+ minor = int(minor)
+ except (TypeError, ValueError), err:
+ logging.error("lvs major/minor cannot be parsed: %s", str(err))
+
+ try:
+ pe_size = int(float(pe_size))
+ except (TypeError, ValueError), err:
+ logging.error("Can't parse vg extent size: %s", err)
+ return False
+
+ try:
+ stripes = int(stripes)
+ except (TypeError, ValueError), err:
+ logging.error("Can't parse the number of stripes: %s", err)
+ return False
+
+ self.major = major
+ self.minor = minor
+ self.pe_size = pe_size
+ self.stripe_count = stripes
+ self._degraded = status[0] == "v" # virtual volume, i.e. doesn't backing
+ # storage
+ self.attached = True
+ return True
def Assemble(self):
"""Assemble the device.
- This is a no-op for the LV device type. Eventually, we could
- lvchange -ay here if we see that the LV is not active.
+ We always run `lvchange -ay` on the LV to ensure it's active before
+ use, as there were cases when xenvg was not active after boot
+ (also possibly after disk issues).
"""
- return True
+ result = utils.RunCmd(["lvchange", "-ay", self.dev_path])
+ if result.failed:
+ _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output)
def Shutdown(self):
"""Shutdown the device.
volumes on shutdown.
"""
- return True
-
- def GetStatus(self):
- """Return the status of the device.
-
- Logical volumes will can be in all four states, although we don't
- deactivate (lvchange -an) them when shutdown, so STATUS_EXISTING
- should not be seen for our devices.
-
- """
- result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
- if result.failed:
- logger.Error("Can't display lv: %s" % result.fail_reason)
- return self.STATUS_UNKNOWN
- out = result.stdout.strip()
- # format: type/permissions/alloc/fixed_minor/state/open
- if len(out) != 6:
- return self.STATUS_UNKNOWN
- #writable = (out[1] == "w")
- active = (out[4] == "a")
- online = (out[5] == "o")
- if online:
- retval = self.STATUS_ONLINE
- elif active:
- retval = self.STATUS_STANDBY
- else:
- retval = self.STATUS_EXISTING
-
- return retval
+ pass
def GetSyncStatus(self):
"""Returns the sync status of the device.
If this device is a mirroring device, this function returns the
status of the mirror.
- Returns:
- (sync_percent, estimated_time, is_degraded, ldisk)
-
For logical volumes, sync_percent and estimated_time are always
None (no recovery in progress, as we don't handle the mirrored LV
case). The is_degraded parameter is the inverse of the ldisk
physical disk failure and subsequent 'vgreduce --removemissing' on
the volume group.
+ The status was already read in Attach, so we just return it.
+
+ @rtype: objects.BlockDevStatus
+
"""
- result = utils.RunCmd(["lvs", "--noheadings", "-olv_attr", self.dev_path])
- if result.failed:
- logger.Error("Can't display lv: %s" % result.fail_reason)
- return None, None, True, True
- out = result.stdout.strip()
- # format: type/permissions/alloc/fixed_minor/state/open
- if len(out) != 6:
- logger.Debug("Error in lvs output: attrs=%s, len != 6" % out)
- return None, None, True, True
- ldisk = out[0] == 'v' # virtual volume, i.e. doesn't have
- # backing storage
- return None, None, ldisk, ldisk
+ if self._degraded:
+ ldisk_status = constants.LDS_FAULTY
+ else:
+ ldisk_status = constants.LDS_OKAY
+
+ return objects.BlockDevStatus(dev_path=self.dev_path,
+ major=self.major,
+ minor=self.minor,
+ sync_percent=None,
+ estimated_time=None,
+ is_degraded=self._degraded,
+ ldisk_status=ldisk_status)
def Open(self, force=False):
"""Make the device ready for I/O.
This is a no-op for the LV device type.
"""
- return True
+ pass
def Close(self):
"""Notifies that the device will no longer be used for I/O.
This is a no-op for the LV device type.
"""
- return True
+ pass
def Snapshot(self, size):
"""Create a snapshot copy of an lvm block device.
+ @returns: tuple (vg, lv)
+
"""
snap_name = self._lv_name + ".snap"
# remove existing snapshot if found
- snap = LogicalVolume((self._vg_name, snap_name), None)
- snap.Remove()
+ snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params)
+ _IgnoreError(snap.Remove)
- pvs_info = self.GetPVInfo(self._vg_name)
- if not pvs_info:
- raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
- self._vg_name)
- pvs_info.sort()
- pvs_info.reverse()
- free_size, pv_name = pvs_info[0]
+ vg_info = self.GetVGInfo([self._vg_name], False)
+ if not vg_info:
+ _ThrowError("Can't compute VG info for vg %s", self._vg_name)
+ free_size, _, _ = vg_info[0]
if free_size < size:
- raise errors.BlockDeviceError("Not enough free space: required %s,"
- " available %s" % (size, free_size))
+ _ThrowError("Not enough free space: required %s,"
+ " available %s", size, free_size)
- result = utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
- "-n%s" % snap_name, self.dev_path])
- if result.failed:
- raise errors.BlockDeviceError("command: %s error: %s" %
- (result.cmd, result.fail_reason))
+ _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s",
+ "-n%s" % snap_name, self.dev_path]))
+
+ return (self._vg_name, snap_name)
+
+ def _RemoveOldInfo(self):
+ """Try to remove old tags from the lv.
- return snap_name
+ """
+ result = utils.RunCmd(["lvs", "-o", "tags", "--noheadings", "--nosuffix",
+ self.dev_path])
+ _CheckResult(result)
+
+ raw_tags = result.stdout.strip()
+ if raw_tags:
+ for tag in raw_tags.split(","):
+ _CheckResult(utils.RunCmd(["lvchange", "--deltag",
+ tag.strip(), self.dev_path]))
def SetInfo(self, text):
"""Update metadata with info text.
"""
BlockDev.SetInfo(self, text)
+ self._RemoveOldInfo()
+
# Replace invalid characters
- text = re.sub('^[^A-Za-z0-9_+.]', '_', text)
- text = re.sub('[^-A-Za-z0-9_+.]', '_', text)
+ text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
+ text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
# Only up to 128 characters are allowed
text = text[:128]
- result = utils.RunCmd(["lvchange", "--addtag", text,
- self.dev_path])
- if result.failed:
- raise errors.BlockDeviceError("Command: %s error: %s" %
- (result.cmd, result.fail_reason))
+ _CheckResult(utils.RunCmd(["lvchange", "--addtag", text, self.dev_path]))
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the logical volume.
-class MDRaid1(BlockDev):
- """raid1 device implemented via md.
+ """
+ if not backingstore:
+ return
+ if self.pe_size is None or self.stripe_count is None:
+ if not self.Attach():
+ _ThrowError("Can't attach to LV during Grow()")
+ full_stripe_size = self.pe_size * self.stripe_count
+ # pe_size is in KB
+ amount *= 1024
+ rest = amount % full_stripe_size
+ if rest != 0:
+ amount += full_stripe_size - rest
+ cmd = ["lvextend", "-L", "+%dk" % amount]
+ if dryrun:
+ cmd.append("--test")
+ # we try multiple algorithms since the 'best' ones might not have
+ # space available in the right place, but later ones might (since
+ # they have less constraints); also note that only recent LVM
+ # supports 'cling'
+ for alloc_policy in "contiguous", "cling", "normal":
+ result = utils.RunCmd(cmd + ["--alloc", alloc_policy, self.dev_path])
+ if not result.failed:
+ return
+ _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
+
+
+class DRBD8Status(object):
+ """A DRBD status representation class.
+
+ Note that this doesn't support unconfigured devices (cs:Unconfigured).
"""
- def __init__(self, unique_id, children):
- super(MDRaid1, self).__init__(unique_id, children)
- self.major = 9
- self.Attach()
-
- def Attach(self):
- """Find an array which matches our config and attach to it.
-
- This tries to find a MD array which has the same UUID as our own.
-
- """
- minor = self._FindMDByUUID(self.unique_id)
- if minor is not None:
- self._SetFromMinor(minor)
+ UNCONF_RE = re.compile(r"\s*[0-9]+:\s*cs:Unconfigured$")
+ LINE_RE = re.compile(r"\s*[0-9]+:\s*cs:(\S+)\s+(?:st|ro):([^/]+)/(\S+)"
+ "\s+ds:([^/]+)/(\S+)\s+.*$")
+ SYNC_RE = re.compile(r"^.*\ssync'ed:\s*([0-9.]+)%.*"
+ # Due to a bug in drbd in the kernel, introduced in
+ # commit 4b0715f096 (still unfixed as of 2011-08-22)
+ "(?:\s|M)"
+ "finish: ([0-9]+):([0-9]+):([0-9]+)\s.*$")
+
+ CS_UNCONFIGURED = "Unconfigured"
+ CS_STANDALONE = "StandAlone"
+ CS_WFCONNECTION = "WFConnection"
+ CS_WFREPORTPARAMS = "WFReportParams"
+ CS_CONNECTED = "Connected"
+ CS_STARTINGSYNCS = "StartingSyncS"
+ CS_STARTINGSYNCT = "StartingSyncT"
+ CS_WFBITMAPS = "WFBitMapS"
+ CS_WFBITMAPT = "WFBitMapT"
+ CS_WFSYNCUUID = "WFSyncUUID"
+ CS_SYNCSOURCE = "SyncSource"
+ CS_SYNCTARGET = "SyncTarget"
+ CS_PAUSEDSYNCS = "PausedSyncS"
+ CS_PAUSEDSYNCT = "PausedSyncT"
+ CSET_SYNC = compat.UniqueFrozenset([
+ CS_WFREPORTPARAMS,
+ CS_STARTINGSYNCS,
+ CS_STARTINGSYNCT,
+ CS_WFBITMAPS,
+ CS_WFBITMAPT,
+ CS_WFSYNCUUID,
+ CS_SYNCSOURCE,
+ CS_SYNCTARGET,
+ CS_PAUSEDSYNCS,
+ CS_PAUSEDSYNCT,
+ ])
+
+ DS_DISKLESS = "Diskless"
+ DS_ATTACHING = "Attaching" # transient state
+ DS_FAILED = "Failed" # transient state, next: diskless
+ DS_NEGOTIATING = "Negotiating" # transient state
+ DS_INCONSISTENT = "Inconsistent" # while syncing or after creation
+ DS_OUTDATED = "Outdated"
+ DS_DUNKNOWN = "DUnknown" # shown for peer disk when not connected
+ DS_CONSISTENT = "Consistent"
+ DS_UPTODATE = "UpToDate" # normal state
+
+ RO_PRIMARY = "Primary"
+ RO_SECONDARY = "Secondary"
+ RO_UNKNOWN = "Unknown"
+
+ def __init__(self, procline):
+ u = self.UNCONF_RE.match(procline)
+ if u:
+ self.cstatus = self.CS_UNCONFIGURED
+ self.lrole = self.rrole = self.ldisk = self.rdisk = None
else:
- self.minor = None
- self.dev_path = None
+ m = self.LINE_RE.match(procline)
+ if not m:
+ raise errors.BlockDeviceError("Can't parse input data '%s'" % procline)
+ self.cstatus = m.group(1)
+ self.lrole = m.group(2)
+ self.rrole = m.group(3)
+ self.ldisk = m.group(4)
+ self.rdisk = m.group(5)
+
+ # end reading of data from the LINE_RE or UNCONF_RE
+
+ self.is_standalone = self.cstatus == self.CS_STANDALONE
+ self.is_wfconn = self.cstatus == self.CS_WFCONNECTION
+ self.is_connected = self.cstatus == self.CS_CONNECTED
+ self.is_primary = self.lrole == self.RO_PRIMARY
+ self.is_secondary = self.lrole == self.RO_SECONDARY
+ self.peer_primary = self.rrole == self.RO_PRIMARY
+ self.peer_secondary = self.rrole == self.RO_SECONDARY
+ self.both_primary = self.is_primary and self.peer_primary
+ self.both_secondary = self.is_secondary and self.peer_secondary
+
+ self.is_diskless = self.ldisk == self.DS_DISKLESS
+ self.is_disk_uptodate = self.ldisk == self.DS_UPTODATE
+
+ self.is_in_resync = self.cstatus in self.CSET_SYNC
+ self.is_in_use = self.cstatus != self.CS_UNCONFIGURED
+
+ m = self.SYNC_RE.match(procline)
+ if m:
+ self.sync_percent = float(m.group(1))
+ hours = int(m.group(2))
+ minutes = int(m.group(3))
+ seconds = int(m.group(4))
+ self.est_time = hours * 3600 + minutes * 60 + seconds
+ else:
+ # we have (in this if branch) no percent information, but if
+ # we're resyncing we need to 'fake' a sync percent information,
+ # as this is how cmdlib determines if it makes sense to wait for
+ # resyncing or not
+ if self.is_in_resync:
+ self.sync_percent = 0
+ else:
+ self.sync_percent = None
+ self.est_time = None
- return (minor is not None)
- @staticmethod
- def _GetUsedDevs():
- """Compute the list of in-use MD devices.
+class BaseDRBD(BlockDev): # pylint: disable=W0223
+ """Base DRBD class.
- It doesn't matter if the used device have other raid level, just
- that they are in use.
+ This class contains a few bits of common functionality between the
+ 0.7 and 8.x versions of DRBD.
- """
- mdstat = open("/proc/mdstat", "r")
- data = mdstat.readlines()
- mdstat.close()
+ """
+ _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)(?:\.\d+)?"
+ r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
+ _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
+ _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
- used_md = {}
- valid_line = re.compile("^md([0-9]+) : .*$")
- for line in data:
- match = valid_line.match(line)
- if match:
- md_no = int(match.group(1))
- used_md[md_no] = line
+ _DRBD_MAJOR = 147
+ _ST_UNCONFIGURED = "Unconfigured"
+ _ST_WFCONNECTION = "WFConnection"
+ _ST_CONNECTED = "Connected"
- return used_md
+ _STATUS_FILE = constants.DRBD_STATUS_FILE
+ _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper"
@staticmethod
- def _GetDevInfo(minor):
- """Get info about a MD device.
-
- Currently only uuid is returned.
+ def _GetProcData(filename=_STATUS_FILE):
+ """Return data from /proc/drbd.
"""
- result = utils.RunCmd(["mdadm", "-D", "/dev/md%d" % minor])
- if result.failed:
- logger.Error("Can't display md: %s" % result.fail_reason)
- return None
- retval = {}
- for line in result.stdout.splitlines():
- line = line.strip()
- kv = line.split(" : ", 1)
- if kv:
- if kv[0] == "UUID":
- retval["uuid"] = kv[1].split()[0]
- elif kv[0] == "State":
- retval["state"] = kv[1].split(", ")
- return retval
-
- @staticmethod
- def _FindUnusedMinor():
- """Compute an unused MD minor.
-
- This code assumes that there are 256 minors only.
-
- """
- used_md = MDRaid1._GetUsedDevs()
- i = 0
- while i < 256:
- if i not in used_md:
- break
- i += 1
- if i == 256:
- logger.Error("Critical: Out of md minor numbers.")
- raise errors.BlockDeviceError("Can't find a free MD minor")
- return i
-
- @classmethod
- def _FindMDByUUID(cls, uuid):
- """Find the minor of an MD array with a given UUID.
-
- """
- md_list = cls._GetUsedDevs()
- for minor in md_list:
- info = cls._GetDevInfo(minor)
- if info and info["uuid"] == uuid:
- return minor
- return None
-
- @staticmethod
- def _ZeroSuperblock(dev_path):
- """Zero the possible locations for an MD superblock.
-
- The zero-ing can't be done via ``mdadm --zero-superblock`` as that
- fails in versions 2.x with the same error code as non-writable
- device.
-
- The superblocks are located at (negative values are relative to
- the end of the block device):
- - -128k to end for version 0.90 superblock
- - -8k to -12k for version 1.0 superblock (included in the above)
- - 0k to 4k for version 1.1 superblock
- - 4k to 8k for version 1.2 superblock
-
- To cover all situations, the zero-ing will be:
- - 0k to 128k
- - -128k to end
-
- As such, the minimum device size must be 128k, otherwise we'll get
- I/O errors.
-
- Note that this function depends on the fact that one can open,
- read and write block devices normally.
-
- """
- overwrite_size = 128 * 1024
- empty_buf = '\0' * overwrite_size
- fd = open(dev_path, "r+")
try:
- fd.seek(0, 0)
- p1 = fd.tell()
- fd.write(empty_buf)
- p2 = fd.tell()
- logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
- fd.seek(-overwrite_size, 2)
- p1 = fd.tell()
- fd.write(empty_buf)
- p2 = fd.tell()
- logger.Debug("Zeroed %s from %d to %d" % (dev_path, p1, p2))
- finally:
- fd.close()
-
- @classmethod
- def Create(cls, unique_id, children, size):
- """Create a new MD raid1 array.
-
- """
- if not isinstance(children, (tuple, list)):
- raise ValueError("Invalid setup data for MDRaid1 dev: %s" %
- str(children))
- for i in children:
- if not isinstance(i, BlockDev):
- raise ValueError("Invalid member in MDRaid1 dev: %s" % type(i))
- for i in children:
- try:
- cls._ZeroSuperblock(i.dev_path)
- except EnvironmentError, err:
- logger.Error("Can't zero superblock for %s: %s" %
- (i.dev_path, str(err)))
- return None
- minor = cls._FindUnusedMinor()
- result = utils.RunCmd(["mdadm", "--create", "/dev/md%d" % minor,
- "--auto=yes", "--force", "-l1",
- "-n%d" % len(children)] +
- [dev.dev_path for dev in children])
-
- if result.failed:
- logger.Error("Can't create md: %s: %s" % (result.fail_reason,
- result.output))
- return None
- info = cls._GetDevInfo(minor)
- if not info or not "uuid" in info:
- logger.Error("Wrong information returned from mdadm -D: %s" % str(info))
- return None
- return MDRaid1(info["uuid"], children)
-
- def Remove(self):
- """Stub remove function for MD RAID 1 arrays.
-
- We don't remove the superblock right now. Mark a to do.
-
- """
- #TODO: maybe zero superblock on child devices?
- return self.Shutdown()
-
- def Rename(self, new_id):
- """Rename a device.
-
- This is not supported for md raid1 devices.
-
- """
- raise errors.ProgrammerError("Can't rename a md raid1 device")
-
- def AddChildren(self, devices):
- """Add new member(s) to the md raid1.
-
- """
- if self.minor is None and not self.Attach():
- raise errors.BlockDeviceError("Can't attach to device")
-
- args = ["mdadm", "-a", self.dev_path]
- for dev in devices:
- if dev.dev_path is None:
- raise errors.BlockDeviceError("Child '%s' is not initialised" % dev)
- dev.Open()
- args.append(dev.dev_path)
- result = utils.RunCmd(args)
- if result.failed:
- raise errors.BlockDeviceError("Failed to add new device to array: %s" %
- result.output)
- new_len = len(self._children) + len(devices)
- result = utils.RunCmd(["mdadm", "--grow", self.dev_path, "-n", new_len])
- if result.failed:
- raise errors.BlockDeviceError("Can't grow md array: %s" %
- result.output)
- self._children.extend(devices)
-
- def RemoveChildren(self, devices):
- """Remove member(s) from the md raid1.
-
- """
- if self.minor is None and not self.Attach():
- raise errors.BlockDeviceError("Can't attach to device")
- new_len = len(self._children) - len(devices)
- if new_len < 1:
- raise errors.BlockDeviceError("Can't reduce to less than one child")
- args = ["mdadm", "-f", self.dev_path]
- orig_devs = []
- for dev in devices:
- args.append(dev)
- for c in self._children:
- if c.dev_path == dev:
- orig_devs.append(c)
- break
+ data = utils.ReadFile(filename).splitlines()
+ except EnvironmentError, err:
+ if err.errno == errno.ENOENT:
+ _ThrowError("The file %s cannot be opened, check if the module"
+ " is loaded (%s)", filename, str(err))
else:
- raise errors.BlockDeviceError("Can't find device '%s' for removal" %
- dev)
- result = utils.RunCmd(args)
- if result.failed:
- raise errors.BlockDeviceError("Failed to mark device(s) as failed: %s" %
- result.output)
-
- # it seems here we need a short delay for MD to update its
- # superblocks
- time.sleep(0.5)
- args[1] = "-r"
- result = utils.RunCmd(args)
- if result.failed:
- raise errors.BlockDeviceError("Failed to remove device(s) from array:"
- " %s" % result.output)
- result = utils.RunCmd(["mdadm", "--grow", "--force", self.dev_path,
- "-n", new_len])
- if result.failed:
- raise errors.BlockDeviceError("Can't shrink md array: %s" %
- result.output)
- for dev in orig_devs:
- self._children.remove(dev)
-
- def GetStatus(self):
- """Return the status of the device.
-
- """
- self.Attach()
- if self.minor is None:
- retval = self.STATUS_UNKNOWN
- else:
- retval = self.STATUS_ONLINE
- return retval
-
- def _SetFromMinor(self, minor):
- """Set our parameters based on the given minor.
-
- This sets our minor variable and our dev_path.
-
- """
- self.minor = minor
- self.dev_path = "/dev/md%d" % minor
-
- def Assemble(self):
- """Assemble the MD device.
-
- At this point we should have:
- - list of children devices
- - uuid
-
- """
- result = super(MDRaid1, self).Assemble()
- if not result:
- return result
- md_list = self._GetUsedDevs()
- for minor in md_list:
- info = self._GetDevInfo(minor)
- if info and info["uuid"] == self.unique_id:
- self._SetFromMinor(minor)
- logger.Info("MD array %s already started" % str(self))
- return True
- free_minor = self._FindUnusedMinor()
- result = utils.RunCmd(["mdadm", "-A", "--auto=yes", "--uuid",
- self.unique_id, "/dev/md%d" % free_minor] +
- [bdev.dev_path for bdev in self._children])
- if result.failed:
- logger.Error("Can't assemble MD array: %s: %s" %
- (result.fail_reason, result.output))
- self.minor = None
- else:
- self.minor = free_minor
- return not result.failed
-
- def Shutdown(self):
- """Tear down the MD array.
-
- This does a 'mdadm --stop' so after this command, the array is no
- longer available.
-
- """
- if self.minor is None and not self.Attach():
- logger.Info("MD object not attached to a device")
- return True
-
- result = utils.RunCmd(["mdadm", "--stop", "/dev/md%d" % self.minor])
- if result.failed:
- logger.Error("Can't stop MD array: %s" % result.fail_reason)
- return False
- self.minor = None
- self.dev_path = None
- return True
-
- def SetSyncSpeed(self, kbytes):
- """Set the maximum sync speed for the MD array.
-
- """
- result = super(MDRaid1, self).SetSyncSpeed(kbytes)
- if self.minor is None:
- logger.Error("MD array not attached to a device")
- return False
- f = open("/sys/block/md%d/md/sync_speed_max" % self.minor, "w")
- try:
- f.write("%d" % kbytes)
- finally:
- f.close()
- f = open("/sys/block/md%d/md/sync_speed_min" % self.minor, "w")
- try:
- f.write("%d" % (kbytes/2))
- finally:
- f.close()
- return result
-
- def GetSyncStatus(self):
- """Returns the sync status of the device.
-
- Returns:
- (sync_percent, estimated_time, is_degraded, ldisk)
-
- If sync_percent is None, it means all is ok
- If estimated_time is None, it means we can't esimate
- the time needed, otherwise it's the time left in seconds.
-
- The ldisk parameter is always true for MD devices.
-
- """
- if self.minor is None and not self.Attach():
- raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
- dev_info = self._GetDevInfo(self.minor)
- is_clean = ("state" in dev_info and
- len(dev_info["state"]) == 1 and
- dev_info["state"][0] in ("clean", "active"))
- sys_path = "/sys/block/md%s/md/" % self.minor
- f = file(sys_path + "sync_action")
- sync_status = f.readline().strip()
- f.close()
- if sync_status == "idle":
- return None, None, not is_clean, False
- f = file(sys_path + "sync_completed")
- sync_completed = f.readline().strip().split(" / ")
- f.close()
- if len(sync_completed) != 2:
- return 0, None, not is_clean, False
- sync_done, sync_total = [float(i) for i in sync_completed]
- sync_percent = 100.0*sync_done/sync_total
- f = file(sys_path + "sync_speed")
- sync_speed_k = int(f.readline().strip())
- if sync_speed_k == 0:
- time_est = None
- else:
- time_est = (sync_total - sync_done) / 2 / sync_speed_k
- return sync_percent, time_est, not is_clean, False
-
- def Open(self, force=False):
- """Make the device ready for I/O.
-
- This is a no-op for the MDRaid1 device type, although we could use
- the 2.6.18's new array_state thing.
-
- """
- return True
-
- def Close(self):
- """Notifies that the device will no longer be used for I/O.
-
- This is a no-op for the MDRaid1 device type, but see comment for
- `Open()`.
-
- """
- return True
-
-
-class BaseDRBD(BlockDev):
- """Base DRBD class.
-
- This class contains a few bits of common functionality between the
- 0.7 and 8.x versions of DRBD.
-
- """
- _VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
- r" \(api:(\d+)/proto:(\d+)\)")
- _DRBD_MAJOR = 147
- _ST_UNCONFIGURED = "Unconfigured"
- _ST_WFCONNECTION = "WFConnection"
- _ST_CONNECTED = "Connected"
-
- @staticmethod
- def _GetProcData():
- """Return data from /proc/drbd.
-
- """
- stat = open("/proc/drbd", "r")
- try:
- data = stat.read().splitlines()
- finally:
- stat.close()
+ _ThrowError("Can't read the DRBD proc file %s: %s", filename, str(err))
if not data:
- raise errors.BlockDeviceError("Can't read any data from /proc/drbd")
+ _ThrowError("Can't read any data from %s", filename)
return data
- @staticmethod
- def _MassageProcData(data):
+ @classmethod
+ def _MassageProcData(cls, data):
"""Transform the output of _GetProdData into a nicer form.
- Returns:
- a dictionary of minor: joined lines from /proc/drbd for that minor
+ @return: a dictionary of minor: joined lines from /proc/drbd
+ for that minor
"""
- lmatch = re.compile("^ *([0-9]+):.*$")
results = {}
old_minor = old_line = None
for line in data:
- lresult = lmatch.match(line)
+ if not line: # completely empty lines, as can be returned by drbd8.0+
+ continue
+ lresult = cls._VALID_LINE_RE.match(line)
if lresult is not None:
if old_minor is not None:
results[old_minor] = old_line
return results
@classmethod
- def _GetVersion(cls):
+ def _GetVersion(cls, proc_data):
"""Return the DRBD version.
- This will return a list [k_major, k_minor, k_point, api, proto].
+ This will return a dict with keys:
+ - k_major
+ - k_minor
+ - k_point
+ - api
+ - proto
+ - proto2 (only on drbd > 8.2.X)
"""
- proc_data = cls._GetProcData()
first_line = proc_data[0].strip()
version = cls._VERSION_RE.match(first_line)
if not version:
raise errors.BlockDeviceError("Can't parse DRBD version from '%s'" %
first_line)
- return [int(val) for val in version.groups()]
+
+ values = version.groups()
+ retval = {
+ "k_major": int(values[0]),
+ "k_minor": int(values[1]),
+ "k_point": int(values[2]),
+ "api": int(values[3]),
+ "proto": int(values[4]),
+ }
+ if values[5] is not None:
+ retval["proto2"] = values[5]
+
+ return retval
+
+ @staticmethod
+ def GetUsermodeHelper(filename=_USERMODE_HELPER_FILE):
+ """Returns DRBD usermode_helper currently set.
+
+ """
+ try:
+ helper = utils.ReadFile(filename).splitlines()[0]
+ except EnvironmentError, err:
+ if err.errno == errno.ENOENT:
+ _ThrowError("The file %s cannot be opened, check if the module"
+ " is loaded (%s)", filename, str(err))
+ else:
+ _ThrowError("Can't read DRBD helper file %s: %s", filename, str(err))
+ if not helper:
+ _ThrowError("Can't read any data from %s", filename)
+ return helper
@staticmethod
def _DevPath(minor):
return "/dev/drbd%d" % minor
@classmethod
- def _GetUsedDevs(cls):
+ def GetUsedDevs(cls):
"""Compute the list of used DRBD devices.
"""
data = cls._GetProcData()
used_devs = {}
- valid_line = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
for line in data:
- match = valid_line.match(line)
+ match = cls._VALID_LINE_RE.match(line)
if not match:
continue
minor = int(match.group(1))
"""
if minor is None:
self.minor = self.dev_path = None
+ self.attached = False
else:
self.minor = minor
self.dev_path = self._DevPath(minor)
+ self.attached = True
@staticmethod
def _CheckMetaSize(meta_device):
"""Check if the given meta device looks like a valid one.
- This currently only check the size, which must be around
+ This currently only checks the size, which must be around
128MiB.
"""
result = utils.RunCmd(["blockdev", "--getsize", meta_device])
if result.failed:
- logger.Error("Failed to get device size: %s" % result.fail_reason)
- return False
+ _ThrowError("Failed to get device size: %s - %s",
+ result.fail_reason, result.output)
try:
sectors = int(result.stdout)
- except ValueError:
- logger.Error("Invalid output from blockdev: '%s'" % result.stdout)
- return False
- bytes = sectors * 512
- if bytes < 128 * 1024 * 1024: # less than 128MiB
- logger.Error("Meta device too small (%.2fMib)" % (bytes / 1024 / 1024))
- return False
- if bytes > (128 + 32) * 1024 * 1024: # account for an extra (big) PE on LVM
- logger.Error("Meta device too big (%.2fMiB)" % (bytes / 1024 / 1024))
- return False
- return True
+ except (TypeError, ValueError):
+ _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
+ num_bytes = sectors * 512
+ if num_bytes < 128 * 1024 * 1024: # less than 128MiB
+ _ThrowError("Meta device too small (%.2fMib)", (num_bytes / 1024 / 1024))
+ # the maximum *valid* size of the meta device when living on top
+ # of LVM is hard to compute: it depends on the number of stripes
+ # and the PE size; e.g. a 2-stripe, 64MB PE will result in a 128MB
+ # (normal size), but an eight-stripe 128MB PE will result in a 1GB
+ # size meta device; as such, we restrict it to 1GB (a little bit
+ # too generous, but making assumptions about PE size is hard)
+ if num_bytes > 1024 * 1024 * 1024:
+ _ThrowError("Meta device too big (%.2fMiB)", (num_bytes / 1024 / 1024))
def Rename(self, new_id):
"""Rename a device.
raise errors.ProgrammerError("Can't rename a drbd device")
-class DRBDev(BaseDRBD):
- """DRBD block device.
+class DRBD8(BaseDRBD):
+ """DRBD v8.x block device.
This implements the local host part of the DRBD device, i.e. it
doesn't do anything to the supposed peer. If you need a fully
connected DRBD pair, you need to use this class on both hosts.
- The unique_id for the drbd device is the (local_ip, local_port,
- remote_ip, remote_port) tuple, and it must have two children: the
- data device and the meta_device. The meta device is checked for
- valid size and is zeroed on create.
+ The unique_id for the drbd device is a (local_ip, local_port,
+ remote_ip, remote_port, local_minor, secret) tuple, and it must have
+ two children: the data device and the meta_device. The meta device
+ is checked for valid size and is zeroed on create.
"""
- def __init__(self, unique_id, children):
- super(DRBDev, self).__init__(unique_id, children)
- self.major = self._DRBD_MAJOR
- [kmaj, kmin, kfix, api, proto] = self._GetVersion()
- if kmaj != 0 and kmin != 7:
- raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
- " requested ganeti usage: kernel is"
- " %s.%s, ganeti wants 0.7" % (kmaj, kmin))
+ _MAX_MINORS = 255
+ _PARSE_SHOW = None
- if len(children) != 2:
+ # timeout constants
+ _NET_RECONFIG_TIMEOUT = 60
+
+ # command line options for barriers
+ _DISABLE_DISK_OPTION = "--no-disk-barrier" # -a
+ _DISABLE_DRAIN_OPTION = "--no-disk-drain" # -D
+ _DISABLE_FLUSH_OPTION = "--no-disk-flushes" # -i
+ _DISABLE_META_FLUSH_OPTION = "--no-md-flushes" # -m
+
+ def __init__(self, unique_id, children, size, params):
+ if children and children.count(None) > 0:
+ children = []
+ if len(children) not in (0, 2):
raise ValueError("Invalid configuration data %s" % str(children))
- if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 6:
raise ValueError("Invalid configuration data %s" % str(unique_id))
- self._lhost, self._lport, self._rhost, self._rport = unique_id
+ (self._lhost, self._lport,
+ self._rhost, self._rport,
+ self._aminor, self._secret) = unique_id
+ if children:
+ if not _CanReadDevice(children[1].dev_path):
+ logging.info("drbd%s: Ignoring unreadable meta device", self._aminor)
+ children = []
+ super(DRBD8, self).__init__(unique_id, children, size, params)
+ self.major = self._DRBD_MAJOR
+ version = self._GetVersion(self._GetProcData())
+ if version["k_major"] != 8:
+ _ThrowError("Mismatch in DRBD kernel version and requested ganeti"
+ " usage: kernel is %s.%s, ganeti wants 8.x",
+ version["k_major"], version["k_minor"])
+
+ if (self._lhost is not None and self._lhost == self._rhost and
+ self._lport == self._rport):
+ raise ValueError("Invalid configuration data, same local/remote %s" %
+ (unique_id,))
self.Attach()
@classmethod
+ def _InitMeta(cls, minor, dev_path):
+ """Initialize a meta device.
+
+ This will not work if the given minor is in use.
+
+ """
+ # Zero the metadata first, in order to make sure drbdmeta doesn't
+ # try to auto-detect existing filesystems or similar (see
+ # http://code.google.com/p/ganeti/issues/detail?id=182); we only
+ # care about the first 128MB of data in the device, even though it
+ # can be bigger
+ result = utils.RunCmd([constants.DD_CMD,
+ "if=/dev/zero", "of=%s" % dev_path,
+ "bs=1048576", "count=128", "oflag=direct"])
+ if result.failed:
+ _ThrowError("Can't wipe the meta device: %s", result.output)
+
+ result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
+ "v08", dev_path, "0", "create-md"])
+ if result.failed:
+ _ThrowError("Can't initialize meta device: %s", result.output)
+
+ @classmethod
def _FindUnusedMinor(cls):
"""Find an unused DRBD device.
+ This is specific to 8.x as the minors are allocated dynamically,
+ so non-existing numbers up to a max minor count are actually free.
+
"""
data = cls._GetProcData()
- valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
+ highest = None
for line in data:
- match = valid_line.match(line)
+ match = cls._UNUSED_LINE_RE.match(line)
if match:
return int(match.group(1))
- logger.Error("Error: no free drbd minors!")
- raise errors.BlockDeviceError("Can't find a free DRBD minor")
+ match = cls._VALID_LINE_RE.match(line)
+ if match:
+ minor = int(match.group(1))
+ highest = max(highest, minor)
+ if highest is None: # there are no minors in use at all
+ return 0
+ if highest >= cls._MAX_MINORS:
+ logging.error("Error: no free drbd minors!")
+ raise errors.BlockDeviceError("Can't find a free DRBD minor")
+ return highest + 1
@classmethod
- def _GetDevInfo(cls, minor):
- """Get details about a given DRBD minor.
+ def _GetShowParser(cls):
+ """Return a parser for `drbd show` output.
- This return, if available, the local backing device in (major,
- minor) formant and the local and remote (ip, port) information.
+ This will either create or return an already-created parser for the
+ output of the command `drbd show`.
"""
- data = {}
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
- if result.failed:
- logger.Error("Can't display the drbd config: %s" % result.fail_reason)
- return data
- out = result.stdout
- if out == "Not configured\n":
- return data
- for line in out.splitlines():
- if "local_dev" not in data:
- match = re.match("^Lower device: ([0-9]+):([0-9]+) .*$", line)
- if match:
- data["local_dev"] = (int(match.group(1)), int(match.group(2)))
- continue
- if "meta_dev" not in data:
- match = re.match("^Meta device: (([0-9]+):([0-9]+)|internal).*$", line)
- if match:
- if match.group(2) is not None and match.group(3) is not None:
- # matched on the major/minor
- data["meta_dev"] = (int(match.group(2)), int(match.group(3)))
- else:
- # matched on the "internal" string
- data["meta_dev"] = match.group(1)
- # in this case, no meta_index is in the output
- data["meta_index"] = -1
- continue
- if "meta_index" not in data:
- match = re.match("^Meta index: ([0-9]+).*$", line)
- if match:
- data["meta_index"] = int(match.group(1))
- continue
- if "local_addr" not in data:
- match = re.match("^Local address: ([0-9.]+):([0-9]+)$", line)
- if match:
- data["local_addr"] = (match.group(1), int(match.group(2)))
- continue
- if "remote_addr" not in data:
- match = re.match("^Remote address: ([0-9.]+):([0-9]+)$", line)
- if match:
- data["remote_addr"] = (match.group(1), int(match.group(2)))
- continue
- return data
+ if cls._PARSE_SHOW is not None:
+ return cls._PARSE_SHOW
- def _MatchesLocal(self, info):
- """Test if our local config matches with an existing device.
+ # pyparsing setup
+ lbrace = pyp.Literal("{").suppress()
+ rbrace = pyp.Literal("}").suppress()
+ lbracket = pyp.Literal("[").suppress()
+ rbracket = pyp.Literal("]").suppress()
+ semi = pyp.Literal(";").suppress()
+ colon = pyp.Literal(":").suppress()
+ # this also converts the value to an int
+ number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
- The parameter should be as returned from `_GetDevInfo()`. This
- method tests if our local backing device is the same as the one in
- the info parameter, in effect testing if we look like the given
- device.
+ comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine)
+ defa = pyp.Literal("_is_default").suppress()
+ dbl_quote = pyp.Literal('"').suppress()
- """
- if not ("local_dev" in info and "meta_dev" in info and
- "meta_index" in info):
- return False
+ keyword = pyp.Word(pyp.alphanums + "-")
- backend = self._children[0]
- if backend is not None:
- retval = (info["local_dev"] == (backend.major, backend.minor))
- else:
- retval = (info["local_dev"] == (0, 0))
- meta = self._children[1]
- if meta is not None:
- retval = retval and (info["meta_dev"] == (meta.major, meta.minor))
- retval = retval and (info["meta_index"] == 0)
- else:
- retval = retval and (info["meta_dev"] == "internal" and
- info["meta_index"] == -1)
- return retval
+ # value types
+ value = pyp.Word(pyp.alphanums + "_-/.:")
+ quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
+ ipv4_addr = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
+ pyp.Word(pyp.nums + ".") + colon + number)
+ ipv6_addr = (pyp.Optional(pyp.Literal("ipv6")).suppress() +
+ pyp.Optional(lbracket) + pyp.Word(pyp.hexnums + ":") +
+ pyp.Optional(rbracket) + colon + number)
+ # meta device, extended syntax
+ meta_value = ((value ^ quoted) + lbracket + number + rbracket)
+ # device name, extended syntax
+ device_value = pyp.Literal("minor").suppress() + number
- def _MatchesNet(self, info):
- """Test if our network config matches with an existing device.
+ # a statement
+ stmt = (~rbrace + keyword + ~lbrace +
+ pyp.Optional(ipv4_addr ^ ipv6_addr ^ value ^ quoted ^ meta_value ^
+ device_value) +
+ pyp.Optional(defa) + semi +
+ pyp.Optional(pyp.restOfLine).suppress())
- The parameter should be as returned from `_GetDevInfo()`. This
- method tests if our network configuration is the same as the one
- in the info parameter, in effect testing if we look like the given
- device.
+ # an entire section
+ section_name = pyp.Word(pyp.alphas + "_")
+ section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
- """
- if (((self._lhost is None and not ("local_addr" in info)) and
- (self._rhost is None and not ("remote_addr" in info)))):
- return True
+ bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
+ bnf.ignore(comment)
- if self._lhost is None:
+ cls._PARSE_SHOW = bnf
+
+ return bnf
+
+ @classmethod
+ def _GetShowData(cls, minor):
+ """Return the `drbdsetup show` data for a minor.
+
+ """
+ result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
+ if result.failed:
+ logging.error("Can't display the drbd config: %s - %s",
+ result.fail_reason, result.output)
+ return None
+ return result.stdout
+
+ @classmethod
+ def _GetDevInfo(cls, out):
+ """Parse details about a given DRBD minor.
+
+ This return, if available, the local backing device (as a path)
+ and the local and remote (ip, port) information from a string
+ containing the output of the `drbdsetup show` command as returned
+ by _GetShowData.
+
+ """
+ data = {}
+ if not out:
+ return data
+
+ bnf = cls._GetShowParser()
+ # run pyparse
+
+ try:
+ results = bnf.parseString(out)
+ except pyp.ParseException, err:
+ _ThrowError("Can't parse drbdsetup show output: %s", str(err))
+
+ # and massage the results into our desired format
+ for section in results:
+ sname = section[0]
+ if sname == "_this_host":
+ for lst in section[1:]:
+ if lst[0] == "disk":
+ data["local_dev"] = lst[1]
+ elif lst[0] == "meta-disk":
+ data["meta_dev"] = lst[1]
+ data["meta_index"] = lst[2]
+ elif lst[0] == "address":
+ data["local_addr"] = tuple(lst[1:])
+ elif sname == "_remote_host":
+ for lst in section[1:]:
+ if lst[0] == "address":
+ data["remote_addr"] = tuple(lst[1:])
+ return data
+
+ def _MatchesLocal(self, info):
+ """Test if our local config matches with an existing device.
+
+ The parameter should be as returned from `_GetDevInfo()`. This
+ method tests if our local backing device is the same as the one in
+ the info parameter, in effect testing if we look like the given
+ device.
+
+ """
+ if self._children:
+ backend, meta = self._children
+ else:
+ backend = meta = None
+
+ if backend is not None:
+ retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
+ else:
+ retval = ("local_dev" not in info)
+
+ if meta is not None:
+ retval = retval and ("meta_dev" in info and
+ info["meta_dev"] == meta.dev_path)
+ retval = retval and ("meta_index" in info and
+ info["meta_index"] == 0)
+ else:
+ retval = retval and ("meta_dev" not in info and
+ "meta_index" not in info)
+ return retval
+
+ def _MatchesNet(self, info):
+ """Test if our network config matches with an existing device.
+
+ The parameter should be as returned from `_GetDevInfo()`. This
+ method tests if our network configuration is the same as the one
+ in the info parameter, in effect testing if we look like the given
+ device.
+
+ """
+ if (((self._lhost is None and not ("local_addr" in info)) and
+ (self._rhost is None and not ("remote_addr" in info)))):
+ return True
+
+ if self._lhost is None:
return False
if not ("local_addr" in info and
info["remote_addr"] == (self._rhost, self._rport))
return retval
- @classmethod
- def _AssembleLocal(cls, minor, backend, meta):
+ def _AssembleLocal(self, minor, backend, meta, size):
"""Configure the local part of a DRBD device.
- This is the first thing that must be done on an unconfigured DRBD
- device. And it must be done only once.
-
"""
- if not cls._CheckMetaSize(meta):
- return False
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
- backend, meta, "0", "-e", "detach"])
- if result.failed:
- logger.Error("Can't attach local disk: %s" % result.output)
- return not result.failed
+ args = ["drbdsetup", self._DevPath(minor), "disk",
+ backend, meta, "0",
+ "-e", "detach",
+ "--create-device"]
+ if size:
+ args.extend(["-d", "%sm" % size])
- @classmethod
- def _ShutdownLocal(cls, minor):
- """Detach from the local device.
+ version = self._GetVersion(self._GetProcData())
+ vmaj = version["k_major"]
+ vmin = version["k_minor"]
+ vrel = version["k_point"]
- I/Os will continue to be served from the remote device. If we
- don't have a remote device, this operation will fail.
+ barrier_args = \
+ self._ComputeDiskBarrierArgs(vmaj, vmin, vrel,
+ self.params[constants.LDP_BARRIERS],
+ self.params[constants.LDP_NO_META_FLUSH])
+ args.extend(barrier_args)
- """
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
+ if self.params[constants.LDP_DISK_CUSTOM]:
+ args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM]))
+
+ result = utils.RunCmd(args)
if result.failed:
- logger.Error("Can't detach local device: %s" % result.output)
- return not result.failed
+ _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
- @staticmethod
- def _ShutdownAll(minor):
- """Deactivate the device.
+ @classmethod
+ def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers,
+ disable_meta_flush):
+ """Compute the DRBD command line parameters for disk barriers
- This will, of course, fail if the device is in use.
+ Returns a list of the disk barrier parameters as requested via the
+ disabled_barriers and disable_meta_flush arguments, and according to the
+ supported ones in the DRBD version vmaj.vmin.vrel
+
+ If the desired option is unsupported, raises errors.BlockDeviceError.
"""
- result = utils.RunCmd(["drbdsetup", DRBDev._DevPath(minor), "down"])
- if result.failed:
- logger.Error("Can't shutdown drbd device: %s" % result.output)
- return not result.failed
+ disabled_barriers_set = frozenset(disabled_barriers)
+ if not disabled_barriers_set in constants.DRBD_VALID_BARRIER_OPT:
+ raise errors.BlockDeviceError("%s is not a valid option set for DRBD"
+ " barriers" % disabled_barriers)
- @classmethod
- def _AssembleNet(cls, minor, net_info, protocol):
- """Configure the network part of the device.
+ args = []
+
+ # The following code assumes DRBD 8.x, with x < 4 and x != 1 (DRBD 8.1.x
+ # does not exist)
+ if not vmaj == 8 and vmin in (0, 2, 3):
+ raise errors.BlockDeviceError("Unsupported DRBD version: %d.%d.%d" %
+ (vmaj, vmin, vrel))
- This operation can be, in theory, done multiple times, but there
- have been cases (in lab testing) in which the network part of the
- device had become stuck and couldn't be shut down because activity
- from the new peer (also stuck) triggered a timer re-init and
- needed remote peer interface shutdown in order to clear. So please
- don't change online the net config.
+ def _AppendOrRaise(option, min_version):
+ """Helper for DRBD options"""
+ if min_version is not None and vrel >= min_version:
+ args.append(option)
+ else:
+ raise errors.BlockDeviceError("Could not use the option %s as the"
+ " DRBD version %d.%d.%d does not support"
+ " it." % (option, vmaj, vmin, vrel))
+
+ # the minimum version for each feature is encoded via pairs of (minor
+ # version -> x) where x is version in which support for the option was
+ # introduced.
+ meta_flush_supported = disk_flush_supported = {
+ 0: 12,
+ 2: 7,
+ 3: 0,
+ }
+
+ disk_drain_supported = {
+ 2: 7,
+ 3: 0,
+ }
+
+ disk_barriers_supported = {
+ 3: 0,
+ }
+
+ # meta flushes
+ if disable_meta_flush:
+ _AppendOrRaise(cls._DISABLE_META_FLUSH_OPTION,
+ meta_flush_supported.get(vmin, None))
+
+ # disk flushes
+ if constants.DRBD_B_DISK_FLUSH in disabled_barriers_set:
+ _AppendOrRaise(cls._DISABLE_FLUSH_OPTION,
+ disk_flush_supported.get(vmin, None))
+
+ # disk drain
+ if constants.DRBD_B_DISK_DRAIN in disabled_barriers_set:
+ _AppendOrRaise(cls._DISABLE_DRAIN_OPTION,
+ disk_drain_supported.get(vmin, None))
+
+ # disk barriers
+ if constants.DRBD_B_DISK_BARRIERS in disabled_barriers_set:
+ _AppendOrRaise(cls._DISABLE_DISK_OPTION,
+ disk_barriers_supported.get(vmin, None))
+
+ return args
+
+ def _AssembleNet(self, minor, net_info, protocol,
+ dual_pri=False, hmac=None, secret=None):
+ """Configure the network part of the device.
"""
lhost, lport, rhost, rport = net_info
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
- "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
- protocol])
+ if None in net_info:
+ # we don't want network connection and actually want to make
+ # sure its shutdown
+ self._ShutdownNet(minor)
+ return
+
+ # Workaround for a race condition. When DRBD is doing its dance to
+ # establish a connection with its peer, it also sends the
+ # synchronization speed over the wire. In some cases setting the
+ # sync speed only after setting up both sides can race with DRBD
+ # connecting, hence we set it here before telling DRBD anything
+ # about its peer.
+ sync_errors = self._SetMinorSyncParams(minor, self.params)
+ if sync_errors:
+ _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+ (minor, utils.CommaJoin(sync_errors)))
+
+ if netutils.IP6Address.IsValid(lhost):
+ if not netutils.IP6Address.IsValid(rhost):
+ _ThrowError("drbd%d: can't connect ip %s to ip %s" %
+ (minor, lhost, rhost))
+ family = "ipv6"
+ elif netutils.IP4Address.IsValid(lhost):
+ if not netutils.IP4Address.IsValid(rhost):
+ _ThrowError("drbd%d: can't connect ip %s to ip %s" %
+ (minor, lhost, rhost))
+ family = "ipv4"
+ else:
+ _ThrowError("drbd%d: Invalid ip %s" % (minor, lhost))
+
+ args = ["drbdsetup", self._DevPath(minor), "net",
+ "%s:%s:%s" % (family, lhost, lport),
+ "%s:%s:%s" % (family, rhost, rport), protocol,
+ "-A", "discard-zero-changes",
+ "-B", "consensus",
+ "--create-device",
+ ]
+ if dual_pri:
+ args.append("-m")
+ if hmac and secret:
+ args.extend(["-a", hmac, "-x", secret])
+
+ if self.params[constants.LDP_NET_CUSTOM]:
+ args.extend(shlex.split(self.params[constants.LDP_NET_CUSTOM]))
+
+ result = utils.RunCmd(args)
if result.failed:
- logger.Error("Can't setup network for dbrd device: %s" %
- result.fail_reason)
- return False
+ _ThrowError("drbd%d: can't setup network: %s - %s",
+ minor, result.fail_reason, result.output)
- timeout = time.time() + 10
- ok = False
- while time.time() < timeout:
- info = cls._GetDevInfo(minor)
+ def _CheckNetworkConfig():
+ info = self._GetDevInfo(self._GetShowData(minor))
if not "local_addr" in info or not "remote_addr" in info:
- time.sleep(1)
- continue
+ raise utils.RetryAgain()
+
if (info["local_addr"] != (lhost, lport) or
info["remote_addr"] != (rhost, rport)):
- time.sleep(1)
- continue
- ok = True
- break
- if not ok:
- logger.Error("Timeout while configuring network")
- return False
- return True
+ raise utils.RetryAgain()
- @classmethod
- def _ShutdownNet(cls, minor):
- """Disconnect from the remote peer.
+ try:
+ utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
+ except utils.RetryTimeout:
+ _ThrowError("drbd%d: timeout while configuring network", minor)
- This fails if we don't have a local device.
+ def AddChildren(self, devices):
+ """Add a disk to the DRBD device.
"""
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
+ if self.minor is None:
+ _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
+ self._aminor)
+ if len(devices) != 2:
+ _ThrowError("drbd%d: need two devices for AddChildren", self.minor)
+ info = self._GetDevInfo(self._GetShowData(self.minor))
+ if "local_dev" in info:
+ _ThrowError("drbd%d: already attached to a local disk", self.minor)
+ backend, meta = devices
+ if backend.dev_path is None or meta.dev_path is None:
+ _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
+ backend.Open()
+ meta.Open()
+ self._CheckMetaSize(meta.dev_path)
+ self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
+
+ self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
+ self._children = devices
+
+ def RemoveChildren(self, devices):
+ """Detach the drbd device from local storage.
+
+ """
+ if self.minor is None:
+ _ThrowError("drbd%d: can't attach to drbd8 during RemoveChildren",
+ self._aminor)
+ # early return if we don't actually have backing storage
+ info = self._GetDevInfo(self._GetShowData(self.minor))
+ if "local_dev" not in info:
+ return
+ if len(self._children) != 2:
+ _ThrowError("drbd%d: we don't have two children: %s", self.minor,
+ self._children)
+ if self._children.count(None) == 2: # we don't actually have children :)
+ logging.warning("drbd%d: requested detach while detached", self.minor)
+ return
+ if len(devices) != 2:
+ _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
+ for child, dev in zip(self._children, devices):
+ if dev != child.dev_path:
+ _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
+ " RemoveChildren", self.minor, dev, child.dev_path)
+
+ self._ShutdownLocal(self.minor)
+ self._children = []
+
+ @classmethod
+ def _SetMinorSyncParams(cls, minor, params):
+ """Set the parameters of the DRBD syncer.
+
+ This is the low-level implementation.
+
+ @type minor: int
+ @param minor: the drbd minor whose settings we change
+ @type params: dict
+ @param params: LD level disk parameters related to the synchronization
+ @rtype: list
+ @return: a list of error messages
+
+ """
+
+ args = ["drbdsetup", cls._DevPath(minor), "syncer"]
+ if params[constants.LDP_DYNAMIC_RESYNC]:
+ version = cls._GetVersion(cls._GetProcData())
+ vmin = version["k_minor"]
+ vrel = version["k_point"]
+
+ # By definition we are using 8.x, so just check the rest of the version
+ # number
+ if vmin != 3 or vrel < 9:
+ msg = ("The current DRBD version (8.%d.%d) does not support the "
+ "dynamic resync speed controller" % (vmin, vrel))
+ logging.error(msg)
+ return [msg]
+
+ if params[constants.LDP_PLAN_AHEAD] == 0:
+ msg = ("A value of 0 for c-plan-ahead disables the dynamic sync speed"
+ " controller at DRBD level. If you want to disable it, please"
+ " set the dynamic-resync disk parameter to False.")
+ logging.error(msg)
+ return [msg]
+
+ # add the c-* parameters to args
+ args.extend(["--c-plan-ahead", params[constants.LDP_PLAN_AHEAD],
+ "--c-fill-target", params[constants.LDP_FILL_TARGET],
+ "--c-delay-target", params[constants.LDP_DELAY_TARGET],
+ "--c-max-rate", params[constants.LDP_MAX_RATE],
+ "--c-min-rate", params[constants.LDP_MIN_RATE],
+ ])
+
+ else:
+ args.extend(["-r", "%d" % params[constants.LDP_RESYNC_RATE]])
+
+ args.append("--create-device")
+ result = utils.RunCmd(args)
if result.failed:
- logger.Error("Can't shutdown network: %s" % result.output)
- return not result.failed
+ msg = ("Can't change syncer rate: %s - %s" %
+ (result.fail_reason, result.output))
+ logging.error(msg)
+ return [msg]
- def Assemble(self):
- """Assemble the drbd.
+ return []
- Method:
- - if we have a local backing device, we bind to it by:
- - checking the list of used drbd devices
- - check if the local minor use of any of them is our own device
- - if yes, abort?
- - if not, bind
- - if we have a local/remote net info:
- - redo the local backing device step for the remote device
- - check if any drbd device is using the local port,
- if yes abort
- - check if any remote drbd device is using the remote
- port, if yes abort (for now)
- - bind our net port
- - bind the remote net port
+ def SetSyncParams(self, params):
+ """Set the synchronization parameters of the DRBD syncer.
+
+ @type params: dict
+ @param params: LD level disk parameters related to the synchronization
+ @rtype: list
+ @return: a list of error messages, emitted both by the current node and by
+ children. An empty list means no errors
"""
- self.Attach()
- if self.minor is not None:
- logger.Info("Already assembled")
- return True
+ if self.minor is None:
+ err = "Not attached during SetSyncParams"
+ logging.info(err)
+ return [err]
- result = super(DRBDev, self).Assemble()
- if not result:
- return result
-
- minor = self._FindUnusedMinor()
- need_localdev_teardown = False
- if self._children[0]:
- result = self._AssembleLocal(minor, self._children[0].dev_path,
- self._children[1].dev_path)
- if not result:
- return False
- need_localdev_teardown = True
- if self._lhost and self._lport and self._rhost and self._rport:
- result = self._AssembleNet(minor,
- (self._lhost, self._lport,
- self._rhost, self._rport),
- "C")
- if not result:
- if need_localdev_teardown:
- # we will ignore failures from this
- logger.Error("net setup failed, tearing down local device")
- self._ShutdownAll(minor)
- return False
- self._SetFromMinor(minor)
- return True
+ children_result = super(DRBD8, self).SetSyncParams(params)
+ children_result.extend(self._SetMinorSyncParams(self.minor, params))
+ return children_result
- def Shutdown(self):
- """Shutdown the DRBD device.
+ def PauseResumeSync(self, pause):
+ """Pauses or resumes the sync of a DRBD device.
+
+ @param pause: Wether to pause or resume
+ @return: the success of the operation
"""
- if self.minor is None and not self.Attach():
- logger.Info("DRBD device not attached to a device during Shutdown")
- return True
- if not self._ShutdownAll(self.minor):
+ if self.minor is None:
+ logging.info("Not attached during PauseSync")
return False
- self.minor = None
- self.dev_path = None
- return True
- def Attach(self):
- """Find a DRBD device which matches our config and attach to it.
+ children_result = super(DRBD8, self).PauseResumeSync(pause)
- In case of partially attached (local device matches but no network
- setup), we perform the network attach. If successful, we re-test
- the attach if can return success.
+ if pause:
+ cmd = "pause-sync"
+ else:
+ cmd = "resume-sync"
+
+ result = utils.RunCmd(["drbdsetup", self.dev_path, cmd])
+ if result.failed:
+ logging.error("Can't %s: %s - %s", cmd,
+ result.fail_reason, result.output)
+ return not result.failed and children_result
+
+ def GetProcStatus(self):
+ """Return device data from /proc.
"""
- for minor in self._GetUsedDevs():
- info = self._GetDevInfo(minor)
- match_l = self._MatchesLocal(info)
- match_r = self._MatchesNet(info)
- if match_l and match_r:
- break
- if match_l and not match_r and "local_addr" not in info:
- res_r = self._AssembleNet(minor,
- (self._lhost, self._lport,
- self._rhost, self._rport),
- "C")
- if res_r and self._MatchesNet(self._GetDevInfo(minor)):
- break
+ if self.minor is None:
+ _ThrowError("drbd%d: GetStats() called while not attached", self._aminor)
+ proc_info = self._MassageProcData(self._GetProcData())
+ if self.minor not in proc_info:
+ _ThrowError("drbd%d: can't find myself in /proc", self.minor)
+ return DRBD8Status(proc_info[self.minor])
+
+ def GetSyncStatus(self):
+ """Returns the sync status of the device.
+
+
+ If sync_percent is None, it means all is ok
+ If estimated_time is None, it means we can't estimate
+ the time needed, otherwise it's the time left in seconds.
+
+
+ We set the is_degraded parameter to True on two conditions:
+ network not connected or local disk missing.
+
+ We compute the ldisk parameter based on whether we have a local
+ disk or not.
+
+ @rtype: objects.BlockDevStatus
+
+ """
+ if self.minor is None and not self.Attach():
+ _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
+
+ stats = self.GetProcStatus()
+ is_degraded = not stats.is_connected or not stats.is_disk_uptodate
+
+ if stats.is_disk_uptodate:
+ ldisk_status = constants.LDS_OKAY
+ elif stats.is_diskless:
+ ldisk_status = constants.LDS_FAULTY
else:
- minor = None
+ ldisk_status = constants.LDS_UNKNOWN
- self._SetFromMinor(minor)
- return minor is not None
+ return objects.BlockDevStatus(dev_path=self.dev_path,
+ major=self.major,
+ minor=self.minor,
+ sync_percent=stats.sync_percent,
+ estimated_time=stats.est_time,
+ is_degraded=is_degraded,
+ ldisk_status=ldisk_status)
def Open(self, force=False):
"""Make the local state primary.
- If the 'force' parameter is given, the '--do-what-I-say' parameter
- is given. Since this is a pottentialy dangerous operation, the
+ If the 'force' parameter is given, the '-o' option is passed to
+ drbdsetup. Since this is a potentially dangerous operation, the
force flag should be only given after creation, when it actually
- has to be given.
+ is mandatory.
"""
if self.minor is None and not self.Attach():
- logger.Error("DRBD cannot attach to a device during open")
+ logging.error("DRBD cannot attach to a device during open")
return False
cmd = ["drbdsetup", self.dev_path, "primary"]
if force:
- cmd.append("--do-what-I-say")
+ cmd.append("-o")
result = utils.RunCmd(cmd)
if result.failed:
- logger.Error("Can't make drbd device primary: %s" % result.output)
- return False
- return True
+ _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor,
+ result.output)
def Close(self):
"""Make the local state secondary.
"""
if self.minor is None and not self.Attach():
- logger.Info("Instance not attached to a device")
- raise errors.BlockDeviceError("Can't find device")
+ _ThrowError("drbd%d: can't Attach() in Close()", self._aminor)
result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
if result.failed:
- logger.Error("Can't switch drbd device to secondary: %s" % result.output)
- raise errors.BlockDeviceError("Can't switch drbd device to secondary")
+ _ThrowError("drbd%d: can't switch drbd device to secondary: %s",
+ self.minor, result.output)
+
+ def DisconnectNet(self):
+ """Removes network configuration.
- def SetSyncSpeed(self, kbytes):
- """Set the speed of the DRBD syncer.
+ This method shutdowns the network side of the device.
+
+ The method will wait up to a hardcoded timeout for the device to
+ go into standalone after the 'disconnect' command before
+ re-configuring it, as sometimes it takes a while for the
+ disconnect to actually propagate and thus we might issue a 'net'
+ command while the device is still connected. If the device will
+ still be attached to the network and we time out, we raise an
+ exception.
"""
- children_result = super(DRBDev, self).SetSyncSpeed(kbytes)
if self.minor is None:
- logger.Info("Instance not attached to a device")
- return False
- result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
- kbytes])
- if result.failed:
- logger.Error("Can't change syncer rate: %s " % result.fail_reason)
- return not result.failed and children_result
+ _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
- def GetSyncStatus(self):
- """Returns the sync status of the device.
+ if None in (self._lhost, self._lport, self._rhost, self._rport):
+ _ThrowError("drbd%d: DRBD disk missing network info in"
+ " DisconnectNet()", self.minor)
- Returns:
- (sync_percent, estimated_time, is_degraded, ldisk)
+ class _DisconnectStatus:
+ def __init__(self, ever_disconnected):
+ self.ever_disconnected = ever_disconnected
- If sync_percent is None, it means all is ok
- If estimated_time is None, it means we can't esimate
- the time needed, otherwise it's the time left in seconds.
+ dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
- The ldisk parameter will be returned as True, since the DRBD7
- devices have not been converted.
+ def _WaitForDisconnect():
+ if self.GetProcStatus().is_standalone:
+ return
- """
- if self.minor is None and not self.Attach():
- raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
- proc_info = self._MassageProcData(self._GetProcData())
- if self.minor not in proc_info:
- raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
- self.minor)
- line = proc_info[self.minor]
- match = re.match("^.*sync'ed: *([0-9.]+)%.*"
- " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
- if match:
- sync_percent = float(match.group(1))
- hours = int(match.group(2))
- minutes = int(match.group(3))
- seconds = int(match.group(4))
- est_time = hours * 3600 + minutes * 60 + seconds
- else:
- sync_percent = None
- est_time = None
- match = re.match("^ *[0-9]+: cs:([^ ]+).*$", line)
- if not match:
- raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
- self.minor)
- client_state = match.group(1)
- is_degraded = client_state != "Connected"
- return sync_percent, est_time, is_degraded, False
+ # retry the disconnect, it seems possible that due to a well-time
+ # disconnect on the peer, my disconnect command might be ignored and
+ # forgotten
+ dstatus.ever_disconnected = \
+ _IgnoreError(self._ShutdownNet, self.minor) or dstatus.ever_disconnected
- def GetStatus(self):
- """Compute the status of the DRBD device
+ raise utils.RetryAgain()
- Note that DRBD devices don't have the STATUS_EXISTING state.
+ # Keep start time
+ start_time = time.time()
- """
- if self.minor is None and not self.Attach():
- return self.STATUS_UNKNOWN
+ try:
+ # Start delay at 100 milliseconds and grow up to 2 seconds
+ utils.Retry(_WaitForDisconnect, (0.1, 1.5, 2.0),
+ self._NET_RECONFIG_TIMEOUT)
+ except utils.RetryTimeout:
+ if dstatus.ever_disconnected:
+ msg = ("drbd%d: device did not react to the"
+ " 'disconnect' command in a timely manner")
+ else:
+ msg = "drbd%d: can't shutdown network, even after multiple retries"
- data = self._GetProcData()
- match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
- self.minor)
- for line in data:
- mresult = match.match(line)
- if mresult:
- break
+ _ThrowError(msg, self.minor)
+
+ reconfig_time = time.time() - start_time
+ if reconfig_time > (self._NET_RECONFIG_TIMEOUT * 0.25):
+ logging.info("drbd%d: DisconnectNet: detach took %.3f seconds",
+ self.minor, reconfig_time)
+
+ def AttachNet(self, multimaster):
+ """Reconnects the network.
+
+ This method connects the network side of the device with a
+ specified multi-master flag. The device needs to be 'Standalone'
+ but have valid network configuration data.
+
+ Args:
+ - multimaster: init the network in dual-primary mode
+
+ """
+ if self.minor is None:
+ _ThrowError("drbd%d: device not attached in AttachNet", self._aminor)
+
+ if None in (self._lhost, self._lport, self._rhost, self._rport):
+ _ThrowError("drbd%d: missing network info in AttachNet()", self.minor)
+
+ status = self.GetProcStatus()
+
+ if not status.is_standalone:
+ _ThrowError("drbd%d: device is not standalone in AttachNet", self.minor)
+
+ self._AssembleNet(self.minor,
+ (self._lhost, self._lport, self._rhost, self._rport),
+ constants.DRBD_NET_PROTOCOL, dual_pri=multimaster,
+ hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
+
+ def Attach(self):
+ """Check if our minor is configured.
+
+ This doesn't do any device configurations - it only checks if the
+ minor is in a state different from Unconfigured.
+
+ Note that this function will not change the state of the system in
+ any way (except in case of side-effects caused by reading from
+ /proc).
+
+ """
+ used_devs = self.GetUsedDevs()
+ if self._aminor in used_devs:
+ minor = self._aminor
else:
- logger.Error("Can't find myself!")
- return self.STATUS_UNKNOWN
+ minor = None
+
+ self._SetFromMinor(minor)
+ return minor is not None
+
+ def Assemble(self):
+ """Assemble the drbd.
+
+ Method:
+ - if we have a configured device, we try to ensure that it matches
+ our config
+ - if not, we create it from zero
+ - anyway, set the device parameters
+
+ """
+ super(DRBD8, self).Assemble()
- state = mresult.group(2)
- if state == "Primary":
- result = self.STATUS_ONLINE
+ self.Attach()
+ if self.minor is None:
+ # local device completely unconfigured
+ self._FastAssemble()
else:
- result = self.STATUS_STANDBY
+ # we have to recheck the local and network status and try to fix
+ # the device
+ self._SlowAssemble()
- return result
+ sync_errors = self.SetSyncParams(self.params)
+ if sync_errors:
+ _ThrowError("drbd%d: can't set the synchronization parameters: %s" %
+ (self.minor, utils.CommaJoin(sync_errors)))
- @staticmethod
- def _ZeroDevice(device):
- """Zero a device.
+ def _SlowAssemble(self):
+ """Assembles the DRBD device from a (partially) configured device.
- This writes until we get ENOSPC.
+ In case of partially attached (local device matches but no network
+ setup), we perform the network attach. If successful, we re-test
+ the attach if can return success.
"""
- f = open(device, "w")
- buf = "\0" * 1048576
- try:
- while True:
- f.write(buf)
- except IOError, err:
- if err.errno != errno.ENOSPC:
- raise
+ # TODO: Rewrite to not use a for loop just because there is 'break'
+ # pylint: disable=W0631
+ net_data = (self._lhost, self._lport, self._rhost, self._rport)
+ for minor in (self._aminor,):
+ info = self._GetDevInfo(self._GetShowData(minor))
+ match_l = self._MatchesLocal(info)
+ match_r = self._MatchesNet(info)
+
+ if match_l and match_r:
+ # everything matches
+ break
+
+ if match_l and not match_r and "local_addr" not in info:
+ # disk matches, but not attached to network, attach and recheck
+ self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
+ hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
+ if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
+ break
+ else:
+ _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
+ " show' disagrees", minor)
+
+ if match_r and "local_dev" not in info:
+ # no local disk, but network attached and it matches
+ self._AssembleLocal(minor, self._children[0].dev_path,
+ self._children[1].dev_path, self.size)
+ if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
+ break
+ else:
+ _ThrowError("drbd%d: disk attach successful, but 'drbdsetup"
+ " show' disagrees", minor)
+
+ # this case must be considered only if we actually have local
+ # storage, i.e. not in diskless mode, because all diskless
+ # devices are equal from the point of view of local
+ # configuration
+ if (match_l and "local_dev" in info and
+ not match_r and "local_addr" in info):
+ # strange case - the device network part points to somewhere
+ # else, even though its local storage is ours; as we own the
+ # drbd space, we try to disconnect from the remote peer and
+ # reconnect to our correct one
+ try:
+ self._ShutdownNet(minor)
+ except errors.BlockDeviceError, err:
+ _ThrowError("drbd%d: device has correct local storage, wrong"
+ " remote peer and is unable to disconnect in order"
+ " to attach to the correct peer: %s", minor, str(err))
+ # note: _AssembleNet also handles the case when we don't want
+ # local storage (i.e. one or more of the _[lr](host|port) is
+ # None)
+ self._AssembleNet(minor, net_data, constants.DRBD_NET_PROTOCOL,
+ hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
+ if self._MatchesNet(self._GetDevInfo(self._GetShowData(minor))):
+ break
+ else:
+ _ThrowError("drbd%d: network attach successful, but 'drbdsetup"
+ " show' disagrees", minor)
+
+ else:
+ minor = None
+
+ self._SetFromMinor(minor)
+ if minor is None:
+ _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
+ self._aminor)
+
+ def _FastAssemble(self):
+ """Assemble the drbd device from zero.
+
+ This is run when in Assemble we detect our minor is unused.
+
+ """
+ minor = self._aminor
+ if self._children and self._children[0] and self._children[1]:
+ self._AssembleLocal(minor, self._children[0].dev_path,
+ self._children[1].dev_path, self.size)
+ if self._lhost and self._lport and self._rhost and self._rport:
+ self._AssembleNet(minor,
+ (self._lhost, self._lport, self._rhost, self._rport),
+ constants.DRBD_NET_PROTOCOL,
+ hmac=constants.DRBD_HMAC_ALG, secret=self._secret)
+ self._SetFromMinor(minor)
+
+ @classmethod
+ def _ShutdownLocal(cls, minor):
+ """Detach from the local device.
+
+ I/Os will continue to be served from the remote device. If we
+ don't have a remote device, this operation will fail.
+
+ """
+ result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
+ if result.failed:
+ _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
+
+ @classmethod
+ def _ShutdownNet(cls, minor):
+ """Disconnect from the remote peer.
+
+ This fails if we don't have a local device.
+
+ """
+ result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
+ if result.failed:
+ _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
@classmethod
- def Create(cls, unique_id, children, size):
- """Create a new DRBD device.
+ def _ShutdownAll(cls, minor):
+ """Deactivate the device.
+
+ This will, of course, fail if the device is in use.
+
+ """
+ result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
+ if result.failed:
+ _ThrowError("drbd%d: can't shutdown drbd device: %s",
+ minor, result.output)
+
+ def Shutdown(self):
+ """Shutdown the DRBD device.
+
+ """
+ if self.minor is None and not self.Attach():
+ logging.info("drbd%d: not attached during Shutdown()", self._aminor)
+ return
+ minor = self.minor
+ self.minor = None
+ self.dev_path = None
+ self._ShutdownAll(minor)
+
+ def Remove(self):
+ """Stub remove for DRBD devices.
+
+ """
+ self.Shutdown()
+
+ @classmethod
+ def Create(cls, unique_id, children, size, params, excl_stor):
+ """Create a new DRBD8 device.
Since DRBD devices are not created per se, just assembled, this
- function just zeroes the meta device.
+ function only initializes the metadata.
"""
if len(children) != 2:
raise errors.ProgrammerError("Invalid setup for the drbd device")
+ if excl_stor:
+ raise errors.ProgrammerError("DRBD device requested with"
+ " exclusive_storage")
+ # check that the minor is unused
+ aminor = unique_id[4]
+ proc_info = cls._MassageProcData(cls._GetProcData())
+ if aminor in proc_info:
+ status = DRBD8Status(proc_info[aminor])
+ in_use = status.is_in_use
+ else:
+ in_use = False
+ if in_use:
+ _ThrowError("drbd%d: minor is already in use at Create() time", aminor)
meta = children[1]
meta.Assemble()
if not meta.Attach():
- raise errors.BlockDeviceError("Can't attach to meta device")
- if not cls._CheckMetaSize(meta.dev_path):
- raise errors.BlockDeviceError("Invalid meta device")
- logger.Info("Started zeroing device %s" % meta.dev_path)
- cls._ZeroDevice(meta.dev_path)
- logger.Info("Done zeroing device %s" % meta.dev_path)
- return cls(unique_id, children)
+ _ThrowError("drbd%d: can't attach to meta device '%s'",
+ aminor, meta)
+ cls._CheckMetaSize(meta.dev_path)
+ cls._InitMeta(aminor, meta.dev_path)
+ return cls(unique_id, children, size, params)
+
+ def Grow(self, amount, dryrun, backingstore):
+ """Resize the DRBD device and its backing storage.
+
+ """
+ if self.minor is None:
+ _ThrowError("drbd%d: Grow called while not attached", self._aminor)
+ if len(self._children) != 2 or None in self._children:
+ _ThrowError("drbd%d: cannot grow diskless device", self.minor)
+ self._children[0].Grow(amount, dryrun, backingstore)
+ if dryrun or backingstore:
+ # DRBD does not support dry-run mode and is not backing storage,
+ # so we'll return here
+ return
+ result = utils.RunCmd(["drbdsetup", self.dev_path, "resize", "-s",
+ "%dm" % (self.size + amount)])
+ if result.failed:
+ _ThrowError("drbd%d: resize failed: %s", self.minor, result.output)
+
+
+class FileStorage(BlockDev):
+ """File device.
+
+ This class represents the a file storage backend device.
+
+ The unique_id for the file device is a (file_driver, file_path) tuple.
+
+ """
+ def __init__(self, unique_id, children, size, params):
+ """Initalizes a file device backend.
+
+ """
+ if children:
+ raise errors.BlockDeviceError("Invalid setup for file device")
+ super(FileStorage, self).__init__(unique_id, children, size, params)
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise ValueError("Invalid configuration data %s" % str(unique_id))
+ self.driver = unique_id[0]
+ self.dev_path = unique_id[1]
+
+ CheckFileStoragePath(self.dev_path)
+
+ self.Attach()
+
+ def Assemble(self):
+ """Assemble the device.
+
+ Checks whether the file device exists, raises BlockDeviceError otherwise.
+
+ """
+ if not os.path.exists(self.dev_path):
+ _ThrowError("File device '%s' does not exist" % self.dev_path)
+
+ def Shutdown(self):
+ """Shutdown the device.
+
+ This is a no-op for the file type, as we don't deactivate
+ the file on shutdown.
+
+ """
+ pass
+
+ def Open(self, force=False):
+ """Make the device ready for I/O.
+
+ This is a no-op for the file type.
+
+ """
+ pass
+
+ def Close(self):
+ """Notifies that the device will no longer be used for I/O.
+
+ This is a no-op for the file type.
+
+ """
+ pass
def Remove(self):
- """Stub remove for DRBD devices.
+ """Remove the file backing the block device.
+
+ @rtype: boolean
+ @return: True if the removal was successful
"""
- return self.Shutdown()
+ try:
+ os.remove(self.dev_path)
+ except OSError, err:
+ if err.errno != errno.ENOENT:
+ _ThrowError("Can't remove file '%s': %s", self.dev_path, err)
+ def Rename(self, new_id):
+ """Renames the file.
-class DRBD8(BaseDRBD):
- """DRBD v8.x block device.
+ """
+ # TODO: implement rename for file-based storage
+ _ThrowError("Rename is not supported for file-based storage")
- This implements the local host part of the DRBD device, i.e. it
- doesn't do anything to the supposed peer. If you need a fully
- connected DRBD pair, you need to use this class on both hosts.
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the file
+
+ @param amount: the amount (in mebibytes) to grow with
+
+ """
+ if not backingstore:
+ return
+ # Check that the file exists
+ self.Assemble()
+ current_size = self.GetActualSize()
+ new_size = current_size + amount * 1024 * 1024
+ assert new_size > current_size, "Cannot Grow with a negative amount"
+ # We can't really simulate the growth
+ if dryrun:
+ return
+ try:
+ f = open(self.dev_path, "a+")
+ f.truncate(new_size)
+ f.close()
+ except EnvironmentError, err:
+ _ThrowError("Error in file growth: %", str(err))
+
+ def Attach(self):
+ """Attach to an existing file.
+
+ Check if this file already exists.
+
+ @rtype: boolean
+ @return: True if file exists
+
+ """
+ self.attached = os.path.exists(self.dev_path)
+ return self.attached
+
+ def GetActualSize(self):
+ """Return the actual disk size.
- The unique_id for the drbd device is the (local_ip, local_port,
- remote_ip, remote_port) tuple, and it must have two children: the
- data device and the meta_device. The meta device is checked for
- valid size and is zeroed on create.
+ @note: the device needs to be active when this is called
+
+ """
+ assert self.attached, "BlockDevice not attached in GetActualSize()"
+ try:
+ st = os.stat(self.dev_path)
+ return st.st_size
+ except OSError, err:
+ _ThrowError("Can't stat %s: %s", self.dev_path, err)
+
+ @classmethod
+ def Create(cls, unique_id, children, size, params, excl_stor):
+ """Create a new file.
+
+ @param size: the size of file in MiB
+
+ @rtype: L{bdev.FileStorage}
+ @return: an instance of FileStorage
+
+ """
+ if excl_stor:
+ raise errors.ProgrammerError("FileStorage device requested with"
+ " exclusive_storage")
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise ValueError("Invalid configuration data %s" % str(unique_id))
+
+ dev_path = unique_id[1]
+
+ CheckFileStoragePath(dev_path)
+
+ try:
+ fd = os.open(dev_path, os.O_RDWR | os.O_CREAT | os.O_EXCL)
+ f = os.fdopen(fd, "w")
+ f.truncate(size * 1024 * 1024)
+ f.close()
+ except EnvironmentError, err:
+ if err.errno == errno.EEXIST:
+ _ThrowError("File already existing: %s", dev_path)
+ _ThrowError("Error in file creation: %", str(err))
+
+ return FileStorage(unique_id, children, size, params)
+
+
+class PersistentBlockDevice(BlockDev):
+ """A block device with persistent node
+
+ May be either directly attached, or exposed through DM (e.g. dm-multipath).
+ udev helpers are probably required to give persistent, human-friendly
+ names.
+
+ For the time being, pathnames are required to lie under /dev.
"""
- _MAX_MINORS = 255
- _PARSE_SHOW = None
+ def __init__(self, unique_id, children, size, params):
+ """Attaches to a static block device.
- def __init__(self, unique_id, children):
- if children and children.count(None) > 0:
- children = []
- super(DRBD8, self).__init__(unique_id, children)
- self.major = self._DRBD_MAJOR
- [kmaj, kmin, kfix, api, proto] = self._GetVersion()
- if kmaj != 8:
- raise errors.BlockDeviceError("Mismatch in DRBD kernel version and"
- " requested ganeti usage: kernel is"
- " %s.%s, ganeti wants 8.x" % (kmaj, kmin))
+ The unique_id is a path under /dev.
- if len(children) not in (0, 2):
- raise ValueError("Invalid configuration data %s" % str(children))
- if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
+ """
+ super(PersistentBlockDevice, self).__init__(unique_id, children, size,
+ params)
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
raise ValueError("Invalid configuration data %s" % str(unique_id))
- self._lhost, self._lport, self._rhost, self._rport = unique_id
+ self.dev_path = unique_id[1]
+ if not os.path.realpath(self.dev_path).startswith("/dev/"):
+ raise ValueError("Full path '%s' lies outside /dev" %
+ os.path.realpath(self.dev_path))
+ # TODO: this is just a safety guard checking that we only deal with devices
+ # we know how to handle. In the future this will be integrated with
+ # external storage backends and possible values will probably be collected
+ # from the cluster configuration.
+ if unique_id[0] != constants.BLOCKDEV_DRIVER_MANUAL:
+ raise ValueError("Got persistent block device of invalid type: %s" %
+ unique_id[0])
+
+ self.major = self.minor = None
self.Attach()
@classmethod
- def _InitMeta(cls, minor, dev_path):
- """Initialize a meta device.
+ def Create(cls, unique_id, children, size, params, excl_stor):
+ """Create a new device
- This will not work if the given minor is in use.
+ This is a noop, we only return a PersistentBlockDevice instance
"""
- result = utils.RunCmd(["drbdmeta", "--force", cls._DevPath(minor),
- "v08", dev_path, "0", "create-md"])
- if result.failed:
- raise errors.BlockDeviceError("Can't initialize meta device: %s" %
- result.output)
+ if excl_stor:
+ raise errors.ProgrammerError("Persistent block device requested with"
+ " exclusive_storage")
+ return PersistentBlockDevice(unique_id, children, 0, params)
- @classmethod
- def _FindUnusedMinor(cls):
- """Find an unused DRBD device.
+ def Remove(self):
+ """Remove a device
- This is specific to 8.x as the minors are allocated dynamically,
- so non-existing numbers up to a max minor count are actually free.
+ This is a noop
"""
- data = cls._GetProcData()
+ pass
- unused_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
- used_line = re.compile("^ *([0-9]+): cs:")
- highest = None
- for line in data:
- match = unused_line.match(line)
- if match:
- return int(match.group(1))
- match = used_line.match(line)
- if match:
- minor = int(match.group(1))
- highest = max(highest, minor)
- if highest is None: # there are no minors in use at all
- return 0
- if highest >= cls._MAX_MINORS:
- logger.Error("Error: no free drbd minors!")
- raise errors.BlockDeviceError("Can't find a free DRBD minor")
- return highest + 1
+ def Rename(self, new_id):
+ """Rename this device.
+
+ """
+ _ThrowError("Rename is not supported for PersistentBlockDev storage")
+
+ def Attach(self):
+ """Attach to an existing block device.
- @classmethod
- def _IsValidMeta(cls, meta_device):
- """Check if the given meta device looks like a valid one.
"""
- minor = cls._FindUnusedMinor()
- minor_path = cls._DevPath(minor)
- result = utils.RunCmd(["drbdmeta", minor_path,
- "v08", meta_device, "0",
- "dstate"])
- if result.failed:
- logger.Error("Invalid meta device %s: %s" % (meta_device, result.output))
+ self.attached = False
+ try:
+ st = os.stat(self.dev_path)
+ except OSError, err:
+ logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
+ return False
+
+ if not stat.S_ISBLK(st.st_mode):
+ logging.error("%s is not a block device", self.dev_path)
return False
+
+ self.major = os.major(st.st_rdev)
+ self.minor = os.minor(st.st_rdev)
+ self.attached = True
+
return True
- @classmethod
- def _GetShowParser(cls):
- """Return a parser for `drbd show` output.
+ def Assemble(self):
+ """Assemble the device.
- This will either create or return an already-create parser for the
- output of the command `drbd show`.
+ """
+ pass
+
+ def Shutdown(self):
+ """Shutdown the device.
"""
- if cls._PARSE_SHOW is not None:
- return cls._PARSE_SHOW
+ pass
- # pyparsing setup
- lbrace = pyp.Literal("{").suppress()
- rbrace = pyp.Literal("}").suppress()
- semi = pyp.Literal(";").suppress()
- # this also converts the value to an int
- number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t:(l, [int(t[0])]))
+ def Open(self, force=False):
+ """Make the device ready for I/O.
- comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
- defa = pyp.Literal("_is_default").suppress()
- dbl_quote = pyp.Literal('"').suppress()
+ """
+ pass
- keyword = pyp.Word(pyp.alphanums + '-')
+ def Close(self):
+ """Notifies that the device will no longer be used for I/O.
- # value types
- value = pyp.Word(pyp.alphanums + '_-/.:')
- quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
- addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
- number)
- # meta device, extended syntax
- meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
- number + pyp.Word(']').suppress())
+ """
+ pass
- # a statement
- stmt = (~rbrace + keyword + ~lbrace +
- (addr_port ^ value ^ quoted ^ meta_value) +
- pyp.Optional(defa) + semi +
- pyp.Optional(pyp.restOfLine).suppress())
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the logical volume.
- # an entire section
- section_name = pyp.Word(pyp.alphas + '_')
- section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace
+ """
+ _ThrowError("Grow is not supported for PersistentBlockDev storage")
- bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt))
- bnf.ignore(comment)
- cls._PARSE_SHOW = bnf
+class RADOSBlockDevice(BlockDev):
+ """A RADOS Block Device (rbd).
- return bnf
+ This class implements the RADOS Block Device for the backend. You need
+ the rbd kernel driver, the RADOS Tools and a working RADOS cluster for
+ this to be functional.
+
+ """
+ def __init__(self, unique_id, children, size, params):
+ """Attaches to an rbd device.
+
+ """
+ super(RADOSBlockDevice, self).__init__(unique_id, children, size, params)
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise ValueError("Invalid configuration data %s" % str(unique_id))
+
+ self.driver, self.rbd_name = unique_id
+
+ self.major = self.minor = None
+ self.Attach()
@classmethod
- def _GetDevInfo(cls, minor):
- """Get details about a given DRBD minor.
+ def Create(cls, unique_id, children, size, params, excl_stor):
+ """Create a new rbd device.
- This return, if available, the local backing device (as a path)
- and the local and remote (ip, port) information.
+ Provision a new rbd volume inside a RADOS pool.
"""
- data = {}
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "show"])
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise errors.ProgrammerError("Invalid configuration data %s" %
+ str(unique_id))
+ if excl_stor:
+ raise errors.ProgrammerError("RBD device requested with"
+ " exclusive_storage")
+ rbd_pool = params[constants.LDP_POOL]
+ rbd_name = unique_id[1]
+
+ # Provision a new rbd volume (Image) inside the RADOS cluster.
+ cmd = [constants.RBD_CMD, "create", "-p", rbd_pool,
+ rbd_name, "--size", "%s" % size]
+ result = utils.RunCmd(cmd)
if result.failed:
- logger.Error("Can't display the drbd config: %s" % result.fail_reason)
- return data
- out = result.stdout
- if not out:
- return data
+ _ThrowError("rbd creation failed (%s): %s",
+ result.fail_reason, result.output)
- bnf = cls._GetShowParser()
- # run pyparse
+ return RADOSBlockDevice(unique_id, children, size, params)
+
+ def Remove(self):
+ """Remove the rbd device.
+
+ """
+ rbd_pool = self.params[constants.LDP_POOL]
+ rbd_name = self.unique_id[1]
+
+ if not self.minor and not self.Attach():
+ # The rbd device doesn't exist.
+ return
+
+ # First shutdown the device (remove mappings).
+ self.Shutdown()
+
+ # Remove the actual Volume (Image) from the RADOS cluster.
+ cmd = [constants.RBD_CMD, "rm", "-p", rbd_pool, rbd_name]
+ result = utils.RunCmd(cmd)
+ if result.failed:
+ _ThrowError("Can't remove Volume from cluster with rbd rm: %s - %s",
+ result.fail_reason, result.output)
+
+ def Rename(self, new_id):
+ """Rename this device.
+
+ """
+ pass
+
+ def Attach(self):
+ """Attach to an existing rbd device.
+
+ This method maps the rbd volume that matches our name with
+ an rbd device and then attaches to this device.
+
+ """
+ self.attached = False
+
+ # Map the rbd volume to a block device under /dev
+ self.dev_path = self._MapVolumeToBlockdev(self.unique_id)
try:
- results = bnf.parseString(out)
- except pyp.ParseException, err:
- raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
- str(err))
+ st = os.stat(self.dev_path)
+ except OSError, err:
+ logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
+ return False
- # and massage the results into our desired format
- for section in results:
- sname = section[0]
- if sname == "_this_host":
- for lst in section[1:]:
- if lst[0] == "disk":
- data["local_dev"] = lst[1]
- elif lst[0] == "meta-disk":
- data["meta_dev"] = lst[1]
- data["meta_index"] = lst[2]
- elif lst[0] == "address":
- data["local_addr"] = tuple(lst[1:])
- elif sname == "_remote_host":
- for lst in section[1:]:
- if lst[0] == "address":
- data["remote_addr"] = tuple(lst[1:])
- return data
+ if not stat.S_ISBLK(st.st_mode):
+ logging.error("%s is not a block device", self.dev_path)
+ return False
- def _MatchesLocal(self, info):
- """Test if our local config matches with an existing device.
+ self.major = os.major(st.st_rdev)
+ self.minor = os.minor(st.st_rdev)
+ self.attached = True
- The parameter should be as returned from `_GetDevInfo()`. This
- method tests if our local backing device is the same as the one in
- the info parameter, in effect testing if we look like the given
- device.
+ return True
+
+ def _MapVolumeToBlockdev(self, unique_id):
+ """Maps existing rbd volumes to block devices.
+
+ This method should be idempotent if the mapping already exists.
+
+ @rtype: string
+ @return: the block device path that corresponds to the volume
"""
- if self._children:
- backend, meta = self._children
- else:
- backend = meta = None
+ pool = self.params[constants.LDP_POOL]
+ name = unique_id[1]
- if backend is not None:
- retval = ("local_dev" in info and info["local_dev"] == backend.dev_path)
- else:
- retval = ("local_dev" not in info)
+ # Check if the mapping already exists.
+ rbd_dev = self._VolumeToBlockdev(pool, name)
+ if rbd_dev:
+ # The mapping exists. Return it.
+ return rbd_dev
- if meta is not None:
- retval = retval and ("meta_dev" in info and
- info["meta_dev"] == meta.dev_path)
- retval = retval and ("meta_index" in info and
- info["meta_index"] == 0)
- else:
- retval = retval and ("meta_dev" not in info and
- "meta_index" not in info)
- return retval
+ # The mapping doesn't exist. Create it.
+ map_cmd = [constants.RBD_CMD, "map", "-p", pool, name]
+ result = utils.RunCmd(map_cmd)
+ if result.failed:
+ _ThrowError("rbd map failed (%s): %s",
+ result.fail_reason, result.output)
- def _MatchesNet(self, info):
- """Test if our network config matches with an existing device.
+ # Find the corresponding rbd device.
+ rbd_dev = self._VolumeToBlockdev(pool, name)
+ if not rbd_dev:
+ _ThrowError("rbd map succeeded, but could not find the rbd block"
+ " device in output of showmapped, for volume: %s", name)
- The parameter should be as returned from `_GetDevInfo()`. This
- method tests if our network configuration is the same as the one
- in the info parameter, in effect testing if we look like the given
- device.
+ # The device was successfully mapped. Return it.
+ return rbd_dev
+
+ @classmethod
+ def _VolumeToBlockdev(cls, pool, volume_name):
+ """Do the 'volume name'-to-'rbd block device' resolving.
+
+ @type pool: string
+ @param pool: RADOS pool to use
+ @type volume_name: string
+ @param volume_name: the name of the volume whose device we search for
+ @rtype: string or None
+ @return: block device path if the volume is mapped, else None
+
+ """
+ try:
+ # Newer versions of the rbd tool support json output formatting. Use it
+ # if available.
+ showmap_cmd = [
+ constants.RBD_CMD,
+ "showmapped",
+ "-p",
+ pool,
+ "--format",
+ "json"
+ ]
+ result = utils.RunCmd(showmap_cmd)
+ if result.failed:
+ logging.error("rbd JSON output formatting returned error (%s): %s,"
+ "falling back to plain output parsing",
+ result.fail_reason, result.output)
+ raise RbdShowmappedJsonError
+
+ return cls._ParseRbdShowmappedJson(result.output, volume_name)
+ except RbdShowmappedJsonError:
+ # For older versions of rbd, we have to parse the plain / text output
+ # manually.
+ showmap_cmd = [constants.RBD_CMD, "showmapped", "-p", pool]
+ result = utils.RunCmd(showmap_cmd)
+ if result.failed:
+ _ThrowError("rbd showmapped failed (%s): %s",
+ result.fail_reason, result.output)
+
+ return cls._ParseRbdShowmappedPlain(result.output, volume_name)
+
+ @staticmethod
+ def _ParseRbdShowmappedJson(output, volume_name):
+ """Parse the json output of `rbd showmapped'.
+
+ This method parses the json output of `rbd showmapped' and returns the rbd
+ block device path (e.g. /dev/rbd0) that matches the given rbd volume.
+
+ @type output: string
+ @param output: the json output of `rbd showmapped'
+ @type volume_name: string
+ @param volume_name: the name of the volume whose device we search for
+ @rtype: string or None
+ @return: block device path if the volume is mapped, else None
+
+ """
+ try:
+ devices = serializer.LoadJson(output)
+ except ValueError, err:
+ _ThrowError("Unable to parse JSON data: %s" % err)
+
+ rbd_dev = None
+ for d in devices.values(): # pylint: disable=E1103
+ try:
+ name = d["name"]
+ except KeyError:
+ _ThrowError("'name' key missing from json object %s", devices)
+
+ if name == volume_name:
+ if rbd_dev is not None:
+ _ThrowError("rbd volume %s is mapped more than once", volume_name)
+
+ rbd_dev = d["device"]
+
+ return rbd_dev
+
+ @staticmethod
+ def _ParseRbdShowmappedPlain(output, volume_name):
+ """Parse the (plain / text) output of `rbd showmapped'.
+
+ This method parses the output of `rbd showmapped' and returns
+ the rbd block device path (e.g. /dev/rbd0) that matches the
+ given rbd volume.
+
+ @type output: string
+ @param output: the plain text output of `rbd showmapped'
+ @type volume_name: string
+ @param volume_name: the name of the volume whose device we search for
+ @rtype: string or None
+ @return: block device path if the volume is mapped, else None
+
+ """
+ allfields = 5
+ volumefield = 2
+ devicefield = 4
+
+ lines = output.splitlines()
+
+ # Try parsing the new output format (ceph >= 0.55).
+ splitted_lines = map(lambda l: l.split(), lines)
+
+ # Check for empty output.
+ if not splitted_lines:
+ return None
+
+ # Check showmapped output, to determine number of fields.
+ field_cnt = len(splitted_lines[0])
+ if field_cnt != allfields:
+ # Parsing the new format failed. Fallback to parsing the old output
+ # format (< 0.55).
+ splitted_lines = map(lambda l: l.split("\t"), lines)
+ if field_cnt != allfields:
+ _ThrowError("Cannot parse rbd showmapped output expected %s fields,"
+ " found %s", allfields, field_cnt)
+
+ matched_lines = \
+ filter(lambda l: len(l) == allfields and l[volumefield] == volume_name,
+ splitted_lines)
+
+ if len(matched_lines) > 1:
+ _ThrowError("rbd volume %s mapped more than once", volume_name)
+
+ if matched_lines:
+ # rbd block device found. Return it.
+ rbd_dev = matched_lines[0][devicefield]
+ return rbd_dev
+
+ # The given volume is not mapped.
+ return None
+
+ def Assemble(self):
+ """Assemble the device.
+
+ """
+ pass
+
+ def Shutdown(self):
+ """Shutdown the device.
+
+ """
+ if not self.minor and not self.Attach():
+ # The rbd device doesn't exist.
+ return
+
+ # Unmap the block device from the Volume.
+ self._UnmapVolumeFromBlockdev(self.unique_id)
+
+ self.minor = None
+ self.dev_path = None
+
+ def _UnmapVolumeFromBlockdev(self, unique_id):
+ """Unmaps the rbd device from the Volume it is mapped.
+
+ Unmaps the rbd device from the Volume it was previously mapped to.
+ This method should be idempotent if the Volume isn't mapped.
+
+ """
+ pool = self.params[constants.LDP_POOL]
+ name = unique_id[1]
+
+ # Check if the mapping already exists.
+ rbd_dev = self._VolumeToBlockdev(pool, name)
+
+ if rbd_dev:
+ # The mapping exists. Unmap the rbd device.
+ unmap_cmd = [constants.RBD_CMD, "unmap", "%s" % rbd_dev]
+ result = utils.RunCmd(unmap_cmd)
+ if result.failed:
+ _ThrowError("rbd unmap failed (%s): %s",
+ result.fail_reason, result.output)
+
+ def Open(self, force=False):
+ """Make the device ready for I/O.
+
+ """
+ pass
+
+ def Close(self):
+ """Notifies that the device will no longer be used for I/O.
+
+ """
+ pass
+
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the Volume.
+
+ @type amount: integer
+ @param amount: the amount (in mebibytes) to grow with
+ @type dryrun: boolean
+ @param dryrun: whether to execute the operation in simulation mode
+ only, without actually increasing the size
+
+ """
+ if not backingstore:
+ return
+ if not self.Attach():
+ _ThrowError("Can't attach to rbd device during Grow()")
+
+ if dryrun:
+ # the rbd tool does not support dry runs of resize operations.
+ # Since rbd volumes are thinly provisioned, we assume
+ # there is always enough free space for the operation.
+ return
- """
- if (((self._lhost is None and not ("local_addr" in info)) and
- (self._rhost is None and not ("remote_addr" in info)))):
- return True
+ rbd_pool = self.params[constants.LDP_POOL]
+ rbd_name = self.unique_id[1]
+ new_size = self.size + amount
- if self._lhost is None:
- return False
+ # Resize the rbd volume (Image) inside the RADOS cluster.
+ cmd = [constants.RBD_CMD, "resize", "-p", rbd_pool,
+ rbd_name, "--size", "%s" % new_size]
+ result = utils.RunCmd(cmd)
+ if result.failed:
+ _ThrowError("rbd resize failed (%s): %s",
+ result.fail_reason, result.output)
- if not ("local_addr" in info and
- "remote_addr" in info):
- return False
- retval = (info["local_addr"] == (self._lhost, self._lport))
- retval = (retval and
- info["remote_addr"] == (self._rhost, self._rport))
- return retval
+class ExtStorageDevice(BlockDev):
+ """A block device provided by an ExtStorage Provider.
- @classmethod
- def _AssembleLocal(cls, minor, backend, meta):
- """Configure the local part of a DRBD device.
+ This class implements the External Storage Interface, which means
+ handling of the externally provided block devices.
- This is the first thing that must be done on an unconfigured DRBD
- device. And it must be done only once.
+ """
+ def __init__(self, unique_id, children, size, params):
+ """Attaches to an extstorage block device.
"""
- if not cls._IsValidMeta(meta):
- return False
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
- backend, meta, "0", "-e", "detach",
- "--create-device"])
- if result.failed:
- logger.Error("Can't attach local disk: %s" % result.output)
- return not result.failed
+ super(ExtStorageDevice, self).__init__(unique_id, children, size, params)
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise ValueError("Invalid configuration data %s" % str(unique_id))
+
+ self.driver, self.vol_name = unique_id
+ self.ext_params = params
+
+ self.major = self.minor = None
+ self.Attach()
@classmethod
- def _AssembleNet(cls, minor, net_info, protocol,
- dual_pri=False, hmac=None, secret=None):
- """Configure the network part of the device.
+ def Create(cls, unique_id, children, size, params, excl_stor):
+ """Create a new extstorage device.
+
+ Provision a new volume using an extstorage provider, which will
+ then be mapped to a block device.
"""
- lhost, lport, rhost, rport = net_info
- if None in net_info:
- # we don't want network connection and actually want to make
- # sure its shutdown
- return cls._ShutdownNet(minor)
+ if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 2:
+ raise errors.ProgrammerError("Invalid configuration data %s" %
+ str(unique_id))
+ if excl_stor:
+ raise errors.ProgrammerError("extstorage device requested with"
+ " exclusive_storage")
- args = ["drbdsetup", cls._DevPath(minor), "net",
- "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
- "-A", "discard-zero-changes",
- "-B", "consensus",
- ]
- if dual_pri:
- args.append("-m")
- if hmac and secret:
- args.extend(["-a", hmac, "-x", secret])
- result = utils.RunCmd(args)
- if result.failed:
- logger.Error("Can't setup network for dbrd device: %s" %
- result.fail_reason)
- return False
+ # Call the External Storage's create script,
+ # to provision a new Volume inside the External Storage
+ _ExtStorageAction(constants.ES_ACTION_CREATE, unique_id,
+ params, str(size))
- timeout = time.time() + 10
- ok = False
- while time.time() < timeout:
- info = cls._GetDevInfo(minor)
- if not "local_addr" in info or not "remote_addr" in info:
- time.sleep(1)
- continue
- if (info["local_addr"] != (lhost, lport) or
- info["remote_addr"] != (rhost, rport)):
- time.sleep(1)
- continue
- ok = True
- break
- if not ok:
- logger.Error("Timeout while configuring network")
- return False
- return True
+ return ExtStorageDevice(unique_id, children, size, params)
- def AddChildren(self, devices):
- """Add a disk to the DRBD device.
+ def Remove(self):
+ """Remove the extstorage device.
"""
- if self.minor is None:
- raise errors.BlockDeviceError("Can't attach to dbrd8 during AddChildren")
- if len(devices) != 2:
- raise errors.BlockDeviceError("Need two devices for AddChildren")
- info = self._GetDevInfo(self.minor)
- if "local_dev" in info:
- raise errors.BlockDeviceError("DRBD8 already attached to a local disk")
- backend, meta = devices
- if backend.dev_path is None or meta.dev_path is None:
- raise errors.BlockDeviceError("Children not ready during AddChildren")
- backend.Open()
- meta.Open()
- if not self._CheckMetaSize(meta.dev_path):
- raise errors.BlockDeviceError("Invalid meta device size")
- self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
- if not self._IsValidMeta(meta.dev_path):
- raise errors.BlockDeviceError("Cannot initalize meta device")
+ if not self.minor and not self.Attach():
+ # The extstorage device doesn't exist.
+ return
- if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
- raise errors.BlockDeviceError("Can't attach to local storage")
- self._children = devices
+ # First shutdown the device (remove mappings).
+ self.Shutdown()
- def RemoveChildren(self, devices):
- """Detach the drbd device from local storage.
+ # Call the External Storage's remove script,
+ # to remove the Volume from the External Storage
+ _ExtStorageAction(constants.ES_ACTION_REMOVE, self.unique_id,
+ self.ext_params)
+
+ def Rename(self, new_id):
+ """Rename this device.
"""
- if self.minor is None:
- raise errors.BlockDeviceError("Can't attach to drbd8 during"
- " RemoveChildren")
- # early return if we don't actually have backing storage
- info = self._GetDevInfo(self.minor)
- if "local_dev" not in info:
- return
- if len(self._children) != 2:
- raise errors.BlockDeviceError("We don't have two children: %s" %
- self._children)
- if self._children.count(None) == 2: # we don't actually have children :)
- logger.Error("Requested detach while detached")
- return
- if len(devices) != 2:
- raise errors.BlockDeviceError("We need two children in RemoveChildren")
- for child, dev in zip(self._children, devices):
- if dev != child.dev_path:
- raise errors.BlockDeviceError("Mismatch in local storage"
- " (%s != %s) in RemoveChildren" %
- (dev, child.dev_path))
+ pass
- if not self._ShutdownLocal(self.minor):
- raise errors.BlockDeviceError("Can't detach from local storage")
- self._children = []
+ def Attach(self):
+ """Attach to an existing extstorage device.
- def SetSyncSpeed(self, kbytes):
- """Set the speed of the DRBD syncer.
+ This method maps the extstorage volume that matches our name with
+ a corresponding block device and then attaches to this device.
"""
- children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
- if self.minor is None:
- logger.Info("Instance not attached to a device")
- return False
- result = utils.RunCmd(["drbdsetup", self.dev_path, "syncer", "-r", "%d" %
- kbytes])
- if result.failed:
- logger.Error("Can't change syncer rate: %s " % result.fail_reason)
- return not result.failed and children_result
+ self.attached = False
- def GetSyncStatus(self):
- """Returns the sync status of the device.
+ # Call the External Storage's attach script,
+ # to attach an existing Volume to a block device under /dev
+ self.dev_path = _ExtStorageAction(constants.ES_ACTION_ATTACH,
+ self.unique_id, self.ext_params)
- Returns:
- (sync_percent, estimated_time, is_degraded)
+ try:
+ st = os.stat(self.dev_path)
+ except OSError, err:
+ logging.error("Error stat()'ing %s: %s", self.dev_path, str(err))
+ return False
- If sync_percent is None, it means all is ok
- If estimated_time is None, it means we can't esimate
- the time needed, otherwise it's the time left in seconds.
+ if not stat.S_ISBLK(st.st_mode):
+ logging.error("%s is not a block device", self.dev_path)
+ return False
+ self.major = os.major(st.st_rdev)
+ self.minor = os.minor(st.st_rdev)
+ self.attached = True
- We set the is_degraded parameter to True on two conditions:
- network not connected or local disk missing.
+ return True
- We compute the ldisk parameter based on wheter we have a local
- disk or not.
+ def Assemble(self):
+ """Assemble the device.
"""
- if self.minor is None and not self.Attach():
- raise errors.BlockDeviceError("Can't attach to device in GetSyncStatus")
- proc_info = self._MassageProcData(self._GetProcData())
- if self.minor not in proc_info:
- raise errors.BlockDeviceError("Can't find myself in /proc (minor %d)" %
- self.minor)
- line = proc_info[self.minor]
- match = re.match("^.*sync'ed: *([0-9.]+)%.*"
- " finish: ([0-9]+):([0-9]+):([0-9]+) .*$", line)
- if match:
- sync_percent = float(match.group(1))
- hours = int(match.group(2))
- minutes = int(match.group(3))
- seconds = int(match.group(4))
- est_time = hours * 3600 + minutes * 60 + seconds
- else:
- sync_percent = None
- est_time = None
- match = re.match("^ *\d+: cs:(\w+).*ds:(\w+)/(\w+).*$", line)
- if not match:
- raise errors.BlockDeviceError("Can't find my data in /proc (minor %d)" %
- self.minor)
- client_state = match.group(1)
- local_disk_state = match.group(2)
- ldisk = local_disk_state != "UpToDate"
- is_degraded = client_state != "Connected"
- return sync_percent, est_time, is_degraded or ldisk, ldisk
+ pass
- def GetStatus(self):
- """Compute the status of the DRBD device
-
- Note that DRBD devices don't have the STATUS_EXISTING state.
+ def Shutdown(self):
+ """Shutdown the device.
"""
- if self.minor is None and not self.Attach():
- return self.STATUS_UNKNOWN
-
- data = self._GetProcData()
- match = re.compile("^ *%d: cs:[^ ]+ st:(Primary|Secondary)/.*$" %
- self.minor)
- for line in data:
- mresult = match.match(line)
- if mresult:
- break
- else:
- logger.Error("Can't find myself!")
- return self.STATUS_UNKNOWN
+ if not self.minor and not self.Attach():
+ # The extstorage device doesn't exist.
+ return
- state = mresult.group(2)
- if state == "Primary":
- result = self.STATUS_ONLINE
- else:
- result = self.STATUS_STANDBY
+ # Call the External Storage's detach script,
+ # to detach an existing Volume from it's block device under /dev
+ _ExtStorageAction(constants.ES_ACTION_DETACH, self.unique_id,
+ self.ext_params)
- return result
+ self.minor = None
+ self.dev_path = None
def Open(self, force=False):
- """Make the local state primary.
-
- If the 'force' parameter is given, the '--do-what-I-say' parameter
- is given. Since this is a pottentialy dangerous operation, the
- force flag should be only given after creation, when it actually
- has to be given.
+ """Make the device ready for I/O.
"""
- if self.minor is None and not self.Attach():
- logger.Error("DRBD cannot attach to a device during open")
- return False
- cmd = ["drbdsetup", self.dev_path, "primary"]
- if force:
- cmd.append("-o")
- result = utils.RunCmd(cmd)
- if result.failed:
- logger.Error("Can't make drbd device primary: %s" % result.output)
- return False
- return True
+ pass
def Close(self):
- """Make the local state secondary.
+ """Notifies that the device will no longer be used for I/O.
- This will, of course, fail if the device is in use.
+ """
+ pass
+
+ def Grow(self, amount, dryrun, backingstore):
+ """Grow the Volume.
+
+ @type amount: integer
+ @param amount: the amount (in mebibytes) to grow with
+ @type dryrun: boolean
+ @param dryrun: whether to execute the operation in simulation mode
+ only, without actually increasing the size
"""
- if self.minor is None and not self.Attach():
- logger.Info("Instance not attached to a device")
- raise errors.BlockDeviceError("Can't find device")
- result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"])
- if result.failed:
- logger.Error("Can't switch drbd device to secondary: %s" % result.output)
- raise errors.BlockDeviceError("Can't switch drbd device to secondary")
+ if not backingstore:
+ return
+ if not self.Attach():
+ _ThrowError("Can't attach to extstorage device during Grow()")
- def Attach(self):
- """Find a DRBD device which matches our config and attach to it.
+ if dryrun:
+ # we do not support dry runs of resize operations for now.
+ return
- In case of partially attached (local device matches but no network
- setup), we perform the network attach. If successful, we re-test
- the attach if can return success.
+ new_size = self.size + amount
+
+ # Call the External Storage's grow script,
+ # to grow an existing Volume inside the External Storage
+ _ExtStorageAction(constants.ES_ACTION_GROW, self.unique_id,
+ self.ext_params, str(self.size), grow=str(new_size))
+
+ def SetInfo(self, text):
+ """Update metadata with info text.
"""
- for minor in self._GetUsedDevs():
- info = self._GetDevInfo(minor)
- match_l = self._MatchesLocal(info)
- match_r = self._MatchesNet(info)
- if match_l and match_r:
- break
- if match_l and not match_r and "local_addr" not in info:
- res_r = self._AssembleNet(minor,
- (self._lhost, self._lport,
- self._rhost, self._rport),
- "C")
- if res_r and self._MatchesNet(self._GetDevInfo(minor)):
- break
- # the weakest case: we find something that is only net attached
- # even though we were passed some children at init time
- if match_r and "local_dev" not in info:
- break
- if match_l and not match_r and "local_addr" in info:
- # strange case - the device network part points to somewhere
- # else, even though its local storage is ours; as we own the
- # drbd space, we try to disconnect from the remote peer and
- # reconnect to our correct one
- if not self._ShutdownNet(minor):
- raise errors.BlockDeviceError("Device has correct local storage,"
- " wrong remote peer and is unable to"
- " disconnect in order to attach to"
- " the correct peer")
- # note: _AssembleNet also handles the case when we don't want
- # local storage (i.e. one or more of the _[lr](host|port) is
- # None)
- if (self._AssembleNet(minor, (self._lhost, self._lport,
- self._rhost, self._rport), "C") and
- self._MatchesNet(self._GetDevInfo(minor))):
- break
+ # Replace invalid characters
+ text = re.sub("^[^A-Za-z0-9_+.]", "_", text)
+ text = re.sub("[^-A-Za-z0-9_+.]", "_", text)
+
+ # Only up to 128 characters are allowed
+ text = text[:128]
+ # Call the External Storage's setinfo script,
+ # to set metadata for an existing Volume inside the External Storage
+ _ExtStorageAction(constants.ES_ACTION_SETINFO, self.unique_id,
+ self.ext_params, metadata=text)
+
+
+def _ExtStorageAction(action, unique_id, ext_params,
+ size=None, grow=None, metadata=None):
+ """Take an External Storage action.
+
+ Take an External Storage action concerning or affecting
+ a specific Volume inside the External Storage.
+
+ @type action: string
+ @param action: which action to perform. One of:
+ create / remove / grow / attach / detach
+ @type unique_id: tuple (driver, vol_name)
+ @param unique_id: a tuple containing the type of ExtStorage (driver)
+ and the Volume name
+ @type ext_params: dict
+ @param ext_params: ExtStorage parameters
+ @type size: integer
+ @param size: the size of the Volume in mebibytes
+ @type grow: integer
+ @param grow: the new size in mebibytes (after grow)
+ @type metadata: string
+ @param metadata: metadata info of the Volume, for use by the provider
+ @rtype: None or a block device path (during attach)
+
+ """
+ driver, vol_name = unique_id
+
+ # Create an External Storage instance of type `driver'
+ status, inst_es = ExtStorageFromDisk(driver)
+ if not status:
+ _ThrowError("%s" % inst_es)
+
+ # Create the basic environment for the driver's scripts
+ create_env = _ExtStorageEnvironment(unique_id, ext_params, size,
+ grow, metadata)
+
+ # Do not use log file for action `attach' as we need
+ # to get the output from RunResult
+ # TODO: find a way to have a log file for attach too
+ logfile = None
+ if action is not constants.ES_ACTION_ATTACH:
+ logfile = _VolumeLogName(action, driver, vol_name)
+
+ # Make sure the given action results in a valid script
+ if action not in constants.ES_SCRIPTS:
+ _ThrowError("Action '%s' doesn't result in a valid ExtStorage script" %
+ action)
+
+ # Find out which external script to run according the given action
+ script_name = action + "_script"
+ script = getattr(inst_es, script_name)
+
+ # Run the external script
+ result = utils.RunCmd([script], env=create_env,
+ cwd=inst_es.path, output=logfile,)
+ if result.failed:
+ logging.error("External storage's %s command '%s' returned"
+ " error: %s, logfile: %s, output: %s",
+ action, result.cmd, result.fail_reason,
+ logfile, result.output)
+
+ # If logfile is 'None' (during attach), it breaks TailFile
+ # TODO: have a log file for attach too
+ if action is not constants.ES_ACTION_ATTACH:
+ lines = [utils.SafeEncode(val)
+ for val in utils.TailFile(logfile, lines=20)]
else:
- minor = None
+ lines = result.output[-20:]
- self._SetFromMinor(minor)
- return minor is not None
+ _ThrowError("External storage's %s script failed (%s), last"
+ " lines of output:\n%s",
+ action, result.fail_reason, "\n".join(lines))
- def Assemble(self):
- """Assemble the drbd.
+ if action == constants.ES_ACTION_ATTACH:
+ return result.stdout
- Method:
- - if we have a local backing device, we bind to it by:
- - checking the list of used drbd devices
- - check if the local minor use of any of them is our own device
- - if yes, abort?
- - if not, bind
- - if we have a local/remote net info:
- - redo the local backing device step for the remote device
- - check if any drbd device is using the local port,
- if yes abort
- - check if any remote drbd device is using the remote
- port, if yes abort (for now)
- - bind our net port
- - bind the remote net port
- """
- self.Attach()
- if self.minor is not None:
- logger.Info("Already assembled")
- return True
+def ExtStorageFromDisk(name, base_dir=None):
+ """Create an ExtStorage instance from disk.
- result = super(DRBD8, self).Assemble()
- if not result:
- return result
+ This function will return an ExtStorage instance
+ if the given name is a valid ExtStorage name.
- minor = self._FindUnusedMinor()
- need_localdev_teardown = False
- if self._children and self._children[0] and self._children[1]:
- result = self._AssembleLocal(minor, self._children[0].dev_path,
- self._children[1].dev_path)
- if not result:
- return False
- need_localdev_teardown = True
- if self._lhost and self._lport and self._rhost and self._rport:
- result = self._AssembleNet(minor,
- (self._lhost, self._lport,
- self._rhost, self._rport),
- "C")
- if not result:
- if need_localdev_teardown:
- # we will ignore failures from this
- logger.Error("net setup failed, tearing down local device")
- self._ShutdownAll(minor)
- return False
- self._SetFromMinor(minor)
- return True
+ @type base_dir: string
+ @keyword base_dir: Base directory containing ExtStorage installations.
+ Defaults to a search in all the ES_SEARCH_PATH dirs.
+ @rtype: tuple
+ @return: True and the ExtStorage instance if we find a valid one, or
+ False and the diagnose message on error
- @classmethod
- def _ShutdownLocal(cls, minor):
- """Detach from the local device.
+ """
+ if base_dir is None:
+ es_base_dir = pathutils.ES_SEARCH_PATH
+ else:
+ es_base_dir = [base_dir]
- I/Os will continue to be served from the remote device. If we
- don't have a remote device, this operation will fail.
+ es_dir = utils.FindFile(name, es_base_dir, os.path.isdir)
- """
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "detach"])
- if result.failed:
- logger.Error("Can't detach local device: %s" % result.output)
- return not result.failed
+ if es_dir is None:
+ return False, ("Directory for External Storage Provider %s not"
+ " found in search path" % name)
- @classmethod
- def _ShutdownNet(cls, minor):
- """Disconnect from the remote peer.
+ # ES Files dictionary, we will populate it with the absolute path
+ # names; if the value is True, then it is a required file, otherwise
+ # an optional one
+ es_files = dict.fromkeys(constants.ES_SCRIPTS, True)
- This fails if we don't have a local device.
+ es_files[constants.ES_PARAMETERS_FILE] = True
- """
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
- if result.failed:
- logger.Error("Can't shutdown network: %s" % result.output)
- return not result.failed
+ for (filename, _) in es_files.items():
+ es_files[filename] = utils.PathJoin(es_dir, filename)
- @classmethod
- def _ShutdownAll(cls, minor):
- """Deactivate the device.
+ try:
+ st = os.stat(es_files[filename])
+ except EnvironmentError, err:
+ return False, ("File '%s' under path '%s' is missing (%s)" %
+ (filename, es_dir, utils.ErrnoOrStr(err)))
+
+ if not stat.S_ISREG(stat.S_IFMT(st.st_mode)):
+ return False, ("File '%s' under path '%s' is not a regular file" %
+ (filename, es_dir))
+
+ if filename in constants.ES_SCRIPTS:
+ if stat.S_IMODE(st.st_mode) & stat.S_IXUSR != stat.S_IXUSR:
+ return False, ("File '%s' under path '%s' is not executable" %
+ (filename, es_dir))
+
+ parameters = []
+ if constants.ES_PARAMETERS_FILE in es_files:
+ parameters_file = es_files[constants.ES_PARAMETERS_FILE]
+ try:
+ parameters = utils.ReadFile(parameters_file).splitlines()
+ except EnvironmentError, err:
+ return False, ("Error while reading the EXT parameters file at %s: %s" %
+ (parameters_file, utils.ErrnoOrStr(err)))
+ parameters = [v.split(None, 1) for v in parameters]
+
+ es_obj = \
+ objects.ExtStorage(name=name, path=es_dir,
+ create_script=es_files[constants.ES_SCRIPT_CREATE],
+ remove_script=es_files[constants.ES_SCRIPT_REMOVE],
+ grow_script=es_files[constants.ES_SCRIPT_GROW],
+ attach_script=es_files[constants.ES_SCRIPT_ATTACH],
+ detach_script=es_files[constants.ES_SCRIPT_DETACH],
+ setinfo_script=es_files[constants.ES_SCRIPT_SETINFO],
+ verify_script=es_files[constants.ES_SCRIPT_VERIFY],
+ supported_parameters=parameters)
+ return True, es_obj
+
+
+def _ExtStorageEnvironment(unique_id, ext_params,
+ size=None, grow=None, metadata=None):
+ """Calculate the environment for an External Storage script.
+
+ @type unique_id: tuple (driver, vol_name)
+ @param unique_id: ExtStorage pool and name of the Volume
+ @type ext_params: dict
+ @param ext_params: the EXT parameters
+ @type size: string
+ @param size: size of the Volume (in mebibytes)
+ @type grow: string
+ @param grow: new size of Volume after grow (in mebibytes)
+ @type metadata: string
+ @param metadata: metadata info of the Volume
+ @rtype: dict
+ @return: dict of environment variables
- This will, of course, fail if the device is in use.
+ """
+ vol_name = unique_id[1]
- """
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "down"])
- if result.failed:
- logger.Error("Can't shutdown drbd device: %s" % result.output)
- return not result.failed
+ result = {}
+ result["VOL_NAME"] = vol_name
- def Shutdown(self):
- """Shutdown the DRBD device.
+ # EXT params
+ for pname, pvalue in ext_params.items():
+ result["EXTP_%s" % pname.upper()] = str(pvalue)
- """
- if self.minor is None and not self.Attach():
- logger.Info("DRBD device not attached to a device during Shutdown")
- return True
- if not self._ShutdownAll(self.minor):
- return False
- self.minor = None
- self.dev_path = None
- return True
+ if size is not None:
+ result["VOL_SIZE"] = size
- def Remove(self):
- """Stub remove for DRBD devices.
+ if grow is not None:
+ result["VOL_NEW_SIZE"] = grow
- """
- return self.Shutdown()
+ if metadata is not None:
+ result["VOL_METADATA"] = metadata
- @classmethod
- def Create(cls, unique_id, children, size):
- """Create a new DRBD8 device.
+ return result
- Since DRBD devices are not created per se, just assembled, this
- function only initializes the metadata.
- """
- if len(children) != 2:
- raise errors.ProgrammerError("Invalid setup for the drbd device")
- meta = children[1]
- meta.Assemble()
- if not meta.Attach():
- raise errors.BlockDeviceError("Can't attach to meta device")
- if not cls._CheckMetaSize(meta.dev_path):
- raise errors.BlockDeviceError("Invalid meta device size")
- cls._InitMeta(cls._FindUnusedMinor(), meta.dev_path)
- if not cls._IsValidMeta(meta.dev_path):
- raise errors.BlockDeviceError("Cannot initalize meta device")
- return cls(unique_id, children)
+def _VolumeLogName(kind, es_name, volume):
+ """Compute the ExtStorage log filename for a given Volume and operation.
+
+ @type kind: string
+ @param kind: the operation type (e.g. create, remove etc.)
+ @type es_name: string
+ @param es_name: the ExtStorage name
+ @type volume: string
+ @param volume: the name of the Volume inside the External Storage
+
+ """
+ # Check if the extstorage log dir is a valid dir
+ if not os.path.isdir(pathutils.LOG_ES_DIR):
+ _ThrowError("Cannot find log directory: %s", pathutils.LOG_ES_DIR)
+
+ # TODO: Use tempfile.mkstemp to create unique filename
+ base = ("%s-%s-%s-%s.log" %
+ (kind, es_name, volume, utils.TimestampForFilename()))
+ return utils.PathJoin(pathutils.LOG_ES_DIR, base)
DEV_MAP = {
constants.LD_LV: LogicalVolume,
- constants.LD_MD_R1: MDRaid1,
- constants.LD_DRBD7: DRBDev,
constants.LD_DRBD8: DRBD8,
+ constants.LD_BLOCKDEV: PersistentBlockDevice,
+ constants.LD_RBD: RADOSBlockDevice,
+ constants.LD_EXT: ExtStorageDevice,
}
+if constants.ENABLE_FILE_STORAGE or constants.ENABLE_SHARED_FILE_STORAGE:
+ DEV_MAP[constants.LD_FILE] = FileStorage
+
+
+def _VerifyDiskType(dev_type):
+ if dev_type not in DEV_MAP:
+ raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
+
+
+def _VerifyDiskParams(disk):
+ """Verifies if all disk parameters are set.
+
+ """
+ missing = set(constants.DISK_LD_DEFAULTS[disk.dev_type]) - set(disk.params)
+ if missing:
+ raise errors.ProgrammerError("Block device is missing disk parameters: %s" %
+ missing)
+
-def FindDevice(dev_type, unique_id, children):
+def FindDevice(disk, children):
"""Search for an existing, assembled device.
This will succeed only if the device exists and is assembled, but it
does not do any actions in order to activate the device.
+ @type disk: L{objects.Disk}
+ @param disk: the disk object to find
+ @type children: list of L{bdev.BlockDev}
+ @param children: the list of block devices that are children of the device
+ represented by the disk parameter
+
"""
- if dev_type not in DEV_MAP:
- raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
- device = DEV_MAP[dev_type](unique_id, children)
- if not device.Attach():
+ _VerifyDiskType(disk.dev_type)
+ device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
+ disk.params)
+ if not device.attached:
return None
- return device
+ return device
-def AttachOrAssemble(dev_type, unique_id, children):
+def Assemble(disk, children):
"""Try to attach or assemble an existing device.
- This will attach to an existing assembled device or will assemble
- the device, as needed, to bring it fully up.
+ This will attach to assemble the device, as needed, to bring it
+ fully up. It must be safe to run on already-assembled devices.
+
+ @type disk: L{objects.Disk}
+ @param disk: the disk object to assemble
+ @type children: list of L{bdev.BlockDev}
+ @param children: the list of block devices that are children of the device
+ represented by the disk parameter
"""
- if dev_type not in DEV_MAP:
- raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
- device = DEV_MAP[dev_type](unique_id, children)
- if not device.Attach():
- device.Assemble()
- if not device.Attach():
- raise errors.BlockDeviceError("Can't find a valid block device for"
- " %s/%s/%s" %
- (dev_type, unique_id, children))
+ _VerifyDiskType(disk.dev_type)
+ _VerifyDiskParams(disk)
+ device = DEV_MAP[disk.dev_type](disk.physical_id, children, disk.size,
+ disk.params)
+ device.Assemble()
return device
-def Create(dev_type, unique_id, children, size):
+def Create(disk, children, excl_stor):
"""Create a device.
+ @type disk: L{objects.Disk}
+ @param disk: the disk object to create
+ @type children: list of L{bdev.BlockDev}
+ @param children: the list of block devices that are children of the device
+ represented by the disk parameter
+ @type excl_stor: boolean
+ @param excl_stor: Whether exclusive_storage is active
+
"""
- if dev_type not in DEV_MAP:
- raise errors.ProgrammerError("Invalid block device type '%s'" % dev_type)
- device = DEV_MAP[dev_type].Create(unique_id, children, size)
+ _VerifyDiskType(disk.dev_type)
+ _VerifyDiskParams(disk)
+ device = DEV_MAP[disk.dev_type].Create(disk.physical_id, children, disk.size,
+ disk.params, excl_stor)
return device