From 89ff748deb737ce7b25f73a5857b904ca47e37e7 Mon Sep 17 00:00:00 2001 From: Thomas Thrainer <thomasth@google.com> Date: Tue, 23 Apr 2013 14:04:23 +0200 Subject: [PATCH] Extract DRBD-related classes into block/drbd.py This is in preparation to introducing support for DRBD8.4. base.py had to be extracted as well in order to avoid cylic imports between bdev.py and drbd.py. It now contains the BlockDev class and utility functions needed by bdev.py and drbd.py. Signed-off-by: Thomas Thrainer <thomasth@google.com> Signed-off-by: Michele Tartara <mtartara@google.com> Reviewed-by: Michele Tartara <mtartara@google.com> --- Makefile.am | 4 +- lib/backend.py | 10 +- lib/block/base.py | 349 +++++ lib/block/bdev.py | 1785 +------------------------ lib/block/drbd.py | 1370 +++++++++++++++++++ lib/bootstrap.py | 4 +- lib/watcher/nodemaint.py | 6 +- test/py/ganeti.block.bdev_unittest.py | 73 +- 8 files changed, 1843 insertions(+), 1758 deletions(-) create mode 100644 lib/block/base.py create mode 100644 lib/block/drbd.py diff --git a/Makefile.am b/Makefile.am index 8f979c73a..a6557f37b 100644 --- a/Makefile.am +++ b/Makefile.am @@ -317,7 +317,9 @@ hypervisor_PYTHON = \ block_PYTHON = \ lib/block/__init__.py \ - lib/block/bdev.py + lib/block/bdev.py \ + lib/block/base.py \ + lib/block/drbd.py rapi_PYTHON = \ lib/rapi/__init__.py \ diff --git a/lib/backend.py b/lib/backend.py index c4f8d82f5..3e1611daf 100644 --- a/lib/backend.py +++ b/lib/backend.py @@ -55,6 +55,7 @@ from ganeti import ssh from ganeti import hypervisor from ganeti import constants from ganeti.block import bdev +from ganeti.block import drbd from ganeti import objects from ganeti import ssconf from ganeti import serializer @@ -65,6 +66,7 @@ from ganeti import compat from ganeti import pathutils from ganeti import vcluster from ganeti import ht +from ganeti.block.base import BlockDev _BOOT_ID_PATH = "/proc/sys/kernel/random/boot_id" @@ -830,7 +832,7 @@ def VerifyNode(what, cluster_name): if constants.NV_DRBDLIST in what and vm_capable: try: - used_minors = bdev.DRBD8.GetUsedDevs().keys() + used_minors = drbd.DRBD8.GetUsedDevs().keys() except errors.BlockDeviceError, err: logging.warning("Can't get used minors list", exc_info=True) used_minors = str(err) @@ -839,7 +841,7 @@ def VerifyNode(what, cluster_name): if constants.NV_DRBDHELPER in what and vm_capable: status = True try: - payload = bdev.BaseDRBD.GetUsermodeHelper() + payload = drbd.BaseDRBD.GetUsermodeHelper() except errors.BlockDeviceError, err: logging.error("Can't get DRBD usermode helper: %s", str(err)) status = False @@ -1872,7 +1874,7 @@ def BlockdevAssemble(disk, owner, as_primary, idx): """ try: result = _RecursiveAssembleBD(disk, owner, as_primary) - if isinstance(result, bdev.BlockDev): + if isinstance(result, BlockDev): # pylint: disable=E1103 result = result.dev_path if as_primary: @@ -3669,7 +3671,7 @@ def GetDrbdUsermodeHelper(): """ try: - return bdev.BaseDRBD.GetUsermodeHelper() + return drbd.BaseDRBD.GetUsermodeHelper() except errors.BlockDeviceError, err: _Fail(str(err)) diff --git a/lib/block/base.py b/lib/block/base.py new file mode 100644 index 000000000..7e6449e1e --- /dev/null +++ b/lib/block/base.py @@ -0,0 +1,349 @@ +# +# + +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""Block device abstraction - base class and utility functions""" + +import logging + +from ganeti import objects +from ganeti import constants +from ganeti import utils +from ganeti import errors + + +class BlockDev(object): + """Block device abstract class. + + A block device can be in the following states: + - not existing on the system, and by `Create()` it goes into: + - existing but not setup/not active, and by `Assemble()` goes into: + - active read-write and by `Open()` it goes into + - online (=used, or ready for use) + + A device can also be online but read-only, however we are not using + 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 + - drbd devices are attached to a local disk/remote peer and made primary + + A block device is identified by three items: + - the /dev path of the device (dynamic) + - a unique ID of the device (static) + - it's major/minor pair (dynamic) + + 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 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) + - attaching of a python instance to an existing (real) device + + 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. + + """ + 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. + + 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 + + """ + pass + + def Attach(self): + """Find a device which matches our config and attach to it. + + """ + raise NotImplementedError + + def Close(self): + """Notifies that the device will no longer be used for I/O. + + """ + raise NotImplementedError + + @classmethod + def Create(cls, unique_id, children, size, params, excl_stor): + """Create the device. + + If the device cannot be created, it will return None + instead. Error messages go to the logging system. + + Note that for some devices, the unique_id is used, and for other, + the children. The idea is that these two, taken together, are + enough for both creation and assembly (later). + + """ + raise NotImplementedError + + def Remove(self): + """Remove this device. + + 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 + + def Rename(self, new_id): + """Rename this device. + + This may or may not make sense for a given device type. + + """ + raise NotImplementedError + + def Open(self, force=False): + """Make the device ready for use. + + This makes the device ready for I/O. For now, just the DRBD + devices need this. + + The force parameter signifies that if the device has any kind of + --force thing, it should be used, we know what we are doing. + + """ + raise NotImplementedError + + def Shutdown(self): + """Shut down the device, freeing its children. + + This undoes the `Assemble()` work, except for the child + assembling; as such, the children on the device are still + assembled after this call. + + """ + raise NotImplementedError + + 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.PauseResumeSync(pause) + return result + + 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. + + If sync_percent is None, it means the device is not syncing. + + If estimated_time is None, it means we can't estimate + 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. + + 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. + + The return value is the same as for `GetSyncStatus()` except the + minimum percent and maximum time are calculated across our + children. + + @rtype: objects.BlockDevStatus + + """ + 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: + child_status = child.GetSyncStatus() + + if min_percent is None: + 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 = 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. + + Only supported for some device types. + + """ + 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>" % + (self.__class__, self.unique_id, self._children, + self.major, self.minor, self.dev_path)) + + +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 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 diff --git a/lib/block/bdev.py b/lib/block/bdev.py index 649d92ec6..773a6d48d 100644 --- a/lib/block/bdev.py +++ b/lib/block/bdev.py @@ -22,11 +22,8 @@ """Block device abstraction""" import re -import time import errno -import shlex import stat -import pyparsing as pyp import os import logging import math @@ -36,13 +33,10 @@ 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 +from ganeti.block import drbd +from ganeti.block import base class RbdShowmappedJsonError(Exception): @@ -52,38 +46,6 @@ class RbdShowmappedJsonError(Exception): 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. @@ -91,22 +53,8 @@ def _CheckResult(result): """ 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 + base.ThrowError("Command: %s error: %s - %s", + result.cmd, result.fail_reason, result.output) def _GetForbiddenFileStoragePaths(): @@ -219,295 +167,7 @@ def CheckFileStoragePath(path, _filename=pathutils.FILE_STORAGE_PATHS_FILE): _CheckFileStoragePath(path, allowed) -class BlockDev(object): - """Block device abstract class. - - A block device can be in the following states: - - not existing on the system, and by `Create()` it goes into: - - existing but not setup/not active, and by `Assemble()` goes into: - - active read-write and by `Open()` it goes into - - online (=used, or ready for use) - - A device can also be online but read-only, however we are not using - 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 - - drbd devices are attached to a local disk/remote peer and made primary - - A block device is identified by three items: - - the /dev path of the device (dynamic) - - a unique ID of the device (static) - - it's major/minor pair (dynamic) - - 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 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) - - attaching of a python instance to an existing (real) device - - 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. - - """ - 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. - - 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 - - """ - pass - - def Attach(self): - """Find a device which matches our config and attach to it. - - """ - raise NotImplementedError - - def Close(self): - """Notifies that the device will no longer be used for I/O. - - """ - raise NotImplementedError - - @classmethod - def Create(cls, unique_id, children, size, params, excl_stor): - """Create the device. - - If the device cannot be created, it will return None - instead. Error messages go to the logging system. - - Note that for some devices, the unique_id is used, and for other, - the children. The idea is that these two, taken together, are - enough for both creation and assembly (later). - - """ - raise NotImplementedError - - def Remove(self): - """Remove this device. - - 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 - - def Rename(self, new_id): - """Rename this device. - - This may or may not make sense for a given device type. - - """ - raise NotImplementedError - - def Open(self, force=False): - """Make the device ready for use. - - This makes the device ready for I/O. For now, just the DRBD - devices need this. - - The force parameter signifies that if the device has any kind of - --force thing, it should be used, we know what we are doing. - - """ - raise NotImplementedError - - def Shutdown(self): - """Shut down the device, freeing its children. - - This undoes the `Assemble()` work, except for the child - assembling; as such, the children on the device are still - assembled after this call. - - """ - raise NotImplementedError - - 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.PauseResumeSync(pause) - return result - - 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. - - If sync_percent is None, it means the device is not syncing. - - If estimated_time is None, it means we can't estimate - 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. - - 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. - - The return value is the same as for `GetSyncStatus()` except the - minimum percent and maximum time are calculated across our - children. - - @rtype: objects.BlockDevStatus - - """ - 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: - child_status = child.GetSyncStatus() - - if min_percent is None: - 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 = 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. - - Only supported for some device types. - - """ - 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>" % - (self.__class__, self.unique_id, self._children, - self.major, self.minor, self.dev_path)) - - -class LogicalVolume(BlockDev): +class LogicalVolume(base.BlockDev): """Logical Volume block device. """ @@ -586,14 +246,14 @@ class LogicalVolume(BlockDev): msg = "No (empty) PVs found" else: msg = "Can't compute PV info for vg %s" % vg_name - _ThrowError(msg) + base.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'") + base.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] @@ -608,8 +268,8 @@ class LogicalVolume(BlockDev): 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) + base.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 @@ -623,8 +283,8 @@ class LogicalVolume(BlockDev): # 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) + base.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 @@ -636,8 +296,8 @@ class LogicalVolume(BlockDev): if not result.failed: break if result.failed: - _ThrowError("LV create failed (%s): %s", - result.fail_reason, result.output) + base.ThrowError("LV create failed (%s): %s", + result.fail_reason, result.output) return LogicalVolume(unique_id, children, size, params) @staticmethod @@ -795,7 +455,7 @@ class LogicalVolume(BlockDev): 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) + base.ThrowError("Invalid LVM name '%s'", name) def Remove(self): """Remove this logical volume. @@ -807,7 +467,8 @@ class LogicalVolume(BlockDev): result = utils.RunCmd(["lvremove", "-f", "%s/%s" % (self._vg_name, self._lv_name)]) if result.failed: - _ThrowError("Can't lvremove: %s - %s", result.fail_reason, result.output) + base.ThrowError("Can't lvremove: %s - %s", + result.fail_reason, result.output) def Rename(self, new_id): """Rename this logical volume. @@ -822,7 +483,7 @@ class LogicalVolume(BlockDev): (self._vg_name, new_vg)) result = utils.RunCmd(["lvrename", new_vg, self._lv_name, new_name]) if result.failed: - _ThrowError("Failed to rename the logical volume: %s", result.output) + base.ThrowError("Failed to rename the logical volume: %s", result.output) self._lv_name = new_name self.dev_path = utils.PathJoin("/dev", self._vg_name, self._lv_name) @@ -901,7 +562,7 @@ class LogicalVolume(BlockDev): """ result = utils.RunCmd(["lvchange", "-ay", self.dev_path]) if result.failed: - _ThrowError("Can't activate lv %s: %s", self.dev_path, result.output) + base.ThrowError("Can't activate lv %s: %s", self.dev_path, result.output) def Shutdown(self): """Shutdown the device. @@ -973,15 +634,15 @@ class LogicalVolume(BlockDev): # remove existing snapshot if found snap = LogicalVolume((self._vg_name, snap_name), None, size, self.params) - _IgnoreError(snap.Remove) + base.IgnoreError(snap.Remove) vg_info = self.GetVGInfo([self._vg_name], False) if not vg_info: - _ThrowError("Can't compute VG info for vg %s", self._vg_name) + base.ThrowError("Can't compute VG info for vg %s", self._vg_name) free_size, _, _ = vg_info[0] if free_size < size: - _ThrowError("Not enough free space: required %s," - " available %s", size, free_size) + base.ThrowError("Not enough free space: required %s," + " available %s", size, free_size) _CheckResult(utils.RunCmd(["lvcreate", "-L%dm" % size, "-s", "-n%s" % snap_name, self.dev_path])) @@ -1006,7 +667,7 @@ class LogicalVolume(BlockDev): """Update metadata with info text. """ - BlockDev.SetInfo(self, text) + base.BlockDev.SetInfo(self, text) self._RemoveOldInfo() @@ -1027,7 +688,7 @@ class LogicalVolume(BlockDev): 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()") + base.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: @@ -1043,1310 +704,10 @@ class LogicalVolume(BlockDev): 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). - - """ - 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: - 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 - - -class BaseDRBD(BlockDev): # pylint: disable=W0223 - """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+)(?:\.\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 = constants.DRBD_STATUS_FILE - _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" - - @staticmethod - def _GetProcData(filename=_STATUS_FILE): - """Return data from /proc/drbd. - - """ - try: - 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: - _ThrowError("Can't read any data from %s", filename) - return data - - @classmethod - def _MassageProcData(cls, data): - """Transform the output of _GetProdData into a nicer form. - - @return: a dictionary of minor: joined lines from /proc/drbd - for that minor - - """ - results = {} - old_minor = old_line = None - for line in data: - 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 - old_minor = int(lresult.group(1)) - old_line = line - else: - if old_minor is not None: - old_line += " " + line.strip() - # add last line - if old_minor is not None: - results[old_minor] = old_line - return results + base.ThrowError("Can't grow LV %s: %s", self.dev_path, result.output) - @classmethod - def _GetVersion(cls, proc_data): - """Return the DRBD version. - - This will return a dict with keys: - - k_major - - k_minor - - k_point - - api - - proto - - proto2 (only on drbd > 8.2.X) - - """ - 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) - - 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 the path to a drbd device for a given minor. - - """ - return "/dev/drbd%d" % minor - - @classmethod - def GetUsedDevs(cls): - """Compute the list of used DRBD devices. - - """ - data = cls._GetProcData() - - used_devs = {} - for line in data: - match = cls._VALID_LINE_RE.match(line) - if not match: - continue - minor = int(match.group(1)) - state = match.group(2) - if state == cls._ST_UNCONFIGURED: - continue - used_devs[minor] = state, line - - return used_devs - - def _SetFromMinor(self, minor): - """Set our parameters based on the given minor. - - This sets our minor variable and our dev_path. - - """ - 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 checks the size, which must be around - 128MiB. - - """ - result = utils.RunCmd(["blockdev", "--getsize", meta_device]) - if result.failed: - _ThrowError("Failed to get device size: %s - %s", - result.fail_reason, result.output) - try: - sectors = int(result.stdout) - 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. - - This is not supported for drbd devices. - - """ - raise errors.ProgrammerError("Can't rename a drbd 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 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. - - """ - _MAX_MINORS = 255 - _PARSE_SHOW = None - - # 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) != 6: - raise ValueError("Invalid configuration data %s" % str(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() - - highest = None - for line in data: - match = cls._UNUSED_LINE_RE.match(line) - if match: - return int(match.group(1)) - 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 _GetShowParser(cls): - """Return a parser for `drbd show` output. - - This will either create or return an already-created parser for the - output of the command `drbd show`. - - """ - if cls._PARSE_SHOW is not None: - return cls._PARSE_SHOW - - # 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])) - - comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine) - defa = pyp.Literal("_is_default").suppress() - dbl_quote = pyp.Literal('"').suppress() - - keyword = pyp.Word(pyp.alphanums + "-") - - # 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 - - # 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()) - - # an entire section - section_name = pyp.Word(pyp.alphas + "_") - section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace - - bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt)) - bnf.ignore(comment) - - 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 - "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 - - def _AssembleLocal(self, minor, backend, meta, size): - """Configure the local part of a DRBD device. - - """ - args = ["drbdsetup", self._DevPath(minor), "disk", - backend, meta, "0", - "-e", "detach", - "--create-device"] - if size: - args.extend(["-d", "%sm" % size]) - - version = self._GetVersion(self._GetProcData()) - vmaj = version["k_major"] - vmin = version["k_minor"] - vrel = version["k_point"] - - barrier_args = \ - self._ComputeDiskBarrierArgs(vmaj, vmin, vrel, - self.params[constants.LDP_BARRIERS], - self.params[constants.LDP_NO_META_FLUSH]) - args.extend(barrier_args) - - if self.params[constants.LDP_DISK_CUSTOM]: - args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM])) - - result = utils.RunCmd(args) - if result.failed: - _ThrowError("drbd%d: can't attach local disk: %s", minor, result.output) - - @classmethod - def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers, - disable_meta_flush): - """Compute the DRBD command line parameters for disk barriers - - 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. - - """ - 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) - - 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)) - - 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 - 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: - _ThrowError("drbd%d: can't setup network: %s - %s", - minor, result.fail_reason, result.output) - - def _CheckNetworkConfig(): - info = self._GetDevInfo(self._GetShowData(minor)) - if not "local_addr" in info or not "remote_addr" in info: - raise utils.RetryAgain() - - if (info["local_addr"] != (lhost, lport) or - info["remote_addr"] != (rhost, rport)): - 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: - _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: - msg = ("Can't change syncer rate: %s - %s" % - (result.fail_reason, result.output)) - logging.error(msg) - return [msg] - - return [] - - 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 - - """ - if self.minor is None: - err = "Not attached during SetSyncParams" - logging.info(err) - return [err] - - children_result = super(DRBD8, self).SetSyncParams(params) - children_result.extend(self._SetMinorSyncParams(self.minor, params)) - return children_result - - 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: - logging.info("Not attached during PauseSync") - return False - - children_result = super(DRBD8, self).PauseResumeSync(pause) - - 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. - - """ - 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: - ldisk_status = constants.LDS_UNKNOWN - - 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 '-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 - is mandatory. - - """ - if self.minor is None and not self.Attach(): - 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: - _ThrowError("drbd%d: can't make drbd device primary: %s", self.minor, - result.output) - - 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(): - _ThrowError("drbd%d: can't Attach() in Close()", self._aminor) - result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"]) - if result.failed: - _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): - """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 - - anyway, set the device parameters - - """ - 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() - - 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))) - - 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. - - """ - # 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 _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 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(): - _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): +class FileStorage(base.BlockDev): """File device. This class represents the a file storage backend device. @@ -2377,7 +738,7 @@ class FileStorage(BlockDev): """ if not os.path.exists(self.dev_path): - _ThrowError("File device '%s' does not exist" % self.dev_path) + base.ThrowError("File device '%s' does not exist" % self.dev_path) def Shutdown(self): """Shutdown the device. @@ -2415,14 +776,14 @@ class FileStorage(BlockDev): os.remove(self.dev_path) except OSError, err: if err.errno != errno.ENOENT: - _ThrowError("Can't remove file '%s': %s", self.dev_path, err) + base.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") + base.ThrowError("Rename is not supported for file-based storage") def Grow(self, amount, dryrun, backingstore): """Grow the file @@ -2445,7 +806,7 @@ class FileStorage(BlockDev): f.truncate(new_size) f.close() except EnvironmentError, err: - _ThrowError("Error in file growth: %", str(err)) + base.ThrowError("Error in file growth: %", str(err)) def Attach(self): """Attach to an existing file. @@ -2470,7 +831,7 @@ class FileStorage(BlockDev): st = os.stat(self.dev_path) return st.st_size except OSError, err: - _ThrowError("Can't stat %s: %s", self.dev_path, err) + base.ThrowError("Can't stat %s: %s", self.dev_path, err) @classmethod def Create(cls, unique_id, children, size, params, excl_stor): @@ -2499,13 +860,13 @@ class FileStorage(BlockDev): f.close() except EnvironmentError, err: if err.errno == errno.EEXIST: - _ThrowError("File already existing: %s", dev_path) - _ThrowError("Error in file creation: %", str(err)) + base.ThrowError("File already existing: %s", dev_path) + base.ThrowError("Error in file creation: %", str(err)) return FileStorage(unique_id, children, size, params) -class PersistentBlockDevice(BlockDev): +class PersistentBlockDevice(base.BlockDev): """A block device with persistent node May be either directly attached, or exposed through DM (e.g. dm-multipath). @@ -2564,7 +925,7 @@ class PersistentBlockDevice(BlockDev): """Rename this device. """ - _ThrowError("Rename is not supported for PersistentBlockDev storage") + base.ThrowError("Rename is not supported for PersistentBlockDev storage") def Attach(self): """Attach to an existing block device. @@ -2616,10 +977,10 @@ class PersistentBlockDevice(BlockDev): """Grow the logical volume. """ - _ThrowError("Grow is not supported for PersistentBlockDev storage") + base.ThrowError("Grow is not supported for PersistentBlockDev storage") -class RADOSBlockDevice(BlockDev): +class RADOSBlockDevice(base.BlockDev): """A RADOS Block Device (rbd). This class implements the RADOS Block Device for the backend. You need @@ -2661,8 +1022,8 @@ class RADOSBlockDevice(BlockDev): rbd_name, "--size", "%s" % size] result = utils.RunCmd(cmd) if result.failed: - _ThrowError("rbd creation failed (%s): %s", - result.fail_reason, result.output) + base.ThrowError("rbd creation failed (%s): %s", + result.fail_reason, result.output) return RADOSBlockDevice(unique_id, children, size, params) @@ -2684,8 +1045,8 @@ class RADOSBlockDevice(BlockDev): 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) + base.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. @@ -2743,14 +1104,14 @@ class RADOSBlockDevice(BlockDev): 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) + base.ThrowError("rbd map failed (%s): %s", + result.fail_reason, result.output) # 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) + base.ThrowError("rbd map succeeded, but could not find the rbd block" + " device in output of showmapped, for volume: %s", name) # The device was successfully mapped. Return it. return rbd_dev @@ -2792,8 +1153,8 @@ class RADOSBlockDevice(BlockDev): 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) + base.ThrowError("rbd showmapped failed (%s): %s", + result.fail_reason, result.output) return cls._ParseRbdShowmappedPlain(result.output, volume_name) @@ -2815,18 +1176,18 @@ class RADOSBlockDevice(BlockDev): try: devices = serializer.LoadJson(output) except ValueError, err: - _ThrowError("Unable to parse JSON data: %s" % err) + base.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) + base.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) + base.ThrowError("rbd volume %s is mapped more than once", volume_name) rbd_dev = d["device"] @@ -2868,15 +1229,15 @@ class RADOSBlockDevice(BlockDev): # 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) + base.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) + base.ThrowError("rbd volume %s mapped more than once", volume_name) if matched_lines: # rbd block device found. Return it. @@ -2924,8 +1285,8 @@ class RADOSBlockDevice(BlockDev): 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) + base.ThrowError("rbd unmap failed (%s): %s", + result.fail_reason, result.output) def Open(self, force=False): """Make the device ready for I/O. @@ -2952,7 +1313,7 @@ class RADOSBlockDevice(BlockDev): if not backingstore: return if not self.Attach(): - _ThrowError("Can't attach to rbd device during Grow()") + base.ThrowError("Can't attach to rbd device during Grow()") if dryrun: # the rbd tool does not support dry runs of resize operations. @@ -2969,11 +1330,11 @@ class RADOSBlockDevice(BlockDev): rbd_name, "--size", "%s" % new_size] result = utils.RunCmd(cmd) if result.failed: - _ThrowError("rbd resize failed (%s): %s", - result.fail_reason, result.output) + base.ThrowError("rbd resize failed (%s): %s", + result.fail_reason, result.output) -class ExtStorageDevice(BlockDev): +class ExtStorageDevice(base.BlockDev): """A block device provided by an ExtStorage Provider. This class implements the External Storage Interface, which means @@ -3115,7 +1476,7 @@ class ExtStorageDevice(BlockDev): if not backingstore: return if not self.Attach(): - _ThrowError("Can't attach to extstorage device during Grow()") + base.ThrowError("Can't attach to extstorage device during Grow()") if dryrun: # we do not support dry runs of resize operations for now. @@ -3174,7 +1535,7 @@ def _ExtStorageAction(action, unique_id, ext_params, # Create an External Storage instance of type `driver' status, inst_es = ExtStorageFromDisk(driver) if not status: - _ThrowError("%s" % inst_es) + base.ThrowError("%s" % inst_es) # Create the basic environment for the driver's scripts create_env = _ExtStorageEnvironment(unique_id, ext_params, size, @@ -3189,8 +1550,8 @@ def _ExtStorageAction(action, unique_id, ext_params, # 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) + base.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" @@ -3213,9 +1574,9 @@ def _ExtStorageAction(action, unique_id, ext_params, else: lines = result.output[-20:] - _ThrowError("External storage's %s script failed (%s), last" - " lines of output:\n%s", - action, result.fail_reason, "\n".join(lines)) + base.ThrowError("External storage's %s script failed (%s), last" + " lines of output:\n%s", + action, result.fail_reason, "\n".join(lines)) if action == constants.ES_ACTION_ATTACH: return result.stdout @@ -3346,17 +1707,17 @@ def _VolumeLogName(kind, es_name, volume): """ # 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) + base.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) + basename = ("%s-%s-%s-%s.log" % + (kind, es_name, volume, utils.TimestampForFilename())) + return utils.PathJoin(pathutils.LOG_ES_DIR, basename) DEV_MAP = { constants.LD_LV: LogicalVolume, - constants.LD_DRBD8: DRBD8, + constants.LD_DRBD8: drbd.DRBD8, constants.LD_BLOCKDEV: PersistentBlockDevice, constants.LD_RBD: RADOSBlockDevice, constants.LD_EXT: ExtStorageDevice, diff --git a/lib/block/drbd.py b/lib/block/drbd.py new file mode 100644 index 000000000..70d5f0c8c --- /dev/null +++ b/lib/block/drbd.py @@ -0,0 +1,1370 @@ +# +# + +# 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 +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA +# 02110-1301, USA. + + +"""DRBD block device related functionality""" + +import errno +import logging +import pyparsing as pyp +import re +import shlex +import time + +from ganeti import constants +from ganeti import utils +from ganeti import errors +from ganeti import compat +from ganeti import netutils +from ganeti import objects +from ganeti.block import base + + +# Size of reads in _CanReadDevice + +_DEVICE_READ_SIZE = 128 * 1024 + + +class BaseDRBD(base.BlockDev): # pylint: disable=W0223 + """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+)(?:\.\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 = constants.DRBD_STATUS_FILE + _USERMODE_HELPER_FILE = "/sys/module/drbd/parameters/usermode_helper" + + @staticmethod + def _GetProcData(filename=_STATUS_FILE): + """Return data from /proc/drbd. + + """ + try: + data = utils.ReadFile(filename).splitlines() + except EnvironmentError, err: + if err.errno == errno.ENOENT: + base.ThrowError("The file %s cannot be opened, check if the module" + " is loaded (%s)", filename, str(err)) + else: + base.ThrowError("Can't read the DRBD proc file %s: %s", + filename, str(err)) + if not data: + base.ThrowError("Can't read any data from %s", filename) + return data + + @classmethod + def _MassageProcData(cls, data): + """Transform the output of _GetProdData into a nicer form. + + @return: a dictionary of minor: joined lines from /proc/drbd + for that minor + + """ + results = {} + old_minor = old_line = None + for line in data: + 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 + old_minor = int(lresult.group(1)) + old_line = line + else: + if old_minor is not None: + old_line += " " + line.strip() + # add last line + if old_minor is not None: + results[old_minor] = old_line + return results + + @classmethod + def _GetVersion(cls, proc_data): + """Return the DRBD version. + + This will return a dict with keys: + - k_major + - k_minor + - k_point + - api + - proto + - proto2 (only on drbd > 8.2.X) + + """ + 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) + + 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: + base.ThrowError("The file %s cannot be opened, check if the module" + " is loaded (%s)", filename, str(err)) + else: + base.ThrowError("Can't read DRBD helper file %s: %s", + filename, str(err)) + if not helper: + base.ThrowError("Can't read any data from %s", filename) + return helper + + @staticmethod + def _DevPath(minor): + """Return the path to a drbd device for a given minor. + + """ + return "/dev/drbd%d" % minor + + @classmethod + def GetUsedDevs(cls): + """Compute the list of used DRBD devices. + + """ + data = cls._GetProcData() + + used_devs = {} + for line in data: + match = cls._VALID_LINE_RE.match(line) + if not match: + continue + minor = int(match.group(1)) + state = match.group(2) + if state == cls._ST_UNCONFIGURED: + continue + used_devs[minor] = state, line + + return used_devs + + def _SetFromMinor(self, minor): + """Set our parameters based on the given minor. + + This sets our minor variable and our dev_path. + + """ + 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 checks the size, which must be around + 128MiB. + + """ + result = utils.RunCmd(["blockdev", "--getsize", meta_device]) + if result.failed: + base.ThrowError("Failed to get device size: %s - %s", + result.fail_reason, result.output) + try: + sectors = int(result.stdout) + except (TypeError, ValueError): + base.ThrowError("Invalid output from blockdev: '%s'", result.stdout) + num_bytes = sectors * 512 + if num_bytes < 128 * 1024 * 1024: # less than 128MiB + base.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: + base.ThrowError("Meta device too big (%.2fMiB)", + (num_bytes / 1024 / 1024)) + + 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 DRBD8Status(object): + """A DRBD status representation class. + + Note that this doesn't support unconfigured devices (cs:Unconfigured). + + """ + 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: + 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 + + +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 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. + + """ + _MAX_MINORS = 255 + _PARSE_SHOW = None + + # 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) != 6: + raise ValueError("Invalid configuration data %s" % str(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: + base.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: + base.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: + base.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() + + highest = None + for line in data: + match = cls._UNUSED_LINE_RE.match(line) + if match: + return int(match.group(1)) + 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 _GetShowParser(cls): + """Return a parser for `drbd show` output. + + This will either create or return an already-created parser for the + output of the command `drbd show`. + + """ + if cls._PARSE_SHOW is not None: + return cls._PARSE_SHOW + + # 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])) + + comment = pyp.Literal("#") + pyp.Optional(pyp.restOfLine) + defa = pyp.Literal("_is_default").suppress() + dbl_quote = pyp.Literal('"').suppress() + + keyword = pyp.Word(pyp.alphanums + "-") + + # 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 + + # 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()) + + # an entire section + section_name = pyp.Word(pyp.alphas + "_") + section = section_name + lbrace + pyp.ZeroOrMore(pyp.Group(stmt)) + rbrace + + bnf = pyp.ZeroOrMore(pyp.Group(section ^ stmt)) + bnf.ignore(comment) + + 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: + base.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 + "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 + + def _AssembleLocal(self, minor, backend, meta, size): + """Configure the local part of a DRBD device. + + """ + args = ["drbdsetup", self._DevPath(minor), "disk", + backend, meta, "0", + "-e", "detach", + "--create-device"] + if size: + args.extend(["-d", "%sm" % size]) + + version = self._GetVersion(self._GetProcData()) + vmaj = version["k_major"] + vmin = version["k_minor"] + vrel = version["k_point"] + + barrier_args = \ + self._ComputeDiskBarrierArgs(vmaj, vmin, vrel, + self.params[constants.LDP_BARRIERS], + self.params[constants.LDP_NO_META_FLUSH]) + args.extend(barrier_args) + + if self.params[constants.LDP_DISK_CUSTOM]: + args.extend(shlex.split(self.params[constants.LDP_DISK_CUSTOM])) + + result = utils.RunCmd(args) + if result.failed: + base.ThrowError("drbd%d: can't attach local disk: %s", + minor, result.output) + + @classmethod + def _ComputeDiskBarrierArgs(cls, vmaj, vmin, vrel, disabled_barriers, + disable_meta_flush): + """Compute the DRBD command line parameters for disk barriers + + 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. + + """ + 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) + + 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)) + + 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 + 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: + base.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): + base.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): + base.ThrowError("drbd%d: can't connect ip %s to ip %s" % + (minor, lhost, rhost)) + family = "ipv4" + else: + base.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: + base.ThrowError("drbd%d: can't setup network: %s - %s", + minor, result.fail_reason, result.output) + + def _CheckNetworkConfig(): + info = self._GetDevInfo(self._GetShowData(minor)) + if not "local_addr" in info or not "remote_addr" in info: + raise utils.RetryAgain() + + if (info["local_addr"] != (lhost, lport) or + info["remote_addr"] != (rhost, rport)): + raise utils.RetryAgain() + + try: + utils.Retry(_CheckNetworkConfig, 1.0, 10.0) + except utils.RetryTimeout: + base.ThrowError("drbd%d: timeout while configuring network", minor) + + def AddChildren(self, devices): + """Add a disk to the DRBD device. + + """ + if self.minor is None: + base.ThrowError("drbd%d: can't attach to dbrd8 during AddChildren", + self._aminor) + if len(devices) != 2: + base.ThrowError("drbd%d: need two devices for AddChildren", self.minor) + info = self._GetDevInfo(self._GetShowData(self.minor)) + if "local_dev" in info: + base.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: + base.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: + base.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: + base.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: + base.ThrowError("drbd%d: we need two children in RemoveChildren", + self.minor) + for child, dev in zip(self._children, devices): + if dev != child.dev_path: + base.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: + msg = ("Can't change syncer rate: %s - %s" % + (result.fail_reason, result.output)) + logging.error(msg) + return [msg] + + return [] + + 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 + + """ + if self.minor is None: + err = "Not attached during SetSyncParams" + logging.info(err) + return [err] + + children_result = super(DRBD8, self).SetSyncParams(params) + children_result.extend(self._SetMinorSyncParams(self.minor, params)) + return children_result + + 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: + logging.info("Not attached during PauseSync") + return False + + children_result = super(DRBD8, self).PauseResumeSync(pause) + + 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. + + """ + if self.minor is None: + base.ThrowError("drbd%d: GetStats() called while not attached", + self._aminor) + proc_info = self._MassageProcData(self._GetProcData()) + if self.minor not in proc_info: + base.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(): + base.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: + ldisk_status = constants.LDS_UNKNOWN + + 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 '-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 + is mandatory. + + """ + if self.minor is None and not self.Attach(): + 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: + base.ThrowError("drbd%d: can't make drbd device primary: %s", self.minor, + result.output) + + 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(): + base.ThrowError("drbd%d: can't Attach() in Close()", self._aminor) + result = utils.RunCmd(["drbdsetup", self.dev_path, "secondary"]) + if result.failed: + base.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: + base.ThrowError("drbd%d: disk not attached in re-attach net", + self._aminor) + + if None in (self._lhost, self._lport, self._rhost, self._rport): + base.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(base.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 = \ + base.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" + + base.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: + base.ThrowError("drbd%d: device not attached in AttachNet", self._aminor) + + if None in (self._lhost, self._lport, self._rhost, self._rport): + base.ThrowError("drbd%d: missing network info in AttachNet()", self.minor) + + status = self.GetProcStatus() + + if not status.is_standalone: + base.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: + 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() + + 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() + + sync_errors = self.SetSyncParams(self.params) + if sync_errors: + base.ThrowError("drbd%d: can't set the synchronization parameters: %s" % + (self.minor, utils.CommaJoin(sync_errors))) + + 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. + + """ + # 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: + base.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: + base.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: + base.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: + base.ThrowError("drbd%d: network attach successful, but 'drbdsetup" + " show' disagrees", minor) + + else: + minor = None + + self._SetFromMinor(minor) + if minor is None: + base.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: + base.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: + base.ThrowError("drbd%d: can't shutdown network: %s", + minor, result.output) + + @classmethod + 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: + base.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 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: + base.ThrowError("drbd%d: minor is already in use at Create() time", + aminor) + meta = children[1] + meta.Assemble() + if not meta.Attach(): + base.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: + base.ThrowError("drbd%d: Grow called while not attached", self._aminor) + if len(self._children) != 2 or None in self._children: + base.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: + base.ThrowError("drbd%d: resize failed: %s", self.minor, 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 diff --git a/lib/bootstrap.py b/lib/bootstrap.py index 2183d6b08..0d7a4bc39 100644 --- a/lib/bootstrap.py +++ b/lib/bootstrap.py @@ -40,7 +40,7 @@ from ganeti import objects from ganeti import ssconf from ganeti import serializer from ganeti import hypervisor -from ganeti.block import bdev +from ganeti.block import drbd from ganeti import netutils from ganeti import luxi from ganeti import jstore @@ -472,7 +472,7 @@ def InitCluster(cluster_name, mac_prefix, # pylint: disable=R0913, R0914 if drbd_helper is not None: try: - curr_helper = bdev.BaseDRBD.GetUsermodeHelper() + curr_helper = drbd.BaseDRBD.GetUsermodeHelper() except errors.BlockDeviceError, err: raise errors.OpPrereqError("Error while checking drbd helper" " (specify --no-drbd-storage if you are not" diff --git a/lib/watcher/nodemaint.py b/lib/watcher/nodemaint.py index a0a830064..bfe761ecf 100644 --- a/lib/watcher/nodemaint.py +++ b/lib/watcher/nodemaint.py @@ -32,7 +32,7 @@ from ganeti import netutils from ganeti import ssconf from ganeti import utils from ganeti import confd -from ganeti.block import bdev +from ganeti.block import drbd import ganeti.confd.client # pylint: disable=W0611 @@ -80,7 +80,7 @@ class NodeMaintenance(object): """Get list of used DRBD minors. """ - return bdev.DRBD8.GetUsedDevs().keys() + return drbd.DRBD8.GetUsedDevs().keys() @classmethod def DoMaintenance(cls, role): @@ -124,7 +124,7 @@ class NodeMaintenance(object): # pylint: disable=W0212 # using the private method as is, pending enhancements to the DRBD # interface - bdev.DRBD8._ShutdownAll(minor) + drbd.DRBD8._ShutdownAll(minor) def Exec(self): """Check node status versus cluster desired state. diff --git a/test/py/ganeti.block.bdev_unittest.py b/test/py/ganeti.block.bdev_unittest.py index 161058228..bc3326922 100755 --- a/test/py/ganeti.block.bdev_unittest.py +++ b/test/py/ganeti.block.bdev_unittest.py @@ -32,6 +32,7 @@ from ganeti import errors from ganeti import objects from ganeti import utils from ganeti.block import bdev +from ganeti.block import drbd import testutils @@ -70,11 +71,11 @@ class TestBaseDRBD(testutils.GanetiTestCase): } ] for d,r in zip(data, result): - self.assertEqual(bdev.BaseDRBD._GetVersion(d), r) + self.assertEqual(drbd.BaseDRBD._GetVersion(d), r) class TestDRBD8Runner(testutils.GanetiTestCase): - """Testing case for DRBD8""" + """Testing case for drbd.DRBD8""" @staticmethod def _has_disk(data, dname, mname): @@ -102,12 +103,12 @@ class TestDRBD8Runner(testutils.GanetiTestCase): def testParserCreation(self): """Test drbdsetup show parser creation""" - bdev.DRBD8._GetShowParser() + drbd.DRBD8._GetShowParser() def testParser80(self): """Test drbdsetup show parser for disk and network version 8.0""" data = testutils.ReadTestData("bdev-drbd-8.0.txt") - result = bdev.DRBD8._GetDevInfo(data) + result = drbd.DRBD8._GetDevInfo(data) self.failUnless(self._has_disk(result, "/dev/xenvg/test.data", "/dev/xenvg/test.meta"), "Wrong local disk info") @@ -118,7 +119,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): def testParser83(self): """Test drbdsetup show parser for disk and network version 8.3""" data = testutils.ReadTestData("bdev-drbd-8.3.txt") - result = bdev.DRBD8._GetDevInfo(data) + result = drbd.DRBD8._GetDevInfo(data) self.failUnless(self._has_disk(result, "/dev/xenvg/test.data", "/dev/xenvg/test.meta"), "Wrong local disk info") @@ -129,7 +130,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): def testParserNetIP4(self): """Test drbdsetup show parser for IPv4 network""" data = testutils.ReadTestData("bdev-drbd-net-ip4.txt") - result = bdev.DRBD8._GetDevInfo(data) + result = drbd.DRBD8._GetDevInfo(data) self.failUnless(("local_dev" not in result and "meta_dev" not in result and "meta_index" not in result), @@ -141,7 +142,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): def testParserNetIP6(self): """Test drbdsetup show parser for IPv6 network""" data = testutils.ReadTestData("bdev-drbd-net-ip6.txt") - result = bdev.DRBD8._GetDevInfo(data) + result = drbd.DRBD8._GetDevInfo(data) self.failUnless(("local_dev" not in result and "meta_dev" not in result and "meta_index" not in result), @@ -153,7 +154,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): def testParserDisk(self): """Test drbdsetup show parser for disk""" data = testutils.ReadTestData("bdev-drbd-disk.txt") - result = bdev.DRBD8._GetDevInfo(data) + result = drbd.DRBD8._GetDevInfo(data) self.failUnless(self._has_disk(result, "/dev/xenvg/test.data", "/dev/xenvg/test.meta"), "Wrong local disk info") @@ -174,7 +175,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): for vmaj, vmin, vrel, opts, meta in should_fail: self.assertRaises(errors.BlockDeviceError, - bdev.DRBD8._ComputeDiskBarrierArgs, + drbd.DRBD8._ComputeDiskBarrierArgs, vmaj, vmin, vrel, opts, meta) # get the valid options from the frozenset(frozenset()) in constants. @@ -184,7 +185,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): for vmaj, vmin, vrel in ((8, 0, 0), (8, 0, 11), (8, 2, 6)): for opts in valid_options: self.assertRaises(errors.BlockDeviceError, - bdev.DRBD8._ComputeDiskBarrierArgs, + drbd.DRBD8._ComputeDiskBarrierArgs, vmaj, vmin, vrel, opts, True) # Versions with partial support (testing only options that are supported) @@ -212,7 +213,7 @@ class TestDRBD8Runner(testutils.GanetiTestCase): for test in tests: vmaj, vmin, vrel, disabled_barriers, disable_meta_flush, expected = test args = \ - bdev.DRBD8._ComputeDiskBarrierArgs(vmaj, vmin, vrel, + drbd.DRBD8._ComputeDiskBarrierArgs(vmaj, vmin, vrel, disabled_barriers, disable_meta_flush) self.failUnless(set(args) == set(expected), @@ -221,13 +222,13 @@ class TestDRBD8Runner(testutils.GanetiTestCase): # Unsupported or invalid versions for vmaj, vmin, vrel in ((0, 7, 25), (9, 0, 0), (7, 0, 0), (8, 4, 0)): self.assertRaises(errors.BlockDeviceError, - bdev.DRBD8._ComputeDiskBarrierArgs, + drbd.DRBD8._ComputeDiskBarrierArgs, vmaj, vmin, vrel, "n", True) # Invalid options for option in ("", "c", "whatever", "nbdfc", "nf"): self.assertRaises(errors.BlockDeviceError, - bdev.DRBD8._ComputeDiskBarrierArgs, + drbd.DRBD8._ComputeDiskBarrierArgs, 8, 3, 11, option, True) @@ -243,30 +244,30 @@ class TestDRBD8Status(testutils.GanetiTestCase): proc83_sync_data = testutils.TestDataFilename("proc_drbd83_sync.txt") proc83_sync_krnl_data = \ testutils.TestDataFilename("proc_drbd83_sync_krnl2.6.39.txt") - self.proc_data = bdev.DRBD8._GetProcData(filename=proc_data) - self.proc80e_data = bdev.DRBD8._GetProcData(filename=proc80e_data) - self.proc83_data = bdev.DRBD8._GetProcData(filename=proc83_data) - self.proc83_sync_data = bdev.DRBD8._GetProcData(filename=proc83_sync_data) + self.proc_data = drbd.DRBD8._GetProcData(filename=proc_data) + self.proc80e_data = drbd.DRBD8._GetProcData(filename=proc80e_data) + self.proc83_data = drbd.DRBD8._GetProcData(filename=proc83_data) + self.proc83_sync_data = drbd.DRBD8._GetProcData(filename=proc83_sync_data) self.proc83_sync_krnl_data = \ - bdev.DRBD8._GetProcData(filename=proc83_sync_krnl_data) - self.mass_data = bdev.DRBD8._MassageProcData(self.proc_data) - self.mass80e_data = bdev.DRBD8._MassageProcData(self.proc80e_data) - self.mass83_data = bdev.DRBD8._MassageProcData(self.proc83_data) - self.mass83_sync_data = bdev.DRBD8._MassageProcData(self.proc83_sync_data) + drbd.DRBD8._GetProcData(filename=proc83_sync_krnl_data) + self.mass_data = drbd.DRBD8._MassageProcData(self.proc_data) + self.mass80e_data = drbd.DRBD8._MassageProcData(self.proc80e_data) + self.mass83_data = drbd.DRBD8._MassageProcData(self.proc83_data) + self.mass83_sync_data = drbd.DRBD8._MassageProcData(self.proc83_sync_data) self.mass83_sync_krnl_data = \ - bdev.DRBD8._MassageProcData(self.proc83_sync_krnl_data) + drbd.DRBD8._MassageProcData(self.proc83_sync_krnl_data) def testIOErrors(self): """Test handling of errors while reading the proc file.""" temp_file = self._CreateTempFile() os.unlink(temp_file) self.failUnlessRaises(errors.BlockDeviceError, - bdev.DRBD8._GetProcData, filename=temp_file) + drbd.DRBD8._GetProcData, filename=temp_file) def testHelper(self): """Test reading usermode_helper in /sys.""" sys_drbd_helper = testutils.TestDataFilename("sys_drbd_usermode_helper.txt") - drbd_helper = bdev.DRBD8.GetUsermodeHelper(filename=sys_drbd_helper) + drbd_helper = drbd.DRBD8.GetUsermodeHelper(filename=sys_drbd_helper) self.failUnlessEqual(drbd_helper, "/bin/true") def testHelperIOErrors(self): @@ -274,7 +275,7 @@ class TestDRBD8Status(testutils.GanetiTestCase): temp_file = self._CreateTempFile() os.unlink(temp_file) self.failUnlessRaises(errors.BlockDeviceError, - bdev.DRBD8.GetUsermodeHelper, filename=temp_file) + drbd.DRBD8.GetUsermodeHelper, filename=temp_file) def testMinorNotFound(self): """Test not-found-minor in /proc""" @@ -283,13 +284,13 @@ class TestDRBD8Status(testutils.GanetiTestCase): self.failUnless(3 not in self.mass80e_data) def testLineNotMatch(self): - """Test wrong line passed to DRBD8Status""" - self.assertRaises(errors.BlockDeviceError, bdev.DRBD8Status, "foo") + """Test wrong line passed to drbd.DRBD8Status""" + self.assertRaises(errors.BlockDeviceError, drbd.DRBD8Status, "foo") def testMinor0(self): """Test connected, primary device""" for data in [self.mass_data, self.mass83_data]: - stats = bdev.DRBD8Status(data[0]) + stats = drbd.DRBD8Status(data[0]) self.failUnless(stats.is_in_use) self.failUnless(stats.is_connected and stats.is_primary and stats.peer_secondary and stats.is_disk_uptodate) @@ -297,7 +298,7 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinor1(self): """Test connected, secondary device""" for data in [self.mass_data, self.mass83_data]: - stats = bdev.DRBD8Status(data[1]) + stats = drbd.DRBD8Status(data[1]) self.failUnless(stats.is_in_use) self.failUnless(stats.is_connected and stats.is_secondary and stats.peer_primary and stats.is_disk_uptodate) @@ -305,13 +306,13 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinor2(self): """Test unconfigured device""" for data in [self.mass_data, self.mass83_data, self.mass80e_data]: - stats = bdev.DRBD8Status(data[2]) + stats = drbd.DRBD8Status(data[2]) self.failIf(stats.is_in_use) def testMinor4(self): """Test WFconn device""" for data in [self.mass_data, self.mass83_data]: - stats = bdev.DRBD8Status(data[4]) + stats = drbd.DRBD8Status(data[4]) self.failUnless(stats.is_in_use) self.failUnless(stats.is_wfconn and stats.is_primary and stats.rrole == "Unknown" and @@ -320,7 +321,7 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinor6(self): """Test diskless device""" for data in [self.mass_data, self.mass83_data]: - stats = bdev.DRBD8Status(data[6]) + stats = drbd.DRBD8Status(data[6]) self.failUnless(stats.is_in_use) self.failUnless(stats.is_connected and stats.is_secondary and stats.peer_primary and stats.is_diskless) @@ -328,19 +329,19 @@ class TestDRBD8Status(testutils.GanetiTestCase): def testMinor8(self): """Test standalone device""" for data in [self.mass_data, self.mass83_data]: - stats = bdev.DRBD8Status(data[8]) + stats = drbd.DRBD8Status(data[8]) self.failUnless(stats.is_in_use) self.failUnless(stats.is_standalone and stats.rrole == "Unknown" and stats.is_disk_uptodate) def testDRBD83SyncFine(self): - stats = bdev.DRBD8Status(self.mass83_sync_data[3]) + stats = drbd.DRBD8Status(self.mass83_sync_data[3]) self.failUnless(stats.is_in_resync) self.failUnless(stats.sync_percent is not None) def testDRBD83SyncBroken(self): - stats = bdev.DRBD8Status(self.mass83_sync_krnl_data[3]) + stats = drbd.DRBD8Status(self.mass83_sync_krnl_data[3]) self.failUnless(stats.is_in_resync) self.failUnless(stats.sync_percent is not None) -- GitLab