| # |
| # |
| |
| # 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, spindles, 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). |
| |
| @type unique_id: 2-element tuple or list |
| @param unique_id: unique identifier; the details depend on the actual device |
| type |
| @type children: list of L{BlockDev} |
| @param children: for hierarchical devices, the child devices |
| @type size: float |
| @param size: size in MiB |
| @type spindles: int |
| @param spindles: number of physical disk to dedicate to the device |
| @type params: dict |
| @param params: device-specific options/parameters |
| @type excl_stor: bool |
| @param excl_stor: whether exclusive_storage is active |
| @rtype: L{BlockDev} |
| @return: the created device, or C{None} in case of an error |
| |
| """ |
| 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. |
| |
| @type force: boolean |
| |
| """ |
| 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. |
| |
| @type pause: boolean |
| @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, excl_stor): |
| """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 |
| @type excl_stor: boolean |
| @param excl_stor: Whether exclusive_storage is active |
| |
| """ |
| 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 GetActualSpindles(self): |
| """Return the actual number of spindles used. |
| |
| This is not supported by all devices; if not supported, C{None} is returned. |
| |
| @note: the device needs to be active when this is called |
| |
| """ |
| assert self.attached, "BlockDevice not attached in GetActualSpindles()" |
| return None |
| |
| def GetActualDimensions(self): |
| """Return the actual disk size and number of spindles used. |
| |
| @rtype: tuple |
| @return: (size, spindles); spindles is C{None} when they are not supported |
| |
| @note: the device needs to be active when this is called |
| |
| """ |
| return (self.GetActualSize(), self.GetActualSpindles()) |
| |
| 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 |