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 AreSpindlesSupported(self):
439 """Are spindles supported by the current configuration?
442 return self.GetExclusiveStorage()
444 def GetVclusterSettings(self):
445 """Returns settings for virtual cluster.
448 master = self.get(_VCLUSTER_MASTER_KEY)
449 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
451 return (master, basedir)
453 def GetDiskOptions(self):
454 """Return options for the disks of the instances.
456 Get 'disks' parameter from the configuration data. If 'disks' is missing,
457 try to create it from the legacy 'disk' and 'disk-growth' parameters.
461 return self._data["disks"]
466 sizes = self._data.get("disk")
467 growths = self._data.get("disk-growth")
469 if (sizes is None or growths is None or len(sizes) != len(growths)):
470 raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
471 " exist and have the same number of items")
473 for (size, growth) in zip(sizes, growths):
474 disks.append({"size": size, "growth": growth})
481 """Loads the passed configuration file.
484 global _config # pylint: disable=W0603
486 _config = _QaConfig.Load(path)
490 """Returns the configuration object.
494 raise RuntimeError("Configuration not yet loaded")
499 def get(name, default=None):
500 """Wrapper for L{_QaConfig.get}.
503 return GetConfig().get(name, default=default)
507 def __init__(self, tests):
508 """Initializes this class.
510 @type tests: list or string
511 @param tests: List of test names
512 @see: L{TestEnabled} for details
518 def _MakeSequence(value):
519 """Make sequence of single argument.
521 If the single argument is not already a list or tuple, a list with the
522 argument as a single item is returned.
525 if isinstance(value, (list, tuple)):
531 def _TestEnabledInner(check_fn, names, fn):
532 """Evaluate test conditions.
534 @type check_fn: callable
535 @param check_fn: Callback to check whether a test is enabled
536 @type names: sequence or string
537 @param names: Test name(s)
539 @param fn: Aggregation function
541 @return: Whether test is enabled
544 names = _MakeSequence(names)
549 if isinstance(name, Either):
550 value = _TestEnabledInner(check_fn, name.tests, compat.any)
551 elif isinstance(name, (list, tuple)):
552 value = _TestEnabledInner(check_fn, name, compat.all)
556 value = check_fn(name)
563 def TestEnabled(tests, _cfg=None):
564 """Returns True if the given tests are enabled.
566 @param tests: A single test as a string, or a list of tests to check; can
567 contain L{Either} for OR conditions, AND is default
575 # Get settings for all tests
576 cfg_tests = cfg.get("tests", {})
578 # Get default setting
579 default = cfg_tests.get("default", True)
581 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
585 def GetInstanceCheckScript(*args):
586 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
589 return GetConfig().GetInstanceCheckScript(*args)
592 def GetEnabledHypervisors(*args):
593 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
596 return GetConfig().GetEnabledHypervisors(*args)
599 def GetDefaultHypervisor(*args):
600 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
603 return GetConfig().GetDefaultHypervisor(*args)
606 def GetEnabledDiskTemplates(*args):
607 """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
610 return GetConfig().GetEnabledDiskTemplates(*args)
613 def GetEnabledStorageTypes(*args):
614 """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
617 return GetConfig().GetEnabledStorageTypes(*args)
620 def GetDefaultDiskTemplate(*args):
621 """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
624 return GetConfig().GetDefaultDiskTemplate(*args)
628 """Wrapper for L{_QaConfig.GetMasterNode}.
631 return GetConfig().GetMasterNode()
634 def AcquireInstance(_cfg=None):
635 """Returns an instance which isn't in use.
643 # Filter out unwanted instances
644 instances = filter(lambda inst: not inst.used, cfg["instances"])
647 raise qa_error.OutOfInstancesError("No instances left")
649 instance = instances[0]
655 def SetExclusiveStorage(value):
656 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
659 return GetConfig().SetExclusiveStorage(value)
662 def GetExclusiveStorage():
663 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
666 return GetConfig().GetExclusiveStorage()
669 def IsTemplateSupported(templ):
670 """Wrapper for L{_QaConfig.IsTemplateSupported}.
673 return GetConfig().IsTemplateSupported(templ)
676 def AreSpindlesSupported():
677 """Wrapper for L{_QaConfig.AreSpindlesSupported}.
680 return GetConfig().AreSpindlesSupported()
683 def _NodeSortKey(node):
684 """Returns sort key for a node.
686 @type node: L{_QaNode}
689 return (node.use_count, utils.NiceSortKey(node.primary))
692 def AcquireNode(exclude=None, _cfg=None):
693 """Returns the least used node.
701 master = cfg.GetMasterNode()
703 # Filter out unwanted nodes
704 # TODO: Maybe combine filters
706 nodes = cfg["nodes"][:]
707 elif isinstance(exclude, (list, tuple)):
708 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
710 nodes = filter(lambda node: node != exclude, cfg["nodes"])
712 nodes = filter(lambda node: node.added or node == master, nodes)
715 raise qa_error.OutOfNodesError("No nodes left")
717 # Return node with least number of uses
718 return sorted(nodes, key=_NodeSortKey)[0].Use()
721 def AcquireManyNodes(num, exclude=None):
722 """Return the least used nodes.
725 @param num: Number of nodes; can be 0.
726 @type exclude: list of nodes or C{None}
727 @param exclude: nodes to be excluded from the choice
728 @rtype: list of nodes
729 @return: C{num} different nodes
735 elif isinstance(exclude, (list, tuple)):
736 # Don't modify the incoming argument
737 exclude = list(exclude)
742 for _ in range(0, num):
743 n = AcquireNode(exclude=exclude)
746 except qa_error.OutOfNodesError:
747 ReleaseManyNodes(nodes)
752 def ReleaseManyNodes(nodes):
757 def GetVclusterSettings():
758 """Wrapper for L{_QaConfig.GetVclusterSettings}.
761 return GetConfig().GetVclusterSettings()
764 def UseVirtualCluster(_cfg=None):
765 """Returns whether a virtual cluster is used.
775 (master, _) = cfg.GetVclusterSettings()
780 @ht.WithDesc("No virtual cluster")
781 def NoVirtualCluster():
782 """Used to disable tests for virtual clusters.
785 return not UseVirtualCluster()
788 def GetDiskOptions():
789 """Wrapper for L{_QaConfig.GetDiskOptions}.
792 return GetConfig().GetDiskOptions()