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 list(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 GetVclusterSettings(self):
422 """Returns settings for virtual cluster.
425 master = self.get(_VCLUSTER_MASTER_KEY)
426 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
428 return (master, basedir)
430 def GetDiskOptions(self):
431 """Return options for the disks of the instances.
433 Get 'disks' parameter from the configuration data. If 'disks' is missing,
434 try to create it from the legacy 'disk' and 'disk-growth' parameters.
438 return self._data["disks"]
443 sizes = self._data.get("disk")
444 growths = self._data.get("disk-growth")
446 if (sizes is None or growths is None or len(sizes) != len(growths)):
447 raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
448 " exist and have the same number of items")
450 for (size, growth) in zip(sizes, growths):
451 disks.append({"size": size, "growth": growth})
458 """Loads the passed configuration file.
461 global _config # pylint: disable=W0603
463 _config = _QaConfig.Load(path)
467 """Returns the configuration object.
471 raise RuntimeError("Configuration not yet loaded")
476 def get(name, default=None):
477 """Wrapper for L{_QaConfig.get}.
480 return GetConfig().get(name, default=default)
484 def __init__(self, tests):
485 """Initializes this class.
487 @type tests: list or string
488 @param tests: List of test names
489 @see: L{TestEnabled} for details
495 def _MakeSequence(value):
496 """Make sequence of single argument.
498 If the single argument is not already a list or tuple, a list with the
499 argument as a single item is returned.
502 if isinstance(value, (list, tuple)):
508 def _TestEnabledInner(check_fn, names, fn):
509 """Evaluate test conditions.
511 @type check_fn: callable
512 @param check_fn: Callback to check whether a test is enabled
513 @type names: sequence or string
514 @param names: Test name(s)
516 @param fn: Aggregation function
518 @return: Whether test is enabled
521 names = _MakeSequence(names)
526 if isinstance(name, Either):
527 value = _TestEnabledInner(check_fn, name.tests, compat.any)
528 elif isinstance(name, (list, tuple)):
529 value = _TestEnabledInner(check_fn, name, compat.all)
533 value = check_fn(name)
540 def TestEnabled(tests, _cfg=None):
541 """Returns True if the given tests are enabled.
543 @param tests: A single test as a string, or a list of tests to check; can
544 contain L{Either} for OR conditions, AND is default
552 # Get settings for all tests
553 cfg_tests = cfg.get("tests", {})
555 # Get default setting
556 default = cfg_tests.get("default", True)
558 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
562 def GetInstanceCheckScript(*args):
563 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
566 return GetConfig().GetInstanceCheckScript(*args)
569 def GetEnabledHypervisors(*args):
570 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
573 return GetConfig().GetEnabledHypervisors(*args)
576 def GetDefaultHypervisor(*args):
577 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
580 return GetConfig().GetDefaultHypervisor(*args)
583 def GetEnabledDiskTemplates(*args):
584 """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
587 return GetConfig().GetEnabledDiskTemplates(*args)
590 def GetDefaultDiskTemplate(*args):
591 """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
594 return GetConfig().GetDefaultDiskTemplate(*args)
598 """Wrapper for L{_QaConfig.GetMasterNode}.
601 return GetConfig().GetMasterNode()
604 def AcquireInstance(_cfg=None):
605 """Returns an instance which isn't in use.
613 # Filter out unwanted instances
614 instances = filter(lambda inst: not inst.used, cfg["instances"])
617 raise qa_error.OutOfInstancesError("No instances left")
619 instance = instances[0]
625 def SetExclusiveStorage(value):
626 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
629 return GetConfig().SetExclusiveStorage(value)
632 def GetExclusiveStorage():
633 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
636 return GetConfig().GetExclusiveStorage()
639 def IsTemplateSupported(templ):
640 """Wrapper for L{_QaConfig.IsTemplateSupported}.
643 return GetConfig().IsTemplateSupported(templ)
646 def _NodeSortKey(node):
647 """Returns sort key for a node.
649 @type node: L{_QaNode}
652 return (node.use_count, utils.NiceSortKey(node.primary))
655 def AcquireNode(exclude=None, _cfg=None):
656 """Returns the least used node.
664 master = cfg.GetMasterNode()
666 # Filter out unwanted nodes
667 # TODO: Maybe combine filters
669 nodes = cfg["nodes"][:]
670 elif isinstance(exclude, (list, tuple)):
671 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
673 nodes = filter(lambda node: node != exclude, cfg["nodes"])
675 nodes = filter(lambda node: node.added or node == master, nodes)
678 raise qa_error.OutOfNodesError("No nodes left")
680 # Return node with least number of uses
681 return sorted(nodes, key=_NodeSortKey)[0].Use()
684 def AcquireManyNodes(num, exclude=None):
685 """Return the least used nodes.
688 @param num: Number of nodes; can be 0.
689 @type exclude: list of nodes or C{None}
690 @param exclude: nodes to be excluded from the choice
691 @rtype: list of nodes
692 @return: C{num} different nodes
698 elif isinstance(exclude, (list, tuple)):
699 # Don't modify the incoming argument
700 exclude = list(exclude)
705 for _ in range(0, num):
706 n = AcquireNode(exclude=exclude)
709 except qa_error.OutOfNodesError:
710 ReleaseManyNodes(nodes)
715 def ReleaseManyNodes(nodes):
720 def GetVclusterSettings():
721 """Wrapper for L{_QaConfig.GetVclusterSettings}.
724 return GetConfig().GetVclusterSettings()
727 def UseVirtualCluster(_cfg=None):
728 """Returns whether a virtual cluster is used.
738 (master, _) = cfg.GetVclusterSettings()
743 @ht.WithDesc("No virtual cluster")
744 def NoVirtualCluster():
745 """Used to disable tests for virtual clusters.
748 return not UseVirtualCluster()
751 def GetDiskOptions():
752 """Wrapper for L{_QaConfig.GetDiskOptions}.
755 return GetConfig().GetDiskOptions()