| # |
| # |
| |
| # Copyright (C) 2007, 2011, 2012, 2013 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. |
| |
| |
| """QA configuration. |
| |
| """ |
| |
| import os |
| |
| from ganeti import constants |
| from ganeti import utils |
| from ganeti import serializer |
| from ganeti import compat |
| from ganeti import ht |
| |
| import qa_error |
| import qa_logging |
| |
| |
| _INSTANCE_CHECK_KEY = "instance-check" |
| _ENABLED_HV_KEY = "enabled-hypervisors" |
| _VCLUSTER_MASTER_KEY = "vcluster-master" |
| _VCLUSTER_BASEDIR_KEY = "vcluster-basedir" |
| _ENABLED_DISK_TEMPLATES_KEY = "enabled-disk-templates" |
| |
| # The constants related to JSON patching (as per RFC6902) that modifies QA's |
| # configuration. |
| _QA_BASE_PATH = os.path.dirname(__file__) |
| _QA_DEFAULT_PATCH = "qa-patch.json" |
| _QA_PATCH_DIR = "patch" |
| _QA_PATCH_ORDER_FILE = "order" |
| |
| #: QA configuration (L{_QaConfig}) |
| _config = None |
| |
| |
| class _QaInstance(object): |
| __slots__ = [ |
| "name", |
| "nicmac", |
| "_used", |
| "_disk_template", |
| ] |
| |
| def __init__(self, name, nicmac): |
| """Initializes instances of this class. |
| |
| """ |
| self.name = name |
| self.nicmac = nicmac |
| self._used = None |
| self._disk_template = None |
| |
| @classmethod |
| def FromDict(cls, data): |
| """Creates instance object from JSON dictionary. |
| |
| """ |
| nicmac = [] |
| |
| macaddr = data.get("nic.mac/0") |
| if macaddr: |
| nicmac.append(macaddr) |
| |
| return cls(name=data["name"], nicmac=nicmac) |
| |
| def __repr__(self): |
| status = [ |
| "%s.%s" % (self.__class__.__module__, self.__class__.__name__), |
| "name=%s" % self.name, |
| "nicmac=%s" % self.nicmac, |
| "used=%s" % self._used, |
| "disk_template=%s" % self._disk_template, |
| ] |
| |
| return "<%s at %#x>" % (" ".join(status), id(self)) |
| |
| def Use(self): |
| """Marks instance as being in use. |
| |
| """ |
| assert not self._used |
| assert self._disk_template is None |
| |
| self._used = True |
| |
| def Release(self): |
| """Releases instance and makes it available again. |
| |
| """ |
| assert self._used, \ |
| ("Instance '%s' was never acquired or released more than once" % |
| self.name) |
| |
| self._used = False |
| self._disk_template = None |
| |
| def GetNicMacAddr(self, idx, default): |
| """Returns MAC address for NIC. |
| |
| @type idx: int |
| @param idx: NIC index |
| @param default: Default value |
| |
| """ |
| if len(self.nicmac) > idx: |
| return self.nicmac[idx] |
| else: |
| return default |
| |
| def SetDiskTemplate(self, template): |
| """Set the disk template. |
| |
| """ |
| assert template in constants.DISK_TEMPLATES |
| |
| self._disk_template = template |
| |
| @property |
| def used(self): |
| """Returns boolean denoting whether instance is in use. |
| |
| """ |
| return self._used |
| |
| @property |
| def disk_template(self): |
| """Returns the current disk template. |
| |
| """ |
| return self._disk_template |
| |
| |
| class _QaNode(object): |
| __slots__ = [ |
| "primary", |
| "secondary", |
| "_added", |
| "_use_count", |
| ] |
| |
| def __init__(self, primary, secondary): |
| """Initializes instances of this class. |
| |
| @type primary: string |
| @param primary: the primary network address of this node |
| @type secondary: string |
| @param secondary: the secondary network address of this node |
| |
| """ |
| self.primary = primary |
| self.secondary = secondary |
| self._added = False |
| self._use_count = 0 |
| |
| @classmethod |
| def FromDict(cls, data): |
| """Creates node object from JSON dictionary. |
| |
| """ |
| return cls(primary=data["primary"], secondary=data.get("secondary")) |
| |
| def __repr__(self): |
| status = [ |
| "%s.%s" % (self.__class__.__module__, self.__class__.__name__), |
| "primary=%s" % self.primary, |
| "secondary=%s" % self.secondary, |
| "added=%s" % self._added, |
| "use_count=%s" % self._use_count, |
| ] |
| |
| return "<%s at %#x>" % (" ".join(status), id(self)) |
| |
| def Use(self): |
| """Marks a node as being in use. |
| |
| """ |
| assert self._use_count >= 0 |
| |
| self._use_count += 1 |
| |
| return self |
| |
| def Release(self): |
| """Release a node (opposite of L{Use}). |
| |
| """ |
| assert self.use_count > 0 |
| |
| self._use_count -= 1 |
| |
| def MarkAdded(self): |
| """Marks node as having been added to a cluster. |
| |
| """ |
| assert not self._added |
| self._added = True |
| |
| def MarkRemoved(self): |
| """Marks node as having been removed from a cluster. |
| |
| """ |
| assert self._added |
| self._added = False |
| |
| @property |
| def added(self): |
| """Returns whether a node is part of a cluster. |
| |
| """ |
| return self._added |
| |
| @property |
| def use_count(self): |
| """Returns number of current uses (controlled by L{Use} and L{Release}). |
| |
| """ |
| return self._use_count |
| |
| |
| _RESOURCE_CONVERTER = { |
| "instances": _QaInstance.FromDict, |
| "nodes": _QaNode.FromDict, |
| } |
| |
| |
| def _ConvertResources((key, value)): |
| """Converts cluster resources in configuration to Python objects. |
| |
| """ |
| fn = _RESOURCE_CONVERTER.get(key, None) |
| if fn: |
| return (key, map(fn, value)) |
| else: |
| return (key, value) |
| |
| |
| class _QaConfig(object): |
| def __init__(self, data): |
| """Initializes instances of this class. |
| |
| """ |
| self._data = data |
| |
| #: Cluster-wide run-time value of the exclusive storage flag |
| self._exclusive_storage = None |
| |
| @staticmethod |
| def LoadPatch(patch_dict, rel_path): |
| """ Loads a single patch. |
| |
| @type patch_dict: dict of string to dict |
| @param patch_dict: A dictionary storing patches by relative path. |
| @type rel_path: string |
| @param rel_path: The relative path to the patch, might or might not exist. |
| |
| """ |
| try: |
| full_path = os.path.join(_QA_BASE_PATH, rel_path) |
| patch = serializer.LoadJson(utils.ReadFile(full_path)) |
| patch_dict[rel_path] = patch |
| except IOError: |
| pass |
| |
| @staticmethod |
| def LoadPatches(): |
| """ Finds and loads all patches supported by the QA. |
| |
| @rtype: dict of string to dict |
| @return: A dictionary of relative path to patch content. |
| |
| """ |
| patches = {} |
| _QaConfig.LoadPatch(patches, _QA_DEFAULT_PATCH) |
| patch_dir_path = os.path.join(_QA_BASE_PATH, _QA_PATCH_DIR) |
| if os.path.exists(patch_dir_path): |
| for filename in os.listdir(patch_dir_path): |
| if filename.endswith(".json"): |
| _QaConfig.LoadPatch(patches, os.path.join(_QA_PATCH_DIR, filename)) |
| return patches |
| |
| @staticmethod |
| def ApplyPatch(data, patch_module, patches, patch_path): |
| """Applies a single patch. |
| |
| @type data: dict (deserialized json) |
| @param data: The QA configuration to modify |
| @type patch_module: module |
| @param patch_module: The json patch module, loaded dynamically |
| @type patches: dict of string to dict |
| @param patches: The dictionary of patch path to content |
| @type patch_path: string |
| @param patch_path: The path to the patch, relative to the QA directory |
| |
| """ |
| patch_content = patches[patch_path] |
| print qa_logging.FormatInfo("Applying patch %s" % patch_path) |
| if not patch_content and patch_path != _QA_DEFAULT_PATCH: |
| print qa_logging.FormatWarning("The patch %s added by the user is empty" % |
| patch_path) |
| patch_module.apply_patch(data, patch_content, in_place=True) |
| |
| @staticmethod |
| def ApplyPatches(data, patch_module, patches): |
| """Applies any patches present, and returns the modified QA configuration. |
| |
| First, patches from the patch directory are applied. They are ordered |
| alphabetically, unless there is an ``order`` file present - any patches |
| listed within are applied in that order, and any remaining ones in |
| alphabetical order again. Finally, the default patch residing in the |
| top-level QA directory is applied. |
| |
| @type data: dict (deserialized json) |
| @param data: The QA configuration to modify |
| @type patch_module: module |
| @param patch_module: The json patch module, loaded dynamically |
| @type patches: dict of string to dict |
| @param patches: The dictionary of patch path to content |
| |
| """ |
| ordered_patches = [] |
| order_path = os.path.join(_QA_BASE_PATH, _QA_PATCH_DIR, |
| _QA_PATCH_ORDER_FILE) |
| if os.path.exists(order_path): |
| order_file = open(order_path, 'r') |
| ordered_patches = order_file.read().splitlines() |
| # Removes empty lines |
| ordered_patches = filter(None, ordered_patches) |
| |
| # Add the patch dir |
| ordered_patches = map(lambda x: os.path.join(_QA_PATCH_DIR, x), |
| ordered_patches) |
| |
| # First the ordered patches |
| for patch in ordered_patches: |
| if patch not in patches: |
| raise qa_error.Error("Patch %s specified in the ordering file does not " |
| "exist" % patch) |
| _QaConfig.ApplyPatch(data, patch_module, patches, patch) |
| |
| # Then the other non-default ones |
| for patch in sorted(patches): |
| if patch != _QA_DEFAULT_PATCH and patch not in ordered_patches: |
| _QaConfig.ApplyPatch(data, patch_module, patches, patch) |
| |
| # Finally the default one |
| if _QA_DEFAULT_PATCH in patches: |
| _QaConfig.ApplyPatch(data, patch_module, patches, _QA_DEFAULT_PATCH) |
| |
| @classmethod |
| def Load(cls, filename): |
| """Loads a configuration file and produces a configuration object. |
| |
| @type filename: string |
| @param filename: Path to configuration file |
| @rtype: L{_QaConfig} |
| |
| """ |
| data = serializer.LoadJson(utils.ReadFile(filename)) |
| |
| # Patch the document using JSON Patch (RFC6902) in file _PATCH_JSON, if |
| # available |
| try: |
| patches = _QaConfig.LoadPatches() |
| # Try to use the module only if there is a non-empty patch present |
| if any(patches.values()): |
| mod = __import__("jsonpatch", fromlist=[]) |
| _QaConfig.ApplyPatches(data, mod, patches) |
| except IOError: |
| pass |
| except ImportError: |
| raise qa_error.Error("For the QA JSON patching feature to work, you " |
| "need to install Python modules 'jsonpatch' and " |
| "'jsonpointer'.") |
| |
| result = cls(dict(map(_ConvertResources, |
| data.items()))) # pylint: disable=E1103 |
| result.Validate() |
| |
| return result |
| |
| def Validate(self): |
| """Validates loaded configuration data. |
| |
| """ |
| if not self.get("name"): |
| raise qa_error.Error("Cluster name is required") |
| |
| if not self.get("nodes"): |
| raise qa_error.Error("Need at least one node") |
| |
| if not self.get("instances"): |
| raise qa_error.Error("Need at least one instance") |
| |
| disks = self.GetDiskOptions() |
| if disks is None: |
| raise qa_error.Error("Config option 'disks' must exist") |
| else: |
| for d in disks: |
| if d.get("size") is None or d.get("growth") is None: |
| raise qa_error.Error("Config options `size` and `growth` must exist" |
| " for all `disks` items") |
| check = self.GetInstanceCheckScript() |
| if check: |
| try: |
| os.stat(check) |
| except EnvironmentError, err: |
| raise qa_error.Error("Can't find instance check script '%s': %s" % |
| (check, err)) |
| |
| enabled_hv = frozenset(self.GetEnabledHypervisors()) |
| if not enabled_hv: |
| raise qa_error.Error("No hypervisor is enabled") |
| |
| difference = enabled_hv - constants.HYPER_TYPES |
| if difference: |
| raise qa_error.Error("Unknown hypervisor(s) enabled: %s" % |
| utils.CommaJoin(difference)) |
| |
| (vc_master, vc_basedir) = self.GetVclusterSettings() |
| if bool(vc_master) != bool(vc_basedir): |
| raise qa_error.Error("All or none of the config options '%s' and '%s'" |
| " must be set" % |
| (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY)) |
| |
| if vc_basedir and not utils.IsNormAbsPath(vc_basedir): |
| raise qa_error.Error("Path given in option '%s' must be absolute and" |
| " normalized" % _VCLUSTER_BASEDIR_KEY) |
| |
| def __getitem__(self, name): |
| """Returns configuration value. |
| |
| @type name: string |
| @param name: Name of configuration entry |
| |
| """ |
| return self._data[name] |
| |
| def __setitem__(self, key, value): |
| """Sets a configuration value. |
| |
| """ |
| self._data[key] = value |
| |
| def __delitem__(self, key): |
| """Deletes a value from the configuration. |
| |
| """ |
| del(self._data[key]) |
| |
| def __len__(self): |
| """Return the number of configuration items. |
| |
| """ |
| return len(self._data) |
| |
| def get(self, name, default=None): |
| """Returns configuration value. |
| |
| @type name: string |
| @param name: Name of configuration entry |
| @param default: Default value |
| |
| """ |
| return self._data.get(name, default) |
| |
| def GetMasterNode(self): |
| """Returns the default master node for the cluster. |
| |
| """ |
| return self["nodes"][0] |
| |
| def GetAllNodes(self): |
| """Returns the list of nodes. |
| |
| This is not intended to 'acquire' those nodes. For that, |
| C{AcquireManyNodes} is better suited. However, often it is |
| helpful to know the total number of nodes available to |
| adjust cluster parameters and that's where this function |
| is useful. |
| """ |
| return self["nodes"] |
| |
| def GetInstanceCheckScript(self): |
| """Returns path to instance check script or C{None}. |
| |
| """ |
| return self._data.get(_INSTANCE_CHECK_KEY, None) |
| |
| def GetEnabledHypervisors(self): |
| """Returns list of enabled hypervisors. |
| |
| @rtype: list |
| |
| """ |
| return self._GetStringListParameter( |
| _ENABLED_HV_KEY, |
| [constants.DEFAULT_ENABLED_HYPERVISOR]) |
| |
| def GetDefaultHypervisor(self): |
| """Returns the default hypervisor to be used. |
| |
| """ |
| return self.GetEnabledHypervisors()[0] |
| |
| def GetEnabledDiskTemplates(self): |
| """Returns the list of enabled disk templates. |
| |
| @rtype: list |
| |
| """ |
| return self._GetStringListParameter( |
| _ENABLED_DISK_TEMPLATES_KEY, |
| constants.DEFAULT_ENABLED_DISK_TEMPLATES) |
| |
| def GetEnabledStorageTypes(self): |
| """Returns the list of enabled storage types. |
| |
| @rtype: list |
| @returns: the list of storage types enabled for QA |
| |
| """ |
| enabled_disk_templates = self.GetEnabledDiskTemplates() |
| enabled_storage_types = list( |
| set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt] |
| for dt in enabled_disk_templates])) |
| # Storage type 'lvm-pv' cannot be activated via a disk template, |
| # therefore we add it if 'lvm-vg' is present. |
| if constants.ST_LVM_VG in enabled_storage_types: |
| enabled_storage_types.append(constants.ST_LVM_PV) |
| return enabled_storage_types |
| |
| def GetDefaultDiskTemplate(self): |
| """Returns the default disk template to be used. |
| |
| """ |
| return self.GetEnabledDiskTemplates()[0] |
| |
| def _GetStringListParameter(self, key, default_values): |
| """Retrieves a parameter's value that is supposed to be a list of strings. |
| |
| @rtype: list |
| |
| """ |
| try: |
| value = self._data[key] |
| except KeyError: |
| return default_values |
| else: |
| if value is None: |
| return [] |
| elif isinstance(value, basestring): |
| return value.split(",") |
| else: |
| return value |
| |
| def SetExclusiveStorage(self, value): |
| """Set the expected value of the C{exclusive_storage} flag for the cluster. |
| |
| """ |
| self._exclusive_storage = bool(value) |
| |
| def GetExclusiveStorage(self): |
| """Get the expected value of the C{exclusive_storage} flag for the cluster. |
| |
| """ |
| value = self._exclusive_storage |
| assert value is not None |
| return value |
| |
| def IsTemplateSupported(self, templ): |
| """Is the given disk template supported by the current configuration? |
| |
| """ |
| enabled = templ in self.GetEnabledDiskTemplates() |
| return enabled and (not self.GetExclusiveStorage() or |
| templ in constants.DTS_EXCL_STORAGE) |
| |
| def IsStorageTypeSupported(self, storage_type): |
| """Is the given storage type supported by the current configuration? |
| |
| This is determined by looking if at least one of the disk templates |
| which is associated with the storage type is enabled in the configuration. |
| |
| """ |
| enabled_disk_templates = self.GetEnabledDiskTemplates() |
| if storage_type == constants.ST_LVM_PV: |
| disk_templates = utils.GetDiskTemplatesOfStorageTypes(constants.ST_LVM_VG) |
| else: |
| disk_templates = utils.GetDiskTemplatesOfStorageTypes(storage_type) |
| return bool(set(enabled_disk_templates).intersection(set(disk_templates))) |
| |
| def AreSpindlesSupported(self): |
| """Are spindles supported by the current configuration? |
| |
| """ |
| return self.GetExclusiveStorage() |
| |
| def GetVclusterSettings(self): |
| """Returns settings for virtual cluster. |
| |
| """ |
| master = self.get(_VCLUSTER_MASTER_KEY) |
| basedir = self.get(_VCLUSTER_BASEDIR_KEY) |
| |
| return (master, basedir) |
| |
| def GetDiskOptions(self): |
| """Return options for the disks of the instances. |
| |
| Get 'disks' parameter from the configuration data. If 'disks' is missing, |
| try to create it from the legacy 'disk' and 'disk-growth' parameters. |
| |
| """ |
| try: |
| return self._data["disks"] |
| except KeyError: |
| pass |
| |
| # Legacy interface |
| sizes = self._data.get("disk") |
| growths = self._data.get("disk-growth") |
| if sizes or growths: |
| if (sizes is None or growths is None or len(sizes) != len(growths)): |
| raise qa_error.Error("Config options 'disk' and 'disk-growth' must" |
| " exist and have the same number of items") |
| disks = [] |
| for (size, growth) in zip(sizes, growths): |
| disks.append({"size": size, "growth": growth}) |
| return disks |
| else: |
| return None |
| |
| def GetModifySshSetup(self): |
| """Return whether to modify the SSH setup or not. |
| |
| This specified whether Ganeti should create and distribute SSH |
| keys for the nodes or not. The default is 'True'. |
| |
| """ |
| if self.get("modify_ssh_setup") is None: |
| return True |
| else: |
| return self.get("modify_ssh_setup") |
| |
| def GetSshConfig(self): |
| """Returns the SSH configuration necessary to create keys etc. |
| |
| @rtype: tuple of (string, string, string, string, string) |
| @return: tuple containing key type ('dsa' or 'rsa'), ssh directory |
| (default '/root/.ssh/'), file path of the private key, file path |
| of the public key, file path of the 'authorized_keys' file |
| |
| """ |
| key_type = "dsa" |
| if self.get("ssh_key_type") is not None: |
| key_type = self.get("ssh_key_type") |
| |
| ssh_dir = "/root/.ssh/" |
| if self.get("ssh_dir") is not None: |
| ssh_dir = self.get("ssh_dir") |
| |
| priv_key_file = os.path.join(ssh_dir, "id_%s" % (key_type)) |
| pub_key_file = "%s.pub" % priv_key_file |
| auth_key_file = os.path.join(ssh_dir, "authorized_keys") |
| |
| return (key_type, ssh_dir, priv_key_file, pub_key_file, auth_key_file) |
| |
| |
| def Load(path): |
| """Loads the passed configuration file. |
| |
| """ |
| global _config # pylint: disable=W0603 |
| |
| _config = _QaConfig.Load(path) |
| |
| |
| def GetConfig(): |
| """Returns the configuration object. |
| |
| """ |
| if _config is None: |
| raise RuntimeError("Configuration not yet loaded") |
| |
| return _config |
| |
| |
| def get(name, default=None): |
| """Wrapper for L{_QaConfig.get}. |
| |
| """ |
| return GetConfig().get(name, default=default) |
| |
| |
| class Either(object): |
| def __init__(self, tests): |
| """Initializes this class. |
| |
| @type tests: list or string |
| @param tests: List of test names |
| @see: L{TestEnabled} for details |
| |
| """ |
| self.tests = tests |
| |
| |
| def _MakeSequence(value): |
| """Make sequence of single argument. |
| |
| If the single argument is not already a list or tuple, a list with the |
| argument as a single item is returned. |
| |
| """ |
| if isinstance(value, (list, tuple)): |
| return value |
| else: |
| return [value] |
| |
| |
| def _TestEnabledInner(check_fn, names, fn): |
| """Evaluate test conditions. |
| |
| @type check_fn: callable |
| @param check_fn: Callback to check whether a test is enabled |
| @type names: sequence or string |
| @param names: Test name(s) |
| @type fn: callable |
| @param fn: Aggregation function |
| @rtype: bool |
| @return: Whether test is enabled |
| |
| """ |
| names = _MakeSequence(names) |
| |
| result = [] |
| |
| for name in names: |
| if isinstance(name, Either): |
| value = _TestEnabledInner(check_fn, name.tests, compat.any) |
| elif isinstance(name, (list, tuple)): |
| value = _TestEnabledInner(check_fn, name, compat.all) |
| elif callable(name): |
| value = name() |
| else: |
| value = check_fn(name) |
| |
| result.append(value) |
| |
| return fn(result) |
| |
| |
| def TestEnabled(tests, _cfg=None): |
| """Returns True if the given tests are enabled. |
| |
| @param tests: A single test as a string, or a list of tests to check; can |
| contain L{Either} for OR conditions, AND is default |
| |
| """ |
| if _cfg is None: |
| cfg = GetConfig() |
| else: |
| cfg = _cfg |
| |
| # Get settings for all tests |
| cfg_tests = cfg.get("tests", {}) |
| |
| # Get default setting |
| default = cfg_tests.get("default", True) |
| |
| return _TestEnabledInner(lambda name: cfg_tests.get(name, default), |
| tests, compat.all) |
| |
| |
| def GetInstanceCheckScript(*args): |
| """Wrapper for L{_QaConfig.GetInstanceCheckScript}. |
| |
| """ |
| return GetConfig().GetInstanceCheckScript(*args) |
| |
| |
| def GetEnabledHypervisors(*args): |
| """Wrapper for L{_QaConfig.GetEnabledHypervisors}. |
| |
| """ |
| return GetConfig().GetEnabledHypervisors(*args) |
| |
| |
| def GetDefaultHypervisor(*args): |
| """Wrapper for L{_QaConfig.GetDefaultHypervisor}. |
| |
| """ |
| return GetConfig().GetDefaultHypervisor(*args) |
| |
| |
| def GetEnabledDiskTemplates(*args): |
| """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}. |
| |
| """ |
| return GetConfig().GetEnabledDiskTemplates(*args) |
| |
| |
| def GetEnabledStorageTypes(*args): |
| """Wrapper for L{_QaConfig.GetEnabledStorageTypes}. |
| |
| """ |
| return GetConfig().GetEnabledStorageTypes(*args) |
| |
| |
| def GetDefaultDiskTemplate(*args): |
| """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}. |
| |
| """ |
| return GetConfig().GetDefaultDiskTemplate(*args) |
| |
| |
| def GetModifySshSetup(*args): |
| """Wrapper for L{_QaConfig.GetModifySshSetup}. |
| |
| """ |
| return GetConfig().GetModifySshSetup(*args) |
| |
| |
| def GetSshConfig(*args): |
| """Wrapper for L{_QaConfig.GetSshConfig}. |
| |
| """ |
| return GetConfig().GetSshConfig(*args) |
| |
| |
| def GetMasterNode(): |
| """Wrapper for L{_QaConfig.GetMasterNode}. |
| |
| """ |
| return GetConfig().GetMasterNode() |
| |
| |
| def GetAllNodes(): |
| """Wrapper for L{_QaConfig.GetAllNodes}. |
| |
| """ |
| return GetConfig().GetAllNodes() |
| |
| |
| def AcquireInstance(_cfg=None): |
| """Returns an instance which isn't in use. |
| |
| """ |
| if _cfg is None: |
| cfg = GetConfig() |
| else: |
| cfg = _cfg |
| |
| # Filter out unwanted instances |
| instances = filter(lambda inst: not inst.used, cfg["instances"]) |
| |
| if not instances: |
| raise qa_error.OutOfInstancesError("No instances left") |
| |
| instance = instances[0] |
| instance.Use() |
| |
| return instance |
| |
| |
| def AcquireManyInstances(num, _cfg=None): |
| """Return instances that are not in use. |
| |
| @type num: int |
| @param num: Number of instances; can be 0. |
| @rtype: list of instances |
| @return: C{num} different instances |
| |
| """ |
| |
| if _cfg is None: |
| cfg = GetConfig() |
| else: |
| cfg = _cfg |
| |
| # Filter out unwanted instances |
| instances = filter(lambda inst: not inst.used, cfg["instances"]) |
| |
| if len(instances) < num: |
| raise qa_error.OutOfInstancesError( |
| "Not enough instances left (%d needed, %d remaining)" % |
| (num, len(instances)) |
| ) |
| |
| instances = [] |
| |
| try: |
| for _ in range(0, num): |
| n = AcquireInstance(_cfg=cfg) |
| instances.append(n) |
| except qa_error.OutOfInstancesError: |
| ReleaseManyInstances(instances) |
| raise |
| |
| return instances |
| |
| |
| def ReleaseManyInstances(instances): |
| for instance in instances: |
| instance.Release() |
| |
| |
| def SetExclusiveStorage(value): |
| """Wrapper for L{_QaConfig.SetExclusiveStorage}. |
| |
| """ |
| return GetConfig().SetExclusiveStorage(value) |
| |
| |
| def GetExclusiveStorage(): |
| """Wrapper for L{_QaConfig.GetExclusiveStorage}. |
| |
| """ |
| return GetConfig().GetExclusiveStorage() |
| |
| |
| def IsTemplateSupported(templ): |
| """Wrapper for L{_QaConfig.IsTemplateSupported}. |
| |
| """ |
| return GetConfig().IsTemplateSupported(templ) |
| |
| |
| def IsStorageTypeSupported(storage_type): |
| """Wrapper for L{_QaConfig.IsTemplateSupported}. |
| |
| """ |
| return GetConfig().IsStorageTypeSupported(storage_type) |
| |
| |
| def AreSpindlesSupported(): |
| """Wrapper for L{_QaConfig.AreSpindlesSupported}. |
| |
| """ |
| return GetConfig().AreSpindlesSupported() |
| |
| |
| def _NodeSortKey(node): |
| """Returns sort key for a node. |
| |
| @type node: L{_QaNode} |
| |
| """ |
| return (node.use_count, utils.NiceSortKey(node.primary)) |
| |
| |
| def AcquireNode(exclude=None, _cfg=None): |
| """Returns the least used node. |
| |
| """ |
| if _cfg is None: |
| cfg = GetConfig() |
| else: |
| cfg = _cfg |
| |
| master = cfg.GetMasterNode() |
| |
| # Filter out unwanted nodes |
| # TODO: Maybe combine filters |
| if exclude is None: |
| nodes = cfg["nodes"][:] |
| elif isinstance(exclude, (list, tuple)): |
| nodes = filter(lambda node: node not in exclude, cfg["nodes"]) |
| else: |
| nodes = filter(lambda node: node != exclude, cfg["nodes"]) |
| |
| nodes = filter(lambda node: node.added or node == master, nodes) |
| |
| if not nodes: |
| raise qa_error.OutOfNodesError("No nodes left") |
| |
| # Return node with least number of uses |
| return sorted(nodes, key=_NodeSortKey)[0].Use() |
| |
| |
| class AcquireManyNodesCtx(object): |
| """Returns the least used nodes for use with a `with` block |
| |
| """ |
| def __init__(self, num, exclude=None, cfg=None): |
| self._num = num |
| self._exclude = exclude |
| self._cfg = cfg |
| |
| def __enter__(self): |
| self._nodes = AcquireManyNodes(self._num, exclude=self._exclude, |
| cfg=self._cfg) |
| return self._nodes |
| |
| def __exit__(self, exc_type, exc_value, exc_tb): |
| ReleaseManyNodes(self._nodes) |
| |
| |
| def AcquireManyNodes(num, exclude=None, cfg=None): |
| """Return the least used nodes. |
| |
| @type num: int |
| @param num: Number of nodes; can be 0. |
| @type exclude: list of nodes or C{None} |
| @param exclude: nodes to be excluded from the choice |
| @rtype: list of nodes |
| @return: C{num} different nodes |
| |
| """ |
| nodes = [] |
| if exclude is None: |
| exclude = [] |
| elif isinstance(exclude, (list, tuple)): |
| # Don't modify the incoming argument |
| exclude = list(exclude) |
| else: |
| exclude = [exclude] |
| |
| try: |
| for _ in range(0, num): |
| n = AcquireNode(exclude=exclude, _cfg=cfg) |
| nodes.append(n) |
| exclude.append(n) |
| except qa_error.OutOfNodesError: |
| ReleaseManyNodes(nodes) |
| raise |
| return nodes |
| |
| |
| def ReleaseManyNodes(nodes): |
| for node in nodes: |
| node.Release() |
| |
| |
| def GetVclusterSettings(): |
| """Wrapper for L{_QaConfig.GetVclusterSettings}. |
| |
| """ |
| return GetConfig().GetVclusterSettings() |
| |
| |
| def UseVirtualCluster(_cfg=None): |
| """Returns whether a virtual cluster is used. |
| |
| @rtype: bool |
| |
| """ |
| if _cfg is None: |
| cfg = GetConfig() |
| else: |
| cfg = _cfg |
| |
| (master, _) = cfg.GetVclusterSettings() |
| |
| return bool(master) |
| |
| |
| @ht.WithDesc("No virtual cluster") |
| def NoVirtualCluster(): |
| """Used to disable tests for virtual clusters. |
| |
| """ |
| return not UseVirtualCluster() |
| |
| |
| def GetDiskOptions(): |
| """Wrapper for L{_QaConfig.GetDiskOptions}. |
| |
| """ |
| return GetConfig().GetDiskOptions() |