| # |
| # |
| |
| # Copyright (C) 2009, 2011, 2012 Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, |
| # this list of conditions and the following disclaimer. |
| # |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| """Storage container abstraction. |
| |
| """ |
| |
| # pylint: disable=W0232,R0201 |
| |
| # W0232, since we use these as singletons rather than object holding |
| # data |
| |
| # R0201, for the same reason |
| |
| # TODO: FileStorage initialised with paths whereas the others not |
| |
| import logging |
| |
| from ganeti import errors |
| from ganeti import constants |
| from ganeti import utils |
| |
| |
| def _ParseSize(value): |
| return int(round(float(value), 0)) |
| |
| |
| class _Base(object): |
| """Base class for storage abstraction. |
| |
| """ |
| def List(self, name, fields): |
| """Returns a list of all entities within the storage unit. |
| |
| @type name: string or None |
| @param name: Entity name or None for all |
| @type fields: list |
| @param fields: List with all requested result fields (order is preserved) |
| |
| """ |
| raise NotImplementedError() |
| |
| def Modify(self, name, changes): # pylint: disable=W0613 |
| """Modifies an entity within the storage unit. |
| |
| @type name: string |
| @param name: Entity name |
| @type changes: dict |
| @param changes: New field values |
| |
| """ |
| # Don't raise an error if no changes are requested |
| if changes: |
| raise errors.ProgrammerError("Unable to modify the following" |
| "fields: %r" % (changes.keys(), )) |
| |
| def Execute(self, name, op): |
| """Executes an operation on an entity within the storage unit. |
| |
| @type name: string |
| @param name: Entity name |
| @type op: string |
| @param op: Operation name |
| |
| """ |
| raise NotImplementedError() |
| |
| |
| class FileStorage(_Base): # pylint: disable=W0223 |
| """File storage unit. |
| |
| """ |
| def __init__(self, paths): |
| """Initializes this class. |
| |
| @type paths: list |
| @param paths: List of file storage paths |
| |
| """ |
| super(FileStorage, self).__init__() |
| |
| self._paths = paths |
| |
| def List(self, name, fields): |
| """Returns a list of all entities within the storage unit. |
| |
| See L{_Base.List}. |
| |
| """ |
| rows = [] |
| |
| if name is None: |
| paths = self._paths |
| else: |
| paths = [name] |
| |
| for path in paths: |
| rows.append(self._ListInner(path, fields)) |
| |
| return rows |
| |
| @staticmethod |
| def _ListInner(path, fields): |
| """Gathers requested information from directory. |
| |
| @type path: string |
| @param path: Path to directory |
| @type fields: list |
| @param fields: Requested fields |
| |
| """ |
| values = [] |
| |
| # Pre-calculate information in case it's requested more than once |
| if constants.SF_USED in fields: |
| dirsize = utils.CalculateDirectorySize(path) |
| else: |
| dirsize = None |
| |
| if constants.SF_FREE in fields or constants.SF_SIZE in fields: |
| fsstats = utils.GetFilesystemStats(path) |
| else: |
| fsstats = None |
| |
| # Make sure to update constants.VALID_STORAGE_FIELDS when changing fields. |
| for field_name in fields: |
| if field_name == constants.SF_NAME: |
| values.append(path) |
| |
| elif field_name == constants.SF_USED: |
| values.append(dirsize) |
| |
| elif field_name == constants.SF_FREE: |
| values.append(fsstats[1]) |
| |
| elif field_name == constants.SF_SIZE: |
| values.append(fsstats[0]) |
| |
| elif field_name == constants.SF_ALLOCATABLE: |
| values.append(True) |
| |
| else: |
| raise errors.StorageError("Unknown field: %r" % field_name) |
| |
| return values |
| |
| |
| class _LvmBase(_Base): # pylint: disable=W0223 |
| """Base class for LVM storage containers. |
| |
| @cvar LIST_FIELDS: list of tuples consisting of three elements: SF_* |
| constants, lvm command output fields (list), and conversion |
| function or static value (for static value, the lvm output field |
| can be an empty list) |
| |
| """ |
| LIST_SEP = "|" |
| LIST_COMMAND = None |
| LIST_FIELDS = None |
| |
| def List(self, name, wanted_field_names): |
| """Returns a list of all entities within the storage unit. |
| |
| See L{_Base.List}. |
| |
| """ |
| # Get needed LVM fields |
| lvm_fields = self._GetLvmFields(self.LIST_FIELDS, wanted_field_names) |
| |
| # Build LVM command |
| cmd_args = self._BuildListCommand(self.LIST_COMMAND, self.LIST_SEP, |
| lvm_fields, name) |
| |
| # Run LVM command |
| cmd_result = self._RunListCommand(cmd_args) |
| |
| # Split and rearrange LVM command output |
| return self._BuildList(self._SplitList(cmd_result, self.LIST_SEP, |
| len(lvm_fields)), |
| self.LIST_FIELDS, |
| wanted_field_names, |
| lvm_fields) |
| |
| @staticmethod |
| def _GetLvmFields(fields_def, wanted_field_names): |
| """Returns unique list of fields wanted from LVM command. |
| |
| @type fields_def: list |
| @param fields_def: Field definitions |
| @type wanted_field_names: list |
| @param wanted_field_names: List of requested fields |
| |
| """ |
| field_to_idx = dict([(field_name, idx) |
| for (idx, (field_name, _, _)) in |
| enumerate(fields_def)]) |
| |
| lvm_fields = [] |
| |
| for field_name in wanted_field_names: |
| try: |
| idx = field_to_idx[field_name] |
| except IndexError: |
| raise errors.StorageError("Unknown field: %r" % field_name) |
| |
| (_, lvm_names, _) = fields_def[idx] |
| |
| lvm_fields.extend(lvm_names) |
| |
| return utils.UniqueSequence(lvm_fields) |
| |
| @classmethod |
| def _BuildList(cls, cmd_result, fields_def, wanted_field_names, lvm_fields): |
| """Builds the final result list. |
| |
| @type cmd_result: iterable |
| @param cmd_result: Iterable of LVM command output (iterable of lists) |
| @type fields_def: list |
| @param fields_def: Field definitions |
| @type wanted_field_names: list |
| @param wanted_field_names: List of requested fields |
| @type lvm_fields: list |
| @param lvm_fields: LVM fields |
| |
| """ |
| lvm_name_to_idx = dict([(lvm_name, idx) |
| for (idx, lvm_name) in enumerate(lvm_fields)]) |
| field_to_idx = dict([(field_name, idx) |
| for (idx, (field_name, _, _)) in |
| enumerate(fields_def)]) |
| |
| data = [] |
| for raw_data in cmd_result: |
| row = [] |
| |
| for field_name in wanted_field_names: |
| (_, lvm_names, mapper) = fields_def[field_to_idx[field_name]] |
| |
| values = [raw_data[lvm_name_to_idx[i]] for i in lvm_names] |
| |
| if callable(mapper): |
| # we got a function, call it with all the declared fields |
| val = mapper(*values) # pylint: disable=W0142 |
| elif len(values) == 1: |
| assert mapper is None, ("Invalid mapper value (neither callable" |
| " nor None) for one-element fields") |
| # we don't have a function, but we had a single field |
| # declared, pass it unchanged |
| val = values[0] |
| else: |
| # let's make sure there are no fields declared (cannot map > |
| # 1 field without a function) |
| assert not values, "LVM storage has multi-fields without a function" |
| val = mapper |
| |
| row.append(val) |
| |
| data.append(row) |
| |
| return data |
| |
| @staticmethod |
| def _BuildListCommand(cmd, sep, options, name): |
| """Builds LVM command line. |
| |
| @type cmd: string |
| @param cmd: Command name |
| @type sep: string |
| @param sep: Field separator character |
| @type options: list of strings |
| @param options: Wanted LVM fields |
| @type name: name or None |
| @param name: Name of requested entity |
| |
| """ |
| args = [cmd, |
| "--noheadings", "--units=m", "--nosuffix", |
| "--separator", sep, |
| "--options", ",".join(options)] |
| |
| if name is not None: |
| args.append(name) |
| |
| return args |
| |
| @staticmethod |
| def _RunListCommand(args): |
| """Run LVM command. |
| |
| """ |
| result = utils.RunCmd(args) |
| |
| if result.failed: |
| raise errors.StorageError("Failed to run %r, command output: %s" % |
| (args[0], result.output)) |
| |
| return result.stdout |
| |
| @staticmethod |
| def _SplitList(data, sep, fieldcount): |
| """Splits LVM command output into rows and fields. |
| |
| @type data: string |
| @param data: LVM command output |
| @type sep: string |
| @param sep: Field separator character |
| @type fieldcount: int |
| @param fieldcount: Expected number of fields |
| |
| """ |
| for line in data.splitlines(): |
| fields = line.strip().split(sep) |
| |
| if len(fields) != fieldcount: |
| logging.warning("Invalid line returned from lvm command: %s", line) |
| continue |
| |
| yield fields |
| |
| |
| def _LvmPvGetAllocatable(attr): |
| """Determines whether LVM PV is allocatable. |
| |
| @rtype: bool |
| |
| """ |
| if attr: |
| return (attr[0] == "a") |
| else: |
| logging.warning("Invalid PV attribute: %r", attr) |
| return False |
| |
| |
| class LvmPvStorage(_LvmBase): # pylint: disable=W0223 |
| """LVM Physical Volume storage unit. |
| |
| """ |
| LIST_COMMAND = "pvs" |
| |
| # Make sure to update constants.VALID_STORAGE_FIELDS when changing field |
| # definitions. |
| LIST_FIELDS = [ |
| (constants.SF_NAME, ["pv_name"], None), |
| (constants.SF_SIZE, ["pv_size"], _ParseSize), |
| (constants.SF_USED, ["pv_used"], _ParseSize), |
| (constants.SF_FREE, ["pv_free"], _ParseSize), |
| (constants.SF_ALLOCATABLE, ["pv_attr"], _LvmPvGetAllocatable), |
| ] |
| |
| def _SetAllocatable(self, name, allocatable): |
| """Sets the "allocatable" flag on a physical volume. |
| |
| @type name: string |
| @param name: Physical volume name |
| @type allocatable: bool |
| @param allocatable: Whether to set the "allocatable" flag |
| |
| """ |
| args = ["pvchange", "--allocatable"] |
| |
| if allocatable: |
| args.append("y") |
| else: |
| args.append("n") |
| |
| args.append(name) |
| |
| result = utils.RunCmd(args) |
| if result.failed: |
| raise errors.StorageError("Failed to modify physical volume," |
| " pvchange output: %s" % |
| result.output) |
| |
| def Modify(self, name, changes): |
| """Modifies flags on a physical volume. |
| |
| See L{_Base.Modify}. |
| |
| """ |
| if constants.SF_ALLOCATABLE in changes: |
| self._SetAllocatable(name, changes[constants.SF_ALLOCATABLE]) |
| del changes[constants.SF_ALLOCATABLE] |
| |
| # Other changes will be handled (and maybe refused) by the base class. |
| return _LvmBase.Modify(self, name, changes) |
| |
| |
| class LvmVgStorage(_LvmBase): |
| """LVM Volume Group storage unit. |
| |
| """ |
| LIST_COMMAND = "vgs" |
| VGREDUCE_COMMAND = "vgreduce" |
| |
| # Make sure to update constants.VALID_STORAGE_FIELDS when changing field |
| # definitions. |
| LIST_FIELDS = [ |
| (constants.SF_NAME, ["vg_name"], None), |
| (constants.SF_SIZE, ["vg_size"], _ParseSize), |
| (constants.SF_FREE, ["vg_free"], _ParseSize), |
| (constants.SF_USED, ["vg_size", "vg_free"], |
| lambda x, y: _ParseSize(x) - _ParseSize(y)), |
| (constants.SF_ALLOCATABLE, [], True), |
| ] |
| |
| def _RemoveMissing(self, name, _runcmd_fn=utils.RunCmd): |
| """Runs "vgreduce --removemissing" on a volume group. |
| |
| @type name: string |
| @param name: Volume group name |
| |
| """ |
| # Ignoring vgreduce exit code. Older versions exit with an error even tough |
| # the VG is already consistent. This was fixed in later versions, but we |
| # cannot depend on it. |
| result = _runcmd_fn([self.VGREDUCE_COMMAND, "--removemissing", name]) |
| |
| # Keep output in case something went wrong |
| vgreduce_output = result.output |
| |
| # work around newer LVM version |
| if ("Wrote out consistent volume group" not in vgreduce_output or |
| "vgreduce --removemissing --force" in vgreduce_output): |
| # we need to re-run with --force |
| result = _runcmd_fn([self.VGREDUCE_COMMAND, "--removemissing", |
| "--force", name]) |
| vgreduce_output += "\n" + result.output |
| |
| result = _runcmd_fn([self.LIST_COMMAND, "--noheadings", |
| "--nosuffix", name]) |
| # we also need to check the output |
| if result.failed or "Couldn't find device with uuid" in result.output: |
| raise errors.StorageError(("Volume group '%s' still not consistent," |
| " 'vgreduce' output: %r," |
| " 'vgs' output: %r") % |
| (name, vgreduce_output, result.output)) |
| |
| def Execute(self, name, op): |
| """Executes an operation on a virtual volume. |
| |
| See L{_Base.Execute}. |
| |
| """ |
| if op == constants.SO_FIX_CONSISTENCY: |
| return self._RemoveMissing(name) |
| |
| return _LvmBase.Execute(self, name, op) |
| |
| |
| # Lookup table for storage types |
| _STORAGE_TYPES = { |
| constants.ST_FILE: FileStorage, |
| constants.ST_LVM_PV: LvmPvStorage, |
| constants.ST_LVM_VG: LvmVgStorage, |
| constants.ST_SHARED_FILE: FileStorage, |
| constants.ST_GLUSTER: FileStorage, |
| } |
| |
| |
| def GetStorageClass(name): |
| """Returns the class for a storage type. |
| |
| @type name: string |
| @param name: Storage type |
| |
| """ |
| try: |
| return _STORAGE_TYPES[name] |
| except KeyError: |
| raise errors.StorageError("Unknown storage type: %r" % name) |
| |
| |
| def GetStorage(name, *args): |
| """Factory function for storage methods. |
| |
| @type name: string |
| @param name: Storage type |
| |
| """ |
| return GetStorageClass(name)(*args) |