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 GetDefaultDiskTemplate(self):
376 """Returns the default disk template to be used.
379 return self.GetEnabledDiskTemplates()[0]
381 def _GetStringListParameter(self, key, default_values):
382 """Retrieves a parameter's value that is supposed to be a list of strings.
388 value = self._data[key]
390 return default_values
394 elif isinstance(value, basestring):
395 return value.split(",")
399 def SetExclusiveStorage(self, value):
400 """Set the expected value of the C{exclusive_storage} flag for the cluster.
403 self._exclusive_storage = bool(value)
405 def GetExclusiveStorage(self):
406 """Get the expected value of the C{exclusive_storage} flag for the cluster.
409 value = self._exclusive_storage
410 assert value is not None
413 def IsTemplateSupported(self, templ):
414 """Is the given disk template supported by the current configuration?
417 enabled = templ in self.GetEnabledDiskTemplates()
418 return enabled and (not self.GetExclusiveStorage() or
419 templ in constants.DTS_EXCL_STORAGE)
421 def AreSpindlesSupported(self):
422 """Are spindles supported by the current configuration?
425 return self.GetExclusiveStorage()
427 def GetVclusterSettings(self):
428 """Returns settings for virtual cluster.
431 master = self.get(_VCLUSTER_MASTER_KEY)
432 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
434 return (master, basedir)
436 def GetDiskOptions(self):
437 """Return options for the disks of the instances.
439 Get 'disks' parameter from the configuration data. If 'disks' is missing,
440 try to create it from the legacy 'disk' and 'disk-growth' parameters.
444 return self._data["disks"]
449 sizes = self._data.get("disk")
450 growths = self._data.get("disk-growth")
452 if (sizes is None or growths is None or len(sizes) != len(growths)):
453 raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
454 " exist and have the same number of items")
456 for (size, growth) in zip(sizes, growths):
457 disks.append({"size": size, "growth": growth})
464 """Loads the passed configuration file.
467 global _config # pylint: disable=W0603
469 _config = _QaConfig.Load(path)
473 """Returns the configuration object.
477 raise RuntimeError("Configuration not yet loaded")
482 def get(name, default=None):
483 """Wrapper for L{_QaConfig.get}.
486 return GetConfig().get(name, default=default)
490 def __init__(self, tests):
491 """Initializes this class.
493 @type tests: list or string
494 @param tests: List of test names
495 @see: L{TestEnabled} for details
501 def _MakeSequence(value):
502 """Make sequence of single argument.
504 If the single argument is not already a list or tuple, a list with the
505 argument as a single item is returned.
508 if isinstance(value, (list, tuple)):
514 def _TestEnabledInner(check_fn, names, fn):
515 """Evaluate test conditions.
517 @type check_fn: callable
518 @param check_fn: Callback to check whether a test is enabled
519 @type names: sequence or string
520 @param names: Test name(s)
522 @param fn: Aggregation function
524 @return: Whether test is enabled
527 names = _MakeSequence(names)
532 if isinstance(name, Either):
533 value = _TestEnabledInner(check_fn, name.tests, compat.any)
534 elif isinstance(name, (list, tuple)):
535 value = _TestEnabledInner(check_fn, name, compat.all)
539 value = check_fn(name)
546 def TestEnabled(tests, _cfg=None):
547 """Returns True if the given tests are enabled.
549 @param tests: A single test as a string, or a list of tests to check; can
550 contain L{Either} for OR conditions, AND is default
558 # Get settings for all tests
559 cfg_tests = cfg.get("tests", {})
561 # Get default setting
562 default = cfg_tests.get("default", True)
564 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
568 def GetInstanceCheckScript(*args):
569 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
572 return GetConfig().GetInstanceCheckScript(*args)
575 def GetEnabledHypervisors(*args):
576 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
579 return GetConfig().GetEnabledHypervisors(*args)
582 def GetDefaultHypervisor(*args):
583 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
586 return GetConfig().GetDefaultHypervisor(*args)
589 def GetEnabledDiskTemplates(*args):
590 """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
593 return GetConfig().GetEnabledDiskTemplates(*args)
596 def GetDefaultDiskTemplate(*args):
597 """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
600 return GetConfig().GetDefaultDiskTemplate(*args)
604 """Wrapper for L{_QaConfig.GetMasterNode}.
607 return GetConfig().GetMasterNode()
610 def AcquireInstance(_cfg=None):
611 """Returns an instance which isn't in use.
619 # Filter out unwanted instances
620 instances = filter(lambda inst: not inst.used, cfg["instances"])
623 raise qa_error.OutOfInstancesError("No instances left")
625 instance = instances[0]
631 def SetExclusiveStorage(value):
632 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
635 return GetConfig().SetExclusiveStorage(value)
638 def GetExclusiveStorage():
639 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
642 return GetConfig().GetExclusiveStorage()
645 def IsTemplateSupported(templ):
646 """Wrapper for L{_QaConfig.IsTemplateSupported}.
649 return GetConfig().IsTemplateSupported(templ)
652 def AreSpindlesSupported():
653 """Wrapper for L{_QaConfig.AreSpindlesSupported}.
656 return GetConfig().AreSpindlesSupported()
659 def _NodeSortKey(node):
660 """Returns sort key for a node.
662 @type node: L{_QaNode}
665 return (node.use_count, utils.NiceSortKey(node.primary))
668 def AcquireNode(exclude=None, _cfg=None):
669 """Returns the least used node.
677 master = cfg.GetMasterNode()
679 # Filter out unwanted nodes
680 # TODO: Maybe combine filters
682 nodes = cfg["nodes"][:]
683 elif isinstance(exclude, (list, tuple)):
684 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
686 nodes = filter(lambda node: node != exclude, cfg["nodes"])
688 nodes = filter(lambda node: node.added or node == master, nodes)
691 raise qa_error.OutOfNodesError("No nodes left")
693 # Return node with least number of uses
694 return sorted(nodes, key=_NodeSortKey)[0].Use()
697 def AcquireManyNodes(num, exclude=None):
698 """Return the least used nodes.
701 @param num: Number of nodes; can be 0.
702 @type exclude: list of nodes or C{None}
703 @param exclude: nodes to be excluded from the choice
704 @rtype: list of nodes
705 @return: C{num} different nodes
711 elif isinstance(exclude, (list, tuple)):
712 # Don't modify the incoming argument
713 exclude = list(exclude)
718 for _ in range(0, num):
719 n = AcquireNode(exclude=exclude)
722 except qa_error.OutOfNodesError:
723 ReleaseManyNodes(nodes)
728 def ReleaseManyNodes(nodes):
733 def GetVclusterSettings():
734 """Wrapper for L{_QaConfig.GetVclusterSettings}.
737 return GetConfig().GetVclusterSettings()
740 def UseVirtualCluster(_cfg=None):
741 """Returns whether a virtual cluster is used.
751 (master, _) = cfg.GetVclusterSettings()
756 @ht.WithDesc("No virtual cluster")
757 def NoVirtualCluster():
758 """Used to disable tests for virtual clusters.
761 return not UseVirtualCluster()
764 def GetDiskOptions():
765 """Wrapper for L{_QaConfig.GetDiskOptions}.
768 return GetConfig().GetDiskOptions()