blob: f84daf864845816aa277137738395302127494d1 [file] [log] [blame]
#
#
# 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()