4 # Copyright (C) 2007, 2011, 2012, 2013 Google Inc.
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
28 from ganeti import constants
29 from ganeti import utils
30 from ganeti import serializer
31 from ganeti import compat
37 _INSTANCE_CHECK_KEY = "instance-check"
38 _ENABLED_HV_KEY = "enabled-hypervisors"
39 _VCLUSTER_MASTER_KEY = "vcluster-master"
40 _VCLUSTER_BASEDIR_KEY = "vcluster-basedir"
41 _ENABLED_STORAGE_TYPES_KEY = "enabled-storage-types"
43 #: QA configuration (L{_QaConfig})
47 class _QaInstance(object):
55 def __init__(self, name, nicmac):
56 """Initializes instances of this class.
62 self._disk_template = None
65 def FromDict(cls, data):
66 """Creates instance object from JSON dictionary.
71 macaddr = data.get("nic.mac/0")
73 nicmac.append(macaddr)
75 return cls(name=data["name"], nicmac=nicmac)
79 "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
80 "name=%s" % self.name,
81 "nicmac=%s" % self.nicmac,
82 "used=%s" % self._used,
83 "disk_template=%s" % self._disk_template,
86 return "<%s at %#x>" % (" ".join(status), id(self))
89 """Marks instance as being in use.
93 assert self._disk_template is None
98 """Releases instance and makes it available again.
102 ("Instance '%s' was never acquired or released more than once" %
106 self._disk_template = None
108 def GetNicMacAddr(self, idx, default):
109 """Returns MAC address for NIC.
112 @param idx: NIC index
113 @param default: Default value
116 if len(self.nicmac) > idx:
117 return self.nicmac[idx]
121 def SetDiskTemplate(self, template):
122 """Set the disk template.
125 assert template in constants.DISK_TEMPLATES
127 self._disk_template = template
131 """Returns boolean denoting whether instance is in use.
137 def disk_template(self):
138 """Returns the current disk template.
141 return self._disk_template
144 class _QaNode(object):
152 def __init__(self, primary, secondary):
153 """Initializes instances of this class.
156 self.primary = primary
157 self.secondary = secondary
162 def FromDict(cls, data):
163 """Creates node object from JSON dictionary.
166 return cls(primary=data["primary"], secondary=data.get("secondary"))
170 "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
171 "primary=%s" % self.primary,
172 "secondary=%s" % self.secondary,
173 "added=%s" % self._added,
174 "use_count=%s" % self._use_count,
177 return "<%s at %#x>" % (" ".join(status), id(self))
180 """Marks a node as being in use.
183 assert self._use_count >= 0
190 """Release a node (opposite of L{Use}).
193 assert self.use_count > 0
198 """Marks node as having been added to a cluster.
201 assert not self._added
204 def MarkRemoved(self):
205 """Marks node as having been removed from a cluster.
213 """Returns whether a node is part of a cluster.
220 """Returns number of current uses (controlled by L{Use} and L{Release}).
223 return self._use_count
226 _RESOURCE_CONVERTER = {
227 "instances": _QaInstance.FromDict,
228 "nodes": _QaNode.FromDict,
232 def _ConvertResources((key, value)):
233 """Converts cluster resources in configuration to Python objects.
236 fn = _RESOURCE_CONVERTER.get(key, None)
238 return (key, map(fn, value))
243 class _QaConfig(object):
244 def __init__(self, data):
245 """Initializes instances of this class.
250 #: Cluster-wide run-time value of the exclusive storage flag
251 self._exclusive_storage = None
254 def Load(cls, filename):
255 """Loads a configuration file and produces a configuration object.
257 @type filename: string
258 @param filename: Path to configuration file
262 data = serializer.LoadJson(utils.ReadFile(filename))
264 result = cls(dict(map(_ConvertResources,
265 data.items()))) # pylint: disable=E1103
271 """Validates loaded configuration data.
274 if not self.get("name"):
275 raise qa_error.Error("Cluster name is required")
277 if not self.get("nodes"):
278 raise qa_error.Error("Need at least one node")
280 if not self.get("instances"):
281 raise qa_error.Error("Need at least one instance")
283 if (self.get("disk") is None or
284 self.get("disk-growth") is None or
285 len(self.get("disk")) != len(self.get("disk-growth"))):
286 raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
287 " and have the same number of items")
289 check = self.GetInstanceCheckScript()
293 except EnvironmentError, err:
294 raise qa_error.Error("Can't find instance check script '%s': %s" %
297 enabled_hv = frozenset(self.GetEnabledHypervisors())
299 raise qa_error.Error("No hypervisor is enabled")
301 difference = enabled_hv - constants.HYPER_TYPES
303 raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
304 utils.CommaJoin(difference))
306 (vc_master, vc_basedir) = self.GetVclusterSettings()
307 if bool(vc_master) != bool(vc_basedir):
308 raise qa_error.Error("All or none of the config options '%s' and '%s'"
310 (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
312 if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
313 raise qa_error.Error("Path given in option '%s' must be absolute and"
314 " normalized" % _VCLUSTER_BASEDIR_KEY)
316 def __getitem__(self, name):
317 """Returns configuration value.
320 @param name: Name of configuration entry
323 return self._data[name]
325 def get(self, name, default=None):
326 """Returns configuration value.
329 @param name: Name of configuration entry
330 @param default: Default value
333 return self._data.get(name, default)
335 def GetMasterNode(self):
336 """Returns the default master node for the cluster.
339 return self["nodes"][0]
341 def GetInstanceCheckScript(self):
342 """Returns path to instance check script or C{None}.
345 return self._data.get(_INSTANCE_CHECK_KEY, None)
347 def GetEnabledHypervisors(self):
348 """Returns list of enabled hypervisors.
353 return self._GetStringListParameter(
355 [constants.DEFAULT_ENABLED_HYPERVISOR])
357 def GetDefaultHypervisor(self):
358 """Returns the default hypervisor to be used.
361 return self.GetEnabledHypervisors()[0]
363 def GetEnabledStorageTypes(self):
364 """Returns the list of enabled storage types.
369 return self._GetStringListParameter(
370 _ENABLED_STORAGE_TYPES_KEY,
371 list(constants.DEFAULT_ENABLED_STORAGE_TYPES))
373 def GetDefaultStorageType(self):
374 """Returns the default storage type to be used.
377 return self.GetEnabledStorageTypes()[0]
379 def _GetStringListParameter(self, key, default_values):
380 """Retrieves a parameter's value that is supposed to be a list of strings.
386 value = self._data[key]
388 return default_values
392 elif isinstance(value, basestring):
393 return value.split(",")
397 def SetExclusiveStorage(self, value):
398 """Set the expected value of the C{exclusive_storage} flag for the cluster.
401 self._exclusive_storage = bool(value)
403 def GetExclusiveStorage(self):
404 """Get the expected value of the C{exclusive_storage} flag for the cluster.
407 value = self._exclusive_storage
408 assert value is not None
411 def IsTemplateSupported(self, templ):
412 """Is the given disk template supported by the current configuration?
415 return (not self.GetExclusiveStorage() or
416 templ in constants.DTS_EXCL_STORAGE)
418 def GetVclusterSettings(self):
419 """Returns settings for virtual cluster.
422 master = self.get(_VCLUSTER_MASTER_KEY)
423 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
425 return (master, basedir)
429 """Loads the passed configuration file.
432 global _config # pylint: disable=W0603
434 _config = _QaConfig.Load(path)
438 """Returns the configuration object.
442 raise RuntimeError("Configuration not yet loaded")
447 def get(name, default=None):
448 """Wrapper for L{_QaConfig.get}.
451 return GetConfig().get(name, default=default)
455 def __init__(self, tests):
456 """Initializes this class.
458 @type tests: list or string
459 @param tests: List of test names
460 @see: L{TestEnabled} for details
466 def _MakeSequence(value):
467 """Make sequence of single argument.
469 If the single argument is not already a list or tuple, a list with the
470 argument as a single item is returned.
473 if isinstance(value, (list, tuple)):
479 def _TestEnabledInner(check_fn, names, fn):
480 """Evaluate test conditions.
482 @type check_fn: callable
483 @param check_fn: Callback to check whether a test is enabled
484 @type names: sequence or string
485 @param names: Test name(s)
487 @param fn: Aggregation function
489 @return: Whether test is enabled
492 names = _MakeSequence(names)
497 if isinstance(name, Either):
498 value = _TestEnabledInner(check_fn, name.tests, compat.any)
499 elif isinstance(name, (list, tuple)):
500 value = _TestEnabledInner(check_fn, name, compat.all)
504 value = check_fn(name)
511 def TestEnabled(tests, _cfg=None):
512 """Returns True if the given tests are enabled.
514 @param tests: A single test as a string, or a list of tests to check; can
515 contain L{Either} for OR conditions, AND is default
523 # Get settings for all tests
524 cfg_tests = cfg.get("tests", {})
526 # Get default setting
527 default = cfg_tests.get("default", True)
529 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
533 def GetInstanceCheckScript(*args):
534 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
537 return GetConfig().GetInstanceCheckScript(*args)
540 def GetEnabledHypervisors(*args):
541 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
544 return GetConfig().GetEnabledHypervisors(*args)
547 def GetDefaultHypervisor(*args):
548 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
551 return GetConfig().GetDefaultHypervisor(*args)
554 def GetEnabledStorageTypes(*args):
555 """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
558 return GetConfig().GetEnabledStorageTypes(*args)
561 def GetDefaultStorageType(*args):
562 """Wrapper for L{_QaConfig.GetDefaultStorageType}.
565 return GetConfig().GetDefaultStorageType(*args)
569 """Wrapper for L{_QaConfig.GetMasterNode}.
572 return GetConfig().GetMasterNode()
575 def AcquireInstance(_cfg=None):
576 """Returns an instance which isn't in use.
584 # Filter out unwanted instances
585 instances = filter(lambda inst: not inst.used, cfg["instances"])
588 raise qa_error.OutOfInstancesError("No instances left")
590 instance = instances[0]
596 def SetExclusiveStorage(value):
597 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
600 return GetConfig().SetExclusiveStorage(value)
603 def GetExclusiveStorage():
604 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
607 return GetConfig().GetExclusiveStorage()
610 def IsTemplateSupported(templ):
611 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
614 return GetConfig().IsTemplateSupported(templ)
617 def _NodeSortKey(node):
618 """Returns sort key for a node.
620 @type node: L{_QaNode}
623 return (node.use_count, utils.NiceSortKey(node.primary))
626 def AcquireNode(exclude=None, _cfg=None):
627 """Returns the least used node.
635 master = cfg.GetMasterNode()
637 # Filter out unwanted nodes
638 # TODO: Maybe combine filters
640 nodes = cfg["nodes"][:]
641 elif isinstance(exclude, (list, tuple)):
642 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
644 nodes = filter(lambda node: node != exclude, cfg["nodes"])
646 nodes = filter(lambda node: node.added or node == master, nodes)
649 raise qa_error.OutOfNodesError("No nodes left")
651 # Return node with least number of uses
652 return sorted(nodes, key=_NodeSortKey)[0].Use()
655 def AcquireManyNodes(num, exclude=None):
656 """Return the least used nodes.
659 @param num: Number of nodes; can be 0.
660 @type exclude: list of nodes or C{None}
661 @param exclude: nodes to be excluded from the choice
662 @rtype: list of nodes
663 @return: C{num} different nodes
669 elif isinstance(exclude, (list, tuple)):
670 # Don't modify the incoming argument
671 exclude = list(exclude)
676 for _ in range(0, num):
677 n = AcquireNode(exclude=exclude)
680 except qa_error.OutOfNodesError:
681 ReleaseManyNodes(nodes)
686 def ReleaseManyNodes(nodes):
691 def GetVclusterSettings():
692 """Wrapper for L{_QaConfig.GetVclusterSettings}.
695 return GetConfig().GetVclusterSettings()
698 def UseVirtualCluster(_cfg=None):
699 """Returns whether a virtual cluster is used.
709 (master, _) = cfg.GetVclusterSettings()
714 @ht.WithDesc("No virtual cluster")
715 def NoVirtualCluster():
716 """Used to disable tests for virtual clusters.
719 return not UseVirtualCluster()