import time
import errno
import pyparsing as pyp
+import os
+import logging
from ganeti import utils
-from ganeti import logger
from ganeti import errors
from ganeti import constants
+from ganeti import objects
+
+
+# Size of reads in _CanReadDevice
+_DEVICE_READ_SIZE = 128 * 1024
+
+
+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 _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
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
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):
self._children = children
self.dev_path = None
self.unique_id = unique_id
self.major = None
self.minor = None
+ self.attached = False
+ self.size = size
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.
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.
If this device is a mirroring device, this function returns the
status of the mirror.
- Returns:
- (sync_percent, estimated_time, is_degraded)
+ If sync_percent is None, it means the device is not syncing.
- 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
+ the time needed, otherwise it's the time left in seconds.
+
If is_degraded is True, it means the device is missing
redundancy. This is usually a sign that something went wrong in
the device setup, if sync_percent is None.
- """
- return None, None, False
+ The ldisk parameter represents the degradation of the local
+ data. This is only valid for some devices, the rest will always
+ return False (not degraded).
+ @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 = 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 = 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
- return min_percent, max_time, is_degraded
+ 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):
for child in self._children:
child.SetInfo(text)
+ def Grow(self, amount):
+ """Grow the block device.
+
+ @param amount: the amount (in mebibytes) to grow with
+
+ """
+ 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):
+ def __init__(self, unique_id, children, size):
"""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)
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._degraded = True
+ self.major = self.minor = self.pe_size = self.stripe_count = None
self.Attach()
@classmethod
"""
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)
+ pvs_info = cls.GetPVInfo([vg_name])
if not pvs_info:
- raise errors.BlockDeviceError("Can't compute PV info for vg %s" %
- vg_name)
+ _ThrowError("Can't compute PV info for vg %s", vg_name)
pvs_info.sort()
pvs_info.reverse()
pvlist = [ pv[1] for pv in pvs_info ]
+ if utils.any(pvlist, lambda v: ":" in v):
+ _ThrowError("Some of your PVs have invalid character ':'"
+ " in their name")
free_size = sum([ pv[0] for pv in pvs_info ])
+ current_pvs = len(pvlist)
+ stripes = min(current_pvs, constants.LVM_STRIPECOUNT)
# 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)
+ _ThrowError("Not enough free space: required %s,"
+ " available %s", size, free_size)
+ cmd = ["lvcreate", "-L%dm" % size, "-n%s" % lv_name]
+ # 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
+ 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)
@staticmethod
- def GetPVInfo(vg_name):
+ def GetPVInfo(vg_names, filter_allocatable=True):
"""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
- Returns:
- list of (free_space, name) with free_space in mebibytes
+ @rtype: list
+ @return: list of tuples (free_space, name) with free_space in mebibytes
"""
+ sep = "|"
command = ["pvs", "--noheadings", "--nosuffix", "--units=m",
"-opv_name,vg_name,pv_free,pv_attr", "--unbuffered",
- "--separator=:"]
+ "--separator=%s" % sep ]
result = utils.RunCmd(command)
if result.failed:
- logger.Error("Can't get the PV information: %s" % result.fail_reason)
+ logging.error("Can't get the PV information: %s - %s",
+ result.fail_reason, result.output)
return None
data = []
for line in result.stdout.splitlines():
- fields = line.strip().split(':')
+ fields = line.strip().split(sep)
if len(fields) != 4:
- logger.Error("Can't parse pvs output: line '%s'" % line)
+ logging.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':
+ # (possibly) skip over pvs which are not allocatable
+ if filter_allocatable and fields[3][0] != 'a':
+ continue
+ # (possibly) skip over pvs which are not in the right volume group(s)
+ if vg_names and fields[1] not in vg_names:
continue
- data.append((float(fields[2]), fields[0]))
+ data.append((float(fields[2]), fields[0], fields[1]))
return data
"""
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)
-
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=m", "--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" %
- (self.dev_path, result.fail_reason))
+ 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 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
+ pass
+
+ def GetSyncStatus(self):
+ """Returns the sync status of the device.
- def GetStatus(self):
- """Return the status of the device.
+ If this device is a mirroring device, this function returns the
+ status of the mirror.
+
+ 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
+ parameter.
+
+ For the ldisk parameter, we check if the logical volume has the
+ 'virtual' type, which means it's not backed by existing storage
+ anymore (read from it return I/O error). This happens after a
+ physical disk failure and subsequent 'vgreduce --removemissing' on
+ the volume group.
- 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.
+ 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 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
+ if self._degraded:
+ ldisk_status = constants.LDS_FAULTY
else:
- retval = self.STATUS_EXISTING
+ ldisk_status = constants.LDS_OKAY
- return retval
+ 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.
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)
+ _IgnoreError(snap.Remove)
- pvs_info = self.GetPVInfo(self._vg_name)
+ 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)
+ _ThrowError("Can't compute PV info for vg %s", self._vg_name)
pvs_info.sort()
pvs_info.reverse()
- free_size, pv_name = pvs_info[0]
+ free_size, _, _ = pvs_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))
+ _ThrowError("command: %s error: %s - %s",
+ result.cmd, result.fail_reason, result.output)
return snap_name
result = utils.RunCmd(["lvchange", "--addtag", text,
self.dev_path])
if result.failed:
- raise errors.BlockDeviceError("Command: %s error: %s" %
- (result.cmd, result.fail_reason))
-
-
-class MDRaid1(BlockDev):
- """raid1 device implemented via md.
-
- """
- 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)
- else:
- self.minor = None
- self.dev_path = None
-
- return (minor is not None)
-
- @staticmethod
- def _GetUsedDevs():
- """Compute the list of in-use MD devices.
-
- It doesn't matter if the used device have other raid level, just
- that they are in use.
-
- """
- mdstat = open("/proc/mdstat", "r")
- data = mdstat.readlines()
- mdstat.close()
-
- 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
-
- return used_md
-
- @staticmethod
- def _GetDevInfo(minor):
- """Get info about a MD device.
-
- Currently only uuid is returned.
-
- """
- 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)
+ _ThrowError("Command: %s error: %s - %s", result.cmd, result.fail_reason,
+ result.output)
- 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
- 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.
+ def Grow(self, amount):
+ """Grow the logical volume.
"""
- self.Attach()
- if self.minor is None:
- retval = self.STATUS_UNKNOWN
- else:
- retval = self.STATUS_ONLINE
- return retval
+ 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
+ rest = amount % full_stripe_size
+ if rest != 0:
+ amount += full_stripe_size - rest
+ # 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(["lvextend", "--alloc", alloc_policy,
+ "-L", "+%dm" % amount, self.dev_path])
+ if not result.failed:
+ return
+ _ThrowError("Can't grow LV %s: %s", self.dev_path, result.output)
- def _SetFromMinor(self, minor):
- """Set our parameters based on the given minor.
- This sets our minor variable and our dev_path.
+class DRBD8Status(object):
+ """A DRBD status representation class.
- """
- self.minor = minor
- self.dev_path = "/dev/md%d" % minor
+ Note that this doesn't support unconfigured devices (cs:Unconfigured).
- 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
+ """
+ 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.]+)%.*"
+ "\sfinish: ([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 = frozenset([
+ 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 = 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)
-
- 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 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
- 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
- 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
+ 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:
- time_est = (sync_total - sync_done) / 2 / sync_speed_k
- return sync_percent, time_est, not is_clean
-
- 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
+ # 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
-class BaseDRBD(BlockDev):
+class BaseDRBD(BlockDev): # pylint: disable-msg=W0223
"""Base DRBD class.
This class contains a few bits of common functionality between the
"""
_VERSION_RE = re.compile(r"^version: (\d+)\.(\d+)\.(\d+)"
- r" \(api:(\d+)/proto:(\d+)\)")
+ r" \(api:(\d+)/proto:(\d+)(?:-(\d+))?\)")
+ _VALID_LINE_RE = re.compile("^ *([0-9]+): cs:([^ ]+).*$")
+ _UNUSED_LINE_RE = re.compile("^ *([0-9]+): cs:Unconfigured$")
+
_DRBD_MAJOR = 147
_ST_UNCONFIGURED = "Unconfigured"
_ST_WFCONNECTION = "WFConnection"
_ST_CONNECTED = "Connected"
+ _STATUS_FILE = "/proc/drbd"
+
@staticmethod
- def _GetProcData():
+ def _GetProcData(filename=_STATUS_FILE):
"""Return data from /proc/drbd.
"""
- stat = open("/proc/drbd", "r")
try:
- data = stat.read().splitlines()
- finally:
- stat.close()
+ 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:
+ _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
def _GetVersion(cls):
"""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()
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 _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)
-
- @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
- 128MiB.
-
- """
- result = utils.RunCmd(["blockdev", "--getsize", meta_device])
- if result.failed:
- logger.Error("Failed to get device size: %s" % result.fail_reason)
- return False
- 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
-
- def Rename(self, new_id):
- """Rename a device.
-
- This is not supported for drbd devices.
-
- """
- raise errors.ProgrammerError("Can't rename a drbd device")
-
-
-class DRBDev(BaseDRBD):
- """DRBD 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.
-
- """
- 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))
-
- if len(children) != 2:
- raise ValueError("Invalid configuration data %s" % str(children))
- if not isinstance(unique_id, (tuple, list)) or len(unique_id) != 4:
- raise ValueError("Invalid configuration data %s" % str(unique_id))
- self._lhost, self._lport, self._rhost, self._rport = unique_id
- self.Attach()
-
- @classmethod
- def _FindUnusedMinor(cls):
- """Find an unused DRBD device.
-
- """
- data = cls._GetProcData()
-
- valid_line = re.compile("^ *([0-9]+): cs:Unconfigured$")
- for line in data:
- match = valid_line.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")
-
- @classmethod
- def _GetDevInfo(cls, minor):
- """Get details about a given DRBD minor.
-
- This return, if available, the local backing device in (major,
- minor) formant and the local and remote (ip, port) information.
-
- """
- 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
-
- 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 not ("local_dev" in info and "meta_dev" in info and
- "meta_index" in info):
- return False
-
- 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
-
- 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
- "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
-
- @classmethod
- def _AssembleLocal(cls, minor, backend, meta):
- """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
-
- @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:
- logger.Error("Can't detach local device: %s" % result.output)
- return not result.failed
-
- @staticmethod
- def _ShutdownAll(minor):
- """Deactivate the device.
-
- This will, of course, fail if the device is in use.
-
- """
- 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
-
- @classmethod
- def _AssembleNet(cls, minor, net_info, protocol):
- """Configure the network part of the device.
-
- 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.
-
- """
- lhost, lport, rhost, rport = net_info
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "net",
- "%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport),
- protocol])
- if result.failed:
- logger.Error("Can't setup network for dbrd device: %s" %
- result.fail_reason)
- return False
-
- 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
-
- @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"])
- logger.Error("Can't shutdown network: %s" % result.output)
- return not result.failed
-
- def Assemble(self):
- """Assemble the drbd.
-
- 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
-
- 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
-
- def Shutdown(self):
- """Shutdown the DRBD device.
-
- """
- 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
-
- def Attach(self):
- """Find a DRBD device which matches our config and attach to it.
-
- 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.
-
- """
- 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
- else:
- minor = None
-
- self._SetFromMinor(minor)
- return minor is not 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.
-
- """
- 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("--do-what-I-say")
- result = utils.RunCmd(cmd)
- if result.failed:
- logger.Error("Can't make drbd device primary: %s" % result.output)
- return False
- return True
-
- def Close(self):
- """Make the local state secondary.
-
- This will, of course, fail if the device is in use.
-
- """
- 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")
-
- def SetSyncSpeed(self, kbytes):
- """Set the speed of the DRBD syncer.
-
- """
- 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
-
- def GetSyncStatus(self):
- """Returns the sync status of the device.
-
- Returns:
- (sync_percent, estimated_time)
-
- 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 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
-
- def GetStatus(self):
- """Compute the status of the DRBD device
-
- Note that DRBD devices don't have the STATUS_EXISTING state.
-
- """
- 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
-
- state = mresult.group(2)
- if state == "Primary":
- result = self.STATUS_ONLINE
- else:
- result = self.STATUS_STANDBY
-
- return result
+ self.minor = minor
+ self.dev_path = self._DevPath(minor)
+ self.attached = True
@staticmethod
- def _ZeroDevice(device):
- """Zero a device.
+ def _CheckMetaSize(meta_device):
+ """Check if the given meta device looks like a valid one.
- This writes until we get ENOSPC.
+ This currently only check the size, which must be around
+ 128MiB.
"""
- f = open(device, "w")
- buf = "\0" * 1048576
+ result = utils.RunCmd(["blockdev", "--getsize", meta_device])
+ if result.failed:
+ _ThrowError("Failed to get device size: %s - %s",
+ result.fail_reason, result.output)
try:
- while True:
- f.write(buf)
- except IOError, err:
- if err.errno != errno.ENOSPC:
- raise
-
- @classmethod
- def Create(cls, unique_id, children, size):
- """Create a new DRBD device.
-
- Since DRBD devices are not created per se, just assembled, this
- function just zeroes the meta device.
+ sectors = int(result.stdout)
+ except (TypeError, ValueError):
+ _ThrowError("Invalid output from blockdev: '%s'", result.stdout)
+ bytes = sectors * 512
+ if bytes < 128 * 1024 * 1024: # less than 128MiB
+ _ThrowError("Meta device too small (%.2fMib)", (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 bytes > 1024 * 1024 * 1024:
+ _ThrowError("Meta device too big (%.2fMiB)", (bytes / 1024 / 1024))
- """
- 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")
- 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)
+ def Rename(self, new_id):
+ """Rename a device.
- def Remove(self):
- """Stub remove for DRBD devices.
+ This is not supported for drbd devices.
"""
- return self.Shutdown()
+ raise errors.ProgrammerError("Can't rename a drbd device")
class DRBD8(BaseDRBD):
_MAX_MINORS = 255
_PARSE_SHOW = None
- def __init__(self, unique_id, 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))
+ # timeout constants
+ _NET_RECONFIG_TIMEOUT = 60
+ def __init__(self, unique_id, children, size):
+ 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)
+ self.major = self._DRBD_MAJOR
+ version = self._GetVersion()
+ 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
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)
+ _ThrowError("Can't initialize meta device: %s", result.output)
@classmethod
def _FindUnusedMinor(cls):
"""
data = cls._GetProcData()
- 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)
+ match = cls._UNUSED_LINE_RE.match(line)
if match:
return int(match.group(1))
- match = used_line.match(line)
+ 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:
- logger.Error("Error: no free drbd minors!")
+ logging.error("Error: no free drbd minors!")
raise errors.BlockDeviceError("Can't find a free DRBD minor")
return highest + 1
@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))
- return False
- return True
-
- @classmethod
def _GetShowParser(cls):
"""Return a parser for `drbd show` output.
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])]))
+ number = pyp.Word(pyp.nums).setParseAction(lambda s, l, t: int(t[0]))
comment = pyp.Literal ("#") + pyp.Optional(pyp.restOfLine)
defa = pyp.Literal("_is_default").suppress()
# value types
value = pyp.Word(pyp.alphanums + '_-/.:')
quoted = dbl_quote + pyp.CharsNotIn('"') + dbl_quote
- addr_port = (pyp.Word(pyp.nums + '.') + pyp.Literal(':').suppress() +
- number)
+ addr_type = (pyp.Optional(pyp.Literal("ipv4")).suppress() +
+ pyp.Optional(pyp.Literal("ipv6")).suppress())
+ addr_port = (addr_type + pyp.Word(pyp.nums + '.') +
+ pyp.Literal(':').suppress() + number)
# meta device, extended syntax
meta_value = ((value ^ quoted) + pyp.Literal('[').suppress() +
number + pyp.Word(']').suppress())
+ # device name, extended syntax
+ device_value = pyp.Literal("minor").suppress() + number
# a statement
stmt = (~rbrace + keyword + ~lbrace +
- (addr_port ^ value ^ quoted ^ meta_value) +
+ pyp.Optional(addr_port ^ value ^ quoted ^ meta_value ^
+ device_value) +
pyp.Optional(defa) + semi +
pyp.Optional(pyp.restOfLine).suppress())
return bnf
@classmethod
- def _GetDevInfo(cls, minor):
- """Get details about a given DRBD minor.
+ 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.
+ and the local and remote (ip, port) information from a string
+ containing the output of the `drbdsetup show` command as returned
+ by _GetShowData.
"""
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 not out:
return data
try:
results = bnf.parseString(out)
except pyp.ParseException, err:
- raise errors.BlockDeviceError("Can't parse drbdsetup show output: %s" %
- str(err))
+ _ThrowError("Can't parse drbdsetup show output: %s", str(err))
# and massage the results into our desired format
for section in results:
return retval
@classmethod
- def _AssembleLocal(cls, minor, backend, meta):
+ def _AssembleLocal(cls, 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._IsValidMeta(meta):
- return False
- result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disk",
- backend, meta, "0", "-e", "detach",
- "--create-device"])
+ args = ["drbdsetup", cls._DevPath(minor), "disk",
+ backend, meta, "0",
+ "-e", "detach",
+ "--create-device"]
+ if size:
+ args.extend(["-d", "%sm" % size])
+ if not constants.DRBD_BARRIERS: # disable barriers, if configured so
+ version = cls._GetVersion()
+ # various DRBD versions support different disk barrier options;
+ # what we aim here is to revert back to the 'drain' method of
+ # disk flushes and to disable metadata barriers, in effect going
+ # back to pre-8.0.7 behaviour
+ vmaj = version['k_major']
+ vmin = version['k_minor']
+ vrel = version['k_point']
+ assert vmaj == 8
+ if vmin == 0: # 8.0.x
+ if vrel >= 12:
+ args.extend(['-i', '-m'])
+ elif vmin == 2: # 8.2.x
+ if vrel >= 7:
+ args.extend(['-i', '-m'])
+ elif vmaj >= 3: # 8.3.x or newer
+ args.extend(['-i', '-a', 'm'])
+ result = utils.RunCmd(args)
if result.failed:
- logger.Error("Can't attach local disk: %s" % result.output)
- return not result.failed
+ _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output)
@classmethod
def _AssembleNet(cls, minor, net_info, protocol,
"""
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
+ cls._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.
+ cls._SetMinorSyncSpeed(minor, constants.SYNC_SPEED)
+
args = ["drbdsetup", cls._DevPath(minor), "net",
"%s:%s" % (lhost, lport), "%s:%s" % (rhost, rport), protocol,
"-A", "discard-zero-changes",
"-B", "consensus",
+ "--create-device",
]
if dual_pri:
args.append("-m")
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
+ _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 = cls._GetDevInfo(cls._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()
+
+ try:
+ utils.Retry(_CheckNetworkConfig, 1.0, 10.0)
+ except utils.RetryTimeout:
+ _ThrowError("drbd%d: timeout while configuring network", minor)
def AddChildren(self, devices):
"""Add a disk to the DRBD device.
"""
if self.minor is None:
- raise errors.BlockDeviceError("Can't attach to dbrd8 during AddChildren")
-
+ _ThrowError("drbd%d: can't attach to dbrd8 during AddChildren",
+ self._aminor)
if len(devices) != 2:
- raise errors.BlockDeviceError("Need two devices for AddChildren")
- if self._children:
- raise errors.BlockDeviceError("DRBD8 already attached to a local disk")
+ _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:
- raise errors.BlockDeviceError("Children not ready during AddChildren")
+ _ThrowError("drbd%d: children not ready during AddChildren", self.minor)
backend.Open()
meta.Open()
- if not self._CheckMetaSize(meta.dev_path):
- raise errors.BlockDeviceError("Invalid meta device size")
+ self._CheckMetaSize(meta.dev_path)
self._InitMeta(self._FindUnusedMinor(), meta.dev_path)
- if not self._IsValidMeta(meta.dev_path):
- raise errors.BlockDeviceError("Cannot initalize meta device")
- if not self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path):
- raise errors.BlockDeviceError("Can't attach to local storage")
+ self._AssembleLocal(self.minor, backend.dev_path, meta.dev_path, self.size)
self._children = devices
def RemoveChildren(self, devices):
"""
if self.minor is None:
- raise errors.BlockDeviceError("Can't attach to drbd8 during"
- " RemoveChildren")
+ _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:
- raise errors.BlockDeviceError("We don't have two children: %s" %
- self._children)
+ _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 :)
- logger.Error("Requested detach while detached")
+ logging.warning("drbd%d: requested detach while detached", self.minor)
return
if len(devices) != 2:
- raise errors.BlockDeviceError("We need two children in RemoveChildren")
+ _ThrowError("drbd%d: we need two children in RemoveChildren", self.minor)
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))
+ _ThrowError("drbd%d: mismatch in local storage (%s != %s) in"
+ " RemoveChildren", self.minor, dev, child.dev_path)
- if not self._ShutdownLocal(self.minor):
- raise errors.BlockDeviceError("Can't detach from local storage")
+ self._ShutdownLocal(self.minor)
self._children = []
+ @classmethod
+ def _SetMinorSyncSpeed(cls, minor, kbytes):
+ """Set the speed of the DRBD syncer.
+
+ This is the low-level implementation.
+
+ @type minor: int
+ @param minor: the drbd minor whose settings we change
+ @type kbytes: int
+ @param kbytes: the speed in kbytes/second
+ @rtype: boolean
+ @return: the success of the operation
+
+ """
+ result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "syncer",
+ "-r", "%d" % kbytes, "--create-device"])
+ if result.failed:
+ logging.error("Can't change syncer rate: %s - %s",
+ result.fail_reason, result.output)
+ return not result.failed
+
def SetSyncSpeed(self, kbytes):
"""Set the speed of the DRBD syncer.
+ @type kbytes: int
+ @param kbytes: the speed in kbytes/second
+ @rtype: boolean
+ @return: the success of the operation
+
"""
- children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
if self.minor is None:
- logger.Info("Instance not attached to a device")
+ logging.info("Not attached during SetSyncSpeed")
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
+ children_result = super(DRBD8, self).SetSyncSpeed(kbytes)
+ return self._SetMinorSyncSpeed(self.minor, kbytes) and children_result
+
+ def GetProcStatus(self):
+ """Return device data from /proc.
+
+ """
+ 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.
- Returns:
- (sync_percent, estimated_time)
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 estimated_time is None, it means we can't estimate
+ the time needed, otherwise it's the time left in seconds.
- """
- 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
- def GetStatus(self):
- """Compute the status of the DRBD device
+ We set the is_degraded parameter to True on two conditions:
+ network not connected or local disk missing.
- Note that DRBD devices don't have the STATUS_EXISTING state.
+ 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():
- return self.STATUS_UNKNOWN
+ _ThrowError("drbd%d: can't Attach() in GetSyncStatus", self._aminor)
- 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
+ stats = self.GetProcStatus()
+ is_degraded = not stats.is_connected or not stats.is_disk_uptodate
- state = mresult.group(2)
- if state == "Primary":
- result = self.STATUS_ONLINE
+ if stats.is_disk_uptodate:
+ ldisk_status = constants.LDS_OKAY
+ elif stats.is_diskless:
+ ldisk_status = constants.LDS_FAULTY
else:
- result = self.STATUS_STANDBY
+ ldisk_status = constants.LDS_UNKNOWN
- return result
+ 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("-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.
+
+ 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.
+
+ """
+ if self.minor is None:
+ _ThrowError("drbd%d: disk not attached in re-attach net", self._aminor)
+
+ if None in (self._lhost, self._lport, self._rhost, self._rport):
+ _ThrowError("drbd%d: DRBD disk missing network info in"
+ " DisconnectNet()", self.minor)
+
+ class _DisconnectStatus:
+ def __init__(self, ever_disconnected):
+ self.ever_disconnected = ever_disconnected
+
+ dstatus = _DisconnectStatus(_IgnoreError(self._ShutdownNet, self.minor))
+
+ def _WaitForDisconnect():
+ if self.GetProcStatus().is_standalone:
+ return
+
+ # 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
+
+ raise utils.RetryAgain()
+
+ # Keep start time
+ start_time = time.time()
+
+ 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"
+
+ _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):
- """Find a DRBD device which matches our config and attach to it.
+ """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:
+ 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
+
+ """
+ super(DRBD8, self).Assemble()
+
+ self.Attach()
+ if self.minor is None:
+ # local device completely unconfigured
+ self._FastAssemble()
+ else:
+ # we have to recheck the local and network status and try to fix
+ # the device
+ self._SlowAssemble()
+
+ def _SlowAssemble(self):
+ """Assembles the DRBD device from a (partially) configured device.
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.
"""
- for minor in self._GetUsedDevs():
- info = self._GetDevInfo(minor)
+ # TODO: Rewrite to not use a for loop just because there is 'break'
+ # pylint: disable-msg=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:
- res_r = self._AssembleNet(minor,
- (self._lhost, self._lport,
- self._rhost, self._rport),
- "C")
- if res_r and self._MatchesNet(self._GetDevInfo(minor)):
+ # 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)
- return minor is not None
+ if minor is None:
+ _ThrowError("drbd%d: cannot activate, unknown or unhandled reason",
+ self._aminor)
- def Assemble(self):
- """Assemble the drbd.
+ def _FastAssemble(self):
+ """Assemble the drbd device from zero.
- 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
+ This is run when in Assemble we detect our minor is unused.
"""
- self.Attach()
- if self.minor is not None:
- logger.Info("Already assembled")
- return True
-
- result = super(DRBD8, 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
+ 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:
- 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._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)
- return True
@classmethod
def _ShutdownLocal(cls, minor):
"""
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
+ _ThrowError("drbd%d: can't detach local disk: %s", minor, result.output)
@classmethod
def _ShutdownNet(cls, minor):
"""
result = utils.RunCmd(["drbdsetup", cls._DevPath(minor), "disconnect"])
- logger.Error("Can't shutdown network: %s" % result.output)
- return not result.failed
+ if result.failed:
+ _ThrowError("drbd%d: can't shutdown network: %s", minor, result.output)
@classmethod
def _ShutdownAll(cls, minor):
"""
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
+ _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():
- logger.Info("DRBD device not attached to a device during Shutdown")
- return True
- if not self._ShutdownAll(self.minor):
- return False
+ logging.info("drbd%d: not attached during Shutdown()", self._aminor)
+ return
+ minor = self.minor
self.minor = None
self.dev_path = None
- return True
-
- def Rename(self, new_uid):
- """Re-connect this device to another peer.
-
- """
- if self.minor is None:
- raise errors.BlockDeviceError("Device not attached during rename")
- if self._rhost is not None:
- # this means we did have a host when we attached, so we are connected
- if not self._ShutdownNet(self.minor):
- raise errors.BlockDeviceError("Can't disconnect from remote peer")
- old_id = self.unique_id
- else:
- old_id = None
- self.unique_id = new_uid
- if not self._AssembleNet(self.minor, self.unique_id, "C"):
- logger.Error("Can't attach to new peer!")
- if old_id is not None:
- self._AssembleNet(self.minor, old_id, "C")
- self.unique_id = old_id
- raise errors.BlockDeviceError("Can't attach to new peer")
+ self._ShutdownAll(minor)
def Remove(self):
"""Stub remove for DRBD devices.
"""
- return self.Shutdown()
+ self.Shutdown()
@classmethod
def Create(cls, unique_id, children, size):
"""
if len(children) != 2:
raise errors.ProgrammerError("Invalid setup for the drbd device")
+ # 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 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)
+ _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)
+
+ def Grow(self, amount):
+ """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)
+ 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):
+ """Initalizes a file device backend.
+
+ """
+ if children:
+ raise errors.BlockDeviceError("Invalid setup for file device")
+ super(FileStorage, self).__init__(unique_id, children, size)
+ 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]
+ 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):
+ """Remove the file backing the block device.
+
+ @rtype: boolean
+ @return: True if the removal was successful
+
+ """
+ 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.
+
+ """
+ # TODO: implement rename for file-based storage
+ _ThrowError("Rename is not supported for file-based storage")
+
+ def Grow(self, amount):
+ """Grow the file
+
+ @param amount: the amount (in mebibytes) to grow with
+
+ """
+ # TODO: implement grow for file-based storage
+ _ThrowError("Grow not supported for file-based storage")
+
+ 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.
+
+ @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):
+ """Create a new file.
+
+ @param size: the size of file in MiB
+
+ @rtype: L{bdev.FileStorage}
+ @return: an instance of FileStorage
+
+ """
+ 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]
+ if os.path.exists(dev_path):
+ _ThrowError("File already existing: %s", dev_path)
+ try:
+ f = open(dev_path, 'w')
+ f.truncate(size * 1024 * 1024)
+ f.close()
+ except IOError, err:
+ _ThrowError("Error in file creation: %", str(err))
+
+ return FileStorage(unique_id, children, size)
DEV_MAP = {
constants.LD_LV: LogicalVolume,
- constants.LD_MD_R1: MDRaid1,
- constants.LD_DRBD7: DRBDev,
constants.LD_DRBD8: DRBD8,
+ constants.LD_FILE: FileStorage,
}
-def FindDevice(dev_type, unique_id, children):
+def FindDevice(dev_type, unique_id, children, size):
"""Search for an existing, assembled device.
This will succeed only if the device exists and is assembled, but it
"""
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 = DEV_MAP[dev_type](unique_id, children, size)
+ if not device.attached:
return None
- return device
+ return device
-def AttachOrAssemble(dev_type, unique_id, children):
+def Assemble(dev_type, unique_id, children, size):
"""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.
"""
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))
+ device = DEV_MAP[dev_type](unique_id, children, size)
+ device.Assemble()
return device