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(set([constants.DISK_TEMPLATES_STORAGE_TYPE[dt]
384 for dt in enabled_disk_templates]))
385 # Storage type 'lvm-pv' cannot be activated via a disk template,
386 # therefore we add it if 'lvm-vg' is present.
387 if constants.ST_LVM_VG in enabled_storage_types:
388 enabled_storage_types.append(constants.ST_LVM_PV)
389 return enabled_storage_types
391 def GetDefaultDiskTemplate(self):
392 """Returns the default disk template to be used.
395 return self.GetEnabledDiskTemplates()[0]
397 def _GetStringListParameter(self, key, default_values):
398 """Retrieves a parameter's value that is supposed to be a list of strings.
404 value = self._data[key]
406 return default_values
410 elif isinstance(value, basestring):
411 return value.split(",")
415 def SetExclusiveStorage(self, value):
416 """Set the expected value of the C{exclusive_storage} flag for the cluster.
419 self._exclusive_storage = bool(value)
421 def GetExclusiveStorage(self):
422 """Get the expected value of the C{exclusive_storage} flag for the cluster.
425 value = self._exclusive_storage
426 assert value is not None
429 def IsTemplateSupported(self, templ):
430 """Is the given disk template supported by the current configuration?
433 enabled = templ in self.GetEnabledDiskTemplates()
434 return enabled and (not self.GetExclusiveStorage() or
435 templ in constants.DTS_EXCL_STORAGE)
437 def AreSpindlesSupported(self):
438 """Are spindles supported by the current configuration?
441 return self.GetExclusiveStorage()
443 def GetVclusterSettings(self):
444 """Returns settings for virtual cluster.
447 master = self.get(_VCLUSTER_MASTER_KEY)
448 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
450 return (master, basedir)
452 def GetDiskOptions(self):
453 """Return options for the disks of the instances.
455 Get 'disks' parameter from the configuration data. If 'disks' is missing,
456 try to create it from the legacy 'disk' and 'disk-growth' parameters.
460 return self._data["disks"]
465 sizes = self._data.get("disk")
466 growths = self._data.get("disk-growth")
468 if (sizes is None or growths is None or len(sizes) != len(growths)):
469 raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
470 " exist and have the same number of items")
472 for (size, growth) in zip(sizes, growths):
473 disks.append({"size": size, "growth": growth})
480 """Loads the passed configuration file.
483 global _config # pylint: disable=W0603
485 _config = _QaConfig.Load(path)
489 """Returns the configuration object.
493 raise RuntimeError("Configuration not yet loaded")
498 def get(name, default=None):
499 """Wrapper for L{_QaConfig.get}.
502 return GetConfig().get(name, default=default)
506 def __init__(self, tests):
507 """Initializes this class.
509 @type tests: list or string
510 @param tests: List of test names
511 @see: L{TestEnabled} for details
517 def _MakeSequence(value):
518 """Make sequence of single argument.
520 If the single argument is not already a list or tuple, a list with the
521 argument as a single item is returned.
524 if isinstance(value, (list, tuple)):
530 def _TestEnabledInner(check_fn, names, fn):
531 """Evaluate test conditions.
533 @type check_fn: callable
534 @param check_fn: Callback to check whether a test is enabled
535 @type names: sequence or string
536 @param names: Test name(s)
538 @param fn: Aggregation function
540 @return: Whether test is enabled
543 names = _MakeSequence(names)
548 if isinstance(name, Either):
549 value = _TestEnabledInner(check_fn, name.tests, compat.any)
550 elif isinstance(name, (list, tuple)):
551 value = _TestEnabledInner(check_fn, name, compat.all)
555 value = check_fn(name)
562 def TestEnabled(tests, _cfg=None):
563 """Returns True if the given tests are enabled.
565 @param tests: A single test as a string, or a list of tests to check; can
566 contain L{Either} for OR conditions, AND is default
574 # Get settings for all tests
575 cfg_tests = cfg.get("tests", {})
577 # Get default setting
578 default = cfg_tests.get("default", True)
580 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
584 def GetInstanceCheckScript(*args):
585 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
588 return GetConfig().GetInstanceCheckScript(*args)
591 def GetEnabledHypervisors(*args):
592 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
595 return GetConfig().GetEnabledHypervisors(*args)
598 def GetDefaultHypervisor(*args):
599 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
602 return GetConfig().GetDefaultHypervisor(*args)
605 def GetEnabledDiskTemplates(*args):
606 """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
609 return GetConfig().GetEnabledDiskTemplates(*args)
612 def GetEnabledStorageTypes(*args):
613 """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
616 return GetConfig().GetEnabledStorageTypes(*args)
619 def GetDefaultDiskTemplate(*args):
620 """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
623 return GetConfig().GetDefaultDiskTemplate(*args)
627 """Wrapper for L{_QaConfig.GetMasterNode}.
630 return GetConfig().GetMasterNode()
633 def AcquireInstance(_cfg=None):
634 """Returns an instance which isn't in use.
642 # Filter out unwanted instances
643 instances = filter(lambda inst: not inst.used, cfg["instances"])
646 raise qa_error.OutOfInstancesError("No instances left")
648 instance = instances[0]
654 def SetExclusiveStorage(value):
655 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
658 return GetConfig().SetExclusiveStorage(value)
661 def GetExclusiveStorage():
662 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
665 return GetConfig().GetExclusiveStorage()
668 def IsTemplateSupported(templ):
669 """Wrapper for L{_QaConfig.IsTemplateSupported}.
672 return GetConfig().IsTemplateSupported(templ)
675 def AreSpindlesSupported():
676 """Wrapper for L{_QaConfig.AreSpindlesSupported}.
679 return GetConfig().AreSpindlesSupported()
682 def _NodeSortKey(node):
683 """Returns sort key for a node.
685 @type node: L{_QaNode}
688 return (node.use_count, utils.NiceSortKey(node.primary))
691 def AcquireNode(exclude=None, _cfg=None):
692 """Returns the least used node.
700 master = cfg.GetMasterNode()
702 # Filter out unwanted nodes
703 # TODO: Maybe combine filters
705 nodes = cfg["nodes"][:]
706 elif isinstance(exclude, (list, tuple)):
707 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
709 nodes = filter(lambda node: node != exclude, cfg["nodes"])
711 nodes = filter(lambda node: node.added or node == master, nodes)
714 raise qa_error.OutOfNodesError("No nodes left")
716 # Return node with least number of uses
717 return sorted(nodes, key=_NodeSortKey)[0].Use()
720 def AcquireManyNodes(num, exclude=None):
721 """Return the least used nodes.
724 @param num: Number of nodes; can be 0.
725 @type exclude: list of nodes or C{None}
726 @param exclude: nodes to be excluded from the choice
727 @rtype: list of nodes
728 @return: C{num} different nodes
734 elif isinstance(exclude, (list, tuple)):
735 # Don't modify the incoming argument
736 exclude = list(exclude)
741 for _ in range(0, num):
742 n = AcquireNode(exclude=exclude)
745 except qa_error.OutOfNodesError:
746 ReleaseManyNodes(nodes)
751 def ReleaseManyNodes(nodes):
756 def GetVclusterSettings():
757 """Wrapper for L{_QaConfig.GetVclusterSettings}.
760 return GetConfig().GetVclusterSettings()
763 def UseVirtualCluster(_cfg=None):
764 """Returns whether a virtual cluster is used.
774 (master, _) = cfg.GetVclusterSettings()
779 @ht.WithDesc("No virtual cluster")
780 def NoVirtualCluster():
781 """Used to disable tests for virtual clusters.
784 return not UseVirtualCluster()
787 def GetDiskOptions():
788 """Wrapper for L{_QaConfig.GetDiskOptions}.
791 return GetConfig().GetDiskOptions()