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"
42 #: QA configuration (L{_QaConfig})
46 class _QaInstance(object):
54 def __init__(self, name, nicmac):
55 """Initializes instances of this class.
61 self._disk_template = None
64 def FromDict(cls, data):
65 """Creates instance object from JSON dictionary.
70 macaddr = data.get("nic.mac/0")
72 nicmac.append(macaddr)
74 return cls(name=data["name"], nicmac=nicmac)
78 "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
79 "name=%s" % self.name,
80 "nicmac=%s" % self.nicmac,
81 "used=%s" % self._used,
82 "disk_template=%s" % self._disk_template,
85 return "<%s at %#x>" % (" ".join(status), id(self))
88 """Marks instance as being in use.
92 assert self._disk_template is None
97 """Releases instance and makes it available again.
101 ("Instance '%s' was never acquired or released more than once" %
105 self._disk_template = None
107 def GetNicMacAddr(self, idx, default):
108 """Returns MAC address for NIC.
111 @param idx: NIC index
112 @param default: Default value
115 if len(self.nicmac) > idx:
116 return self.nicmac[idx]
120 def SetDiskTemplate(self, template):
121 """Set the disk template.
124 assert template in constants.DISK_TEMPLATES
126 self._disk_template = template
130 """Returns boolean denoting whether instance is in use.
136 def disk_template(self):
137 """Returns the current disk template.
140 return self._disk_template
143 class _QaNode(object):
151 def __init__(self, primary, secondary):
152 """Initializes instances of this class.
155 self.primary = primary
156 self.secondary = secondary
161 def FromDict(cls, data):
162 """Creates node object from JSON dictionary.
165 return cls(primary=data["primary"], secondary=data.get("secondary"))
169 "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
170 "primary=%s" % self.primary,
171 "secondary=%s" % self.secondary,
172 "added=%s" % self._added,
173 "use_count=%s" % self._use_count,
176 return "<%s at %#x>" % (" ".join(status), id(self))
179 """Marks a node as being in use.
182 assert self._use_count >= 0
189 """Release a node (opposite of L{Use}).
192 assert self.use_count > 0
197 """Marks node as having been added to a cluster.
200 assert not self._added
203 def MarkRemoved(self):
204 """Marks node as having been removed from a cluster.
212 """Returns whether a node is part of a cluster.
219 """Returns number of current uses (controlled by L{Use} and L{Release}).
222 return self._use_count
225 _RESOURCE_CONVERTER = {
226 "instances": _QaInstance.FromDict,
227 "nodes": _QaNode.FromDict,
231 def _ConvertResources((key, value)):
232 """Converts cluster resources in configuration to Python objects.
235 fn = _RESOURCE_CONVERTER.get(key, None)
237 return (key, map(fn, value))
242 class _QaConfig(object):
243 def __init__(self, data):
244 """Initializes instances of this class.
249 #: Cluster-wide run-time value of the exclusive storage flag
250 self._exclusive_storage = None
253 def Load(cls, filename):
254 """Loads a configuration file and produces a configuration object.
256 @type filename: string
257 @param filename: Path to configuration file
261 data = serializer.LoadJson(utils.ReadFile(filename))
263 result = cls(dict(map(_ConvertResources,
264 data.items()))) # pylint: disable=E1103
270 """Validates loaded configuration data.
273 if not self.get("name"):
274 raise qa_error.Error("Cluster name is required")
276 if not self.get("nodes"):
277 raise qa_error.Error("Need at least one node")
279 if not self.get("instances"):
280 raise qa_error.Error("Need at least one instance")
282 if (self.get("disk") is None or
283 self.get("disk-growth") is None or
284 len(self.get("disk")) != len(self.get("disk-growth"))):
285 raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
286 " and have the same number of items")
288 check = self.GetInstanceCheckScript()
292 except EnvironmentError, err:
293 raise qa_error.Error("Can't find instance check script '%s': %s" %
296 enabled_hv = frozenset(self.GetEnabledHypervisors())
298 raise qa_error.Error("No hypervisor is enabled")
300 difference = enabled_hv - constants.HYPER_TYPES
302 raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
303 utils.CommaJoin(difference))
305 (vc_master, vc_basedir) = self.GetVclusterSettings()
306 if bool(vc_master) != bool(vc_basedir):
307 raise qa_error.Error("All or none of the config options '%s' and '%s'"
309 (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
311 if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
312 raise qa_error.Error("Path given in option '%s' must be absolute and"
313 " normalized" % _VCLUSTER_BASEDIR_KEY)
315 def __getitem__(self, name):
316 """Returns configuration value.
319 @param name: Name of configuration entry
322 return self._data[name]
324 def get(self, name, default=None):
325 """Returns configuration value.
328 @param name: Name of configuration entry
329 @param default: Default value
332 return self._data.get(name, default)
334 def GetMasterNode(self):
335 """Returns the default master node for the cluster.
338 return self["nodes"][0]
340 def GetInstanceCheckScript(self):
341 """Returns path to instance check script or C{None}.
344 return self._data.get(_INSTANCE_CHECK_KEY, None)
346 def GetEnabledHypervisors(self):
347 """Returns list of enabled hypervisors.
353 value = self._data[_ENABLED_HV_KEY]
355 return [constants.DEFAULT_ENABLED_HYPERVISOR]
359 elif isinstance(value, basestring):
360 # The configuration key ("enabled-hypervisors") implies there can be
361 # multiple values. Multiple hypervisors are comma-separated on the
362 # command line option to "gnt-cluster init", so we need to handle them
364 return value.split(",")
368 def GetDefaultHypervisor(self):
369 """Returns the default hypervisor to be used.
372 return self.GetEnabledHypervisors()[0]
374 def SetExclusiveStorage(self, value):
375 """Set the expected value of the C{exclusive_storage} flag for the cluster.
378 self._exclusive_storage = bool(value)
380 def GetExclusiveStorage(self):
381 """Get the expected value of the C{exclusive_storage} flag for the cluster.
384 value = self._exclusive_storage
385 assert value is not None
388 def IsTemplateSupported(self, templ):
389 """Is the given disk template supported by the current configuration?
392 return (not self.GetExclusiveStorage() or
393 templ in constants.DTS_EXCL_STORAGE)
395 def GetVclusterSettings(self):
396 """Returns settings for virtual cluster.
399 master = self.get(_VCLUSTER_MASTER_KEY)
400 basedir = self.get(_VCLUSTER_BASEDIR_KEY)
402 return (master, basedir)
406 """Loads the passed configuration file.
409 global _config # pylint: disable=W0603
411 _config = _QaConfig.Load(path)
415 """Returns the configuration object.
419 raise RuntimeError("Configuration not yet loaded")
424 def get(name, default=None):
425 """Wrapper for L{_QaConfig.get}.
428 return GetConfig().get(name, default=default)
432 def __init__(self, tests):
433 """Initializes this class.
435 @type tests: list or string
436 @param tests: List of test names
437 @see: L{TestEnabled} for details
443 def _MakeSequence(value):
444 """Make sequence of single argument.
446 If the single argument is not already a list or tuple, a list with the
447 argument as a single item is returned.
450 if isinstance(value, (list, tuple)):
456 def _TestEnabledInner(check_fn, names, fn):
457 """Evaluate test conditions.
459 @type check_fn: callable
460 @param check_fn: Callback to check whether a test is enabled
461 @type names: sequence or string
462 @param names: Test name(s)
464 @param fn: Aggregation function
466 @return: Whether test is enabled
469 names = _MakeSequence(names)
474 if isinstance(name, Either):
475 value = _TestEnabledInner(check_fn, name.tests, compat.any)
476 elif isinstance(name, (list, tuple)):
477 value = _TestEnabledInner(check_fn, name, compat.all)
481 value = check_fn(name)
488 def TestEnabled(tests, _cfg=None):
489 """Returns True if the given tests are enabled.
491 @param tests: A single test as a string, or a list of tests to check; can
492 contain L{Either} for OR conditions, AND is default
500 # Get settings for all tests
501 cfg_tests = cfg.get("tests", {})
503 # Get default setting
504 default = cfg_tests.get("default", True)
506 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
510 def GetInstanceCheckScript(*args):
511 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
514 return GetConfig().GetInstanceCheckScript(*args)
517 def GetEnabledHypervisors(*args):
518 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
521 return GetConfig().GetEnabledHypervisors(*args)
524 def GetDefaultHypervisor(*args):
525 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
528 return GetConfig().GetDefaultHypervisor(*args)
532 """Wrapper for L{_QaConfig.GetMasterNode}.
535 return GetConfig().GetMasterNode()
538 def AcquireInstance(_cfg=None):
539 """Returns an instance which isn't in use.
547 # Filter out unwanted instances
548 instances = filter(lambda inst: not inst.used, cfg["instances"])
551 raise qa_error.OutOfInstancesError("No instances left")
553 instance = instances[0]
559 def SetExclusiveStorage(value):
560 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
563 return GetConfig().SetExclusiveStorage(value)
566 def GetExclusiveStorage():
567 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
570 return GetConfig().GetExclusiveStorage()
573 def IsTemplateSupported(templ):
574 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
577 return GetConfig().IsTemplateSupported(templ)
580 def _NodeSortKey(node):
581 """Returns sort key for a node.
583 @type node: L{_QaNode}
586 return (node.use_count, utils.NiceSortKey(node.primary))
589 def AcquireNode(exclude=None, _cfg=None):
590 """Returns the least used node.
598 master = cfg.GetMasterNode()
600 # Filter out unwanted nodes
601 # TODO: Maybe combine filters
603 nodes = cfg["nodes"][:]
604 elif isinstance(exclude, (list, tuple)):
605 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
607 nodes = filter(lambda node: node != exclude, cfg["nodes"])
609 nodes = filter(lambda node: node.added or node == master, nodes)
612 raise qa_error.OutOfNodesError("No nodes left")
614 # Return node with least number of uses
615 return sorted(nodes, key=_NodeSortKey)[0].Use()
618 def AcquireManyNodes(num, exclude=None):
619 """Return the least used nodes.
622 @param num: Number of nodes; can be 0.
623 @type exclude: list of nodes or C{None}
624 @param exclude: nodes to be excluded from the choice
625 @rtype: list of nodes
626 @return: C{num} different nodes
632 elif isinstance(exclude, (list, tuple)):
633 # Don't modify the incoming argument
634 exclude = list(exclude)
639 for _ in range(0, num):
640 n = AcquireNode(exclude=exclude)
643 except qa_error.OutOfNodesError:
644 ReleaseManyNodes(nodes)
649 def ReleaseManyNodes(nodes):
654 def GetVclusterSettings():
655 """Wrapper for L{_QaConfig.GetVclusterSettings}.
658 return GetConfig().GetVclusterSettings()
661 def UseVirtualCluster(_cfg=None):
662 """Returns whether a virtual cluster is used.
672 (master, _) = cfg.GetVclusterSettings()
677 @ht.WithDesc("No virtual cluster")
678 def NoVirtualCluster():
679 """Used to disable tests for virtual clusters.
682 return not UseVirtualCluster()