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_DISK_TEMPLATES_KEY = "enabled-disk-templates"
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 disks = self.GetDiskOptions()
285 raise qa_error.Error("Config option 'disks' must exist")
288 if d.get("size") is None or d.get("growth") is None:
289 raise qa_error.Error("Config options `size` and `growth` must exist"
290 " for all `disks` items")
291 check = self.GetInstanceCheckScript()
295 except EnvironmentError, err:
296 raise qa_error.Error("Can't find instance check script '%s': %s" %
299 enabled_hv = frozenset(self.GetEnabledHypervisors())
301 raise qa_error.Error("No hypervisor is enabled")
303 difference = enabled_hv - constants.HYPER_TYPES
305 raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
306 utils.CommaJoin(difference))
308 (vc_master, vc_basedir) = self.GetVclusterSettings()
309 if bool(vc_master) != bool(vc_basedir):
310 raise qa_error.Error("All or none of the config options '%s' and '%s'"
312 (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
314 if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
315 raise qa_error.Error("Path given in option '%s' must be absolute and"
316 " normalized" % _VCLUSTER_BASEDIR_KEY)
318 def __getitem__(self, name):
319 """Returns configuration value.
322 @param name: Name of configuration entry
325 return self._data[name]
327 def get(self, name, default=None):
328 """Returns configuration value.
331 @param name: Name of configuration entry
332 @param default: Default value
335 return self._data.get(name, default)
337 def GetMasterNode(self):
338 """Returns the default master node for the cluster.
341 return self["nodes"][0]
343 def GetInstanceCheckScript(self):
344 """Returns path to instance check script or C{None}.
347 return self._data.get(_INSTANCE_CHECK_KEY, None)
349 def GetEnabledHypervisors(self):
350 """Returns list of enabled hypervisors.
355 return self._GetStringListParameter(
357 [constants.DEFAULT_ENABLED_HYPERVISOR])
359 def GetDefaultHypervisor(self):
360 """Returns the default hypervisor to be used.
363 return self.GetEnabledHypervisors()[0]
365 def GetEnabledDiskTemplates(self):
366 """Returns the list of enabled disk templates.
371 return self._GetStringListParameter(
372 _ENABLED_DISK_TEMPLATES_KEY,
373 constants.DEFAULT_ENABLED_DISK_TEMPLATES)
375 def GetEnabledStorageTypes(self):
376 """Returns the list of enabled storage types.
379 @returns: the list of storage types enabled for QA
382 enabled_disk_templates = self.GetEnabledDiskTemplates()
383 enabled_storage_types = list(
384 set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
385 for dt in enabled_disk_templates]))
386 # Storage type 'lvm-pv' cannot be activated via a disk template,
387 # therefore we add it if 'lvm-vg' is present.
388 if constants.ST_LVM_VG in enabled_storage_types:
389 enabled_storage_types.append(constants.ST_LVM_PV)
390 return enabled_storage_types
392 def GetDefaultDiskTemplate(self):
393 """Returns the default disk template to be used.
396 return self.GetEnabledDiskTemplates()[0]
398 def _GetStringListParameter(self, key, default_values):
399 """Retrieves a parameter's value that is supposed to be a list of strings.
405 value = self._data[key]
407 return default_values
411 elif isinstance(value, basestring):
412 return value.split(",")
416 def SetExclusiveStorage(self, value):
417 """Set the expected value of the C{exclusive_storage} flag for the cluster.
420 self._exclusive_storage = bool(value)
422 def GetExclusiveStorage(self):
423 """Get the expected value of the C{exclusive_storage} flag for the cluster.
426 value = self._exclusive_storage
427 assert value is not None
430 def IsTemplateSupported(self, templ):
431 """Is the given disk template supported by the current configuration?
434 enabled = templ in self.GetEnabledDiskTemplates()
435 return enabled and (not self.GetExclusiveStorage() or
436 templ in constants.DTS_EXCL_STORAGE)
438 def IsStorageTypeSupported(self, storage_type):
439 """Is the given storage type supported by the current configuration?
441 This is determined by looking if at least one of the disk templates
442 which is associated with the storage type is enabled in the configuration.
445 enabled_disk_templates = self.GetEnabledDiskTemplates()
446 if storage_type == constants.ST_LVM_PV:
447 disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
449 disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
450 return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
452 def AreSpindlesSupported(self):
453 """Are spindles supported by the current configuration?
456 return self.GetExclusiveStorage()
458 def GetVclusterSettings(self):
459 """Returns settings for virtual cluster.
462 master = self.get(_VCLUSTER_MASTER_KEY)
463 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
465 return (master, basedir)
467 def GetDiskOptions(self):
468 """Return options for the disks of the instances.
470 Get 'disks' parameter from the configuration data. If 'disks' is missing,
471 try to create it from the legacy 'disk' and 'disk-growth' parameters.
475 return self._data["disks"]
480 sizes = self._data.get("disk")
481 growths = self._data.get("disk-growth")
483 if (sizes is None or growths is None or len(sizes) != len(growths)):
484 raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
485 " exist and have the same number of items")
487 for (size, growth) in zip(sizes, growths):
488 disks.append({"size": size, "growth": growth})
495 """Loads the passed configuration file.
498 global _config # pylint: disable=W0603
500 _config = _QaConfig.Load(path)
504 """Returns the configuration object.
508 raise RuntimeError("Configuration not yet loaded")
513 def get(name, default=None):
514 """Wrapper for L{_QaConfig.get}.
517 return GetConfig().get(name, default=default)
521 def __init__(self, tests):
522 """Initializes this class.
524 @type tests: list or string
525 @param tests: List of test names
526 @see: L{TestEnabled} for details
532 def _MakeSequence(value):
533 """Make sequence of single argument.
535 If the single argument is not already a list or tuple, a list with the
536 argument as a single item is returned.
539 if isinstance(value, (list, tuple)):
545 def _TestEnabledInner(check_fn, names, fn):
546 """Evaluate test conditions.
548 @type check_fn: callable
549 @param check_fn: Callback to check whether a test is enabled
550 @type names: sequence or string
551 @param names: Test name(s)
553 @param fn: Aggregation function
555 @return: Whether test is enabled
558 names = _MakeSequence(names)
563 if isinstance(name, Either):
564 value = _TestEnabledInner(check_fn, name.tests, compat.any)
565 elif isinstance(name, (list, tuple)):
566 value = _TestEnabledInner(check_fn, name, compat.all)
570 value = check_fn(name)
577 def TestEnabled(tests, _cfg=None):
578 """Returns True if the given tests are enabled.
580 @param tests: A single test as a string, or a list of tests to check; can
581 contain L{Either} for OR conditions, AND is default
589 # Get settings for all tests
590 cfg_tests = cfg.get("tests", {})
592 # Get default setting
593 default = cfg_tests.get("default", True)
595 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
599 def GetInstanceCheckScript(*args):
600 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
603 return GetConfig().GetInstanceCheckScript(*args)
606 def GetEnabledHypervisors(*args):
607 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
610 return GetConfig().GetEnabledHypervisors(*args)
613 def GetDefaultHypervisor(*args):
614 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
617 return GetConfig().GetDefaultHypervisor(*args)
620 def GetEnabledDiskTemplates(*args):
621 """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
624 return GetConfig().GetEnabledDiskTemplates(*args)
627 def GetEnabledStorageTypes(*args):
628 """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
631 return GetConfig().GetEnabledStorageTypes(*args)
634 def GetDefaultDiskTemplate(*args):
635 """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
638 return GetConfig().GetDefaultDiskTemplate(*args)
642 """Wrapper for L{_QaConfig.GetMasterNode}.
645 return GetConfig().GetMasterNode()
648 def AcquireInstance(_cfg=None):
649 """Returns an instance which isn't in use.
657 # Filter out unwanted instances
658 instances = filter(lambda inst: not inst.used, cfg["instances"])
661 raise qa_error.OutOfInstancesError("No instances left")
663 instance = instances[0]
669 def SetExclusiveStorage(value):
670 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
673 return GetConfig().SetExclusiveStorage(value)
676 def GetExclusiveStorage():
677 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
680 return GetConfig().GetExclusiveStorage()
683 def IsTemplateSupported(templ):
684 """Wrapper for L{_QaConfig.IsTemplateSupported}.
687 return GetConfig().IsTemplateSupported(templ)
690 def IsStorageTypeSupported(storage_type):
691 """Wrapper for L{_QaConfig.IsTemplateSupported}.
694 return GetConfig().IsStorageTypeSupported(storage_type)
697 def AreSpindlesSupported():
698 """Wrapper for L{_QaConfig.AreSpindlesSupported}.
701 return GetConfig().AreSpindlesSupported()
704 def _NodeSortKey(node):
705 """Returns sort key for a node.
707 @type node: L{_QaNode}
710 return (node.use_count, utils.NiceSortKey(node.primary))
713 def AcquireNode(exclude=None, _cfg=None):
714 """Returns the least used node.
722 master = cfg.GetMasterNode()
724 # Filter out unwanted nodes
725 # TODO: Maybe combine filters
727 nodes = cfg["nodes"][:]
728 elif isinstance(exclude, (list, tuple)):
729 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
731 nodes = filter(lambda node: node != exclude, cfg["nodes"])
733 nodes = filter(lambda node: node.added or node == master, nodes)
736 raise qa_error.OutOfNodesError("No nodes left")
738 # Return node with least number of uses
739 return sorted(nodes, key=_NodeSortKey)[0].Use()
742 def AcquireManyNodes(num, exclude=None):
743 """Return the least used nodes.
746 @param num: Number of nodes; can be 0.
747 @type exclude: list of nodes or C{None}
748 @param exclude: nodes to be excluded from the choice
749 @rtype: list of nodes
750 @return: C{num} different nodes
756 elif isinstance(exclude, (list, tuple)):
757 # Don't modify the incoming argument
758 exclude = list(exclude)
763 for _ in range(0, num):
764 n = AcquireNode(exclude=exclude)
767 except qa_error.OutOfNodesError:
768 ReleaseManyNodes(nodes)
773 def ReleaseManyNodes(nodes):
778 def GetVclusterSettings():
779 """Wrapper for L{_QaConfig.GetVclusterSettings}.
782 return GetConfig().GetVclusterSettings()
785 def UseVirtualCluster(_cfg=None):
786 """Returns whether a virtual cluster is used.
796 (master, _) = cfg.GetVclusterSettings()
801 @ht.WithDesc("No virtual cluster")
802 def NoVirtualCluster():
803 """Used to disable tests for virtual clusters.
806 return not UseVirtualCluster()
809 def GetDiskOptions():
810 """Wrapper for L{_QaConfig.GetDiskOptions}.
813 return GetConfig().GetDiskOptions()