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
36 _INSTANCE_CHECK_KEY = "instance-check"
37 _ENABLED_HV_KEY = "enabled-hypervisors"
39 #: QA configuration (L{_QaConfig})
43 class _QaInstance(object):
51 def __init__(self, name, nicmac):
52 """Initializes instances of this class.
58 self._disk_template = None
61 def FromDict(cls, data):
62 """Creates instance object from JSON dictionary.
67 macaddr = data.get("nic.mac/0")
69 nicmac.append(macaddr)
71 return cls(name=data["name"], nicmac=nicmac)
74 """Releases instance and makes it available again.
78 ("Instance '%s' was never acquired or released more than once" %
82 self._disk_template = None
84 def GetNicMacAddr(self, idx, default):
85 """Returns MAC address for NIC.
89 @param default: Default value
92 if len(self.nicmac) > idx:
93 return self.nicmac[idx]
97 def SetDiskTemplate(self, template):
98 """Set the disk template.
101 assert template in constants.DISK_TEMPLATES
103 self._disk_template = template
106 def disk_template(self):
107 """Returns the current disk template.
110 return self._disk_template
113 class _QaNode(object):
121 def __init__(self, primary, secondary):
122 """Initializes instances of this class.
125 self.primary = primary
126 self.secondary = secondary
131 def FromDict(cls, data):
132 """Creates node object from JSON dictionary.
135 return cls(primary=data["primary"], secondary=data.get("secondary"))
138 """Marks a node as being in use.
141 assert self._use_count >= 0
148 """Release a node (opposite of L{Use}).
151 assert self.use_count > 0
156 """Marks node as having been added to a cluster.
159 assert not self._added
162 def MarkRemoved(self):
163 """Marks node as having been removed from a cluster.
171 """Returns whether a node is part of a cluster.
178 """Returns number of current uses (controlled by L{Use} and L{Release}).
181 return self._use_count
184 _RESOURCE_CONVERTER = {
185 "instances": _QaInstance.FromDict,
186 "nodes": _QaNode.FromDict,
190 def _ConvertResources((key, value)):
191 """Converts cluster resources in configuration to Python objects.
194 fn = _RESOURCE_CONVERTER.get(key, None)
196 return (key, map(fn, value))
201 class _QaConfig(object):
202 def __init__(self, data):
203 """Initializes instances of this class.
208 #: Cluster-wide run-time value of the exclusive storage flag
209 self._exclusive_storage = None
212 def Load(cls, filename):
213 """Loads a configuration file and produces a configuration object.
215 @type filename: string
216 @param filename: Path to configuration file
220 data = serializer.LoadJson(utils.ReadFile(filename))
222 result = cls(dict(map(_ConvertResources,
223 data.items()))) # pylint: disable=E1103
229 """Validates loaded configuration data.
232 if not self.get("nodes"):
233 raise qa_error.Error("Need at least one node")
235 if not self.get("instances"):
236 raise qa_error.Error("Need at least one instance")
238 if (self.get("disk") is None or
239 self.get("disk-growth") is None or
240 len(self.get("disk")) != len(self.get("disk-growth"))):
241 raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
242 " and have the same number of items")
244 check = self.GetInstanceCheckScript()
248 except EnvironmentError, err:
249 raise qa_error.Error("Can't find instance check script '%s': %s" %
252 enabled_hv = frozenset(self.GetEnabledHypervisors())
254 raise qa_error.Error("No hypervisor is enabled")
256 difference = enabled_hv - constants.HYPER_TYPES
258 raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
259 utils.CommaJoin(difference))
261 def __getitem__(self, name):
262 """Returns configuration value.
265 @param name: Name of configuration entry
268 return self._data[name]
270 def get(self, name, default=None):
271 """Returns configuration value.
274 @param name: Name of configuration entry
275 @param default: Default value
278 return self._data.get(name, default)
280 def GetMasterNode(self):
281 """Returns the default master node for the cluster.
284 return self["nodes"][0]
286 def GetInstanceCheckScript(self):
287 """Returns path to instance check script or C{None}.
290 return self._data.get(_INSTANCE_CHECK_KEY, None)
292 def GetEnabledHypervisors(self):
293 """Returns list of enabled hypervisors.
299 value = self._data[_ENABLED_HV_KEY]
301 return [constants.DEFAULT_ENABLED_HYPERVISOR]
305 elif isinstance(value, basestring):
306 # The configuration key ("enabled-hypervisors") implies there can be
307 # multiple values. Multiple hypervisors are comma-separated on the
308 # command line option to "gnt-cluster init", so we need to handle them
310 return value.split(",")
314 def GetDefaultHypervisor(self):
315 """Returns the default hypervisor to be used.
318 return self.GetEnabledHypervisors()[0]
320 def SetExclusiveStorage(self, value):
321 """Set the expected value of the C{exclusive_storage} flag for the cluster.
324 self._exclusive_storage = bool(value)
326 def GetExclusiveStorage(self):
327 """Get the expected value of the C{exclusive_storage} flag for the cluster.
330 value = self._exclusive_storage
331 assert value is not None
334 def IsTemplateSupported(self, templ):
335 """Is the given disk template supported by the current configuration?
338 return (not self.GetExclusiveStorage() or
339 templ in constants.DTS_EXCL_STORAGE)
343 """Loads the passed configuration file.
346 global _config # pylint: disable=W0603
348 _config = _QaConfig.Load(path)
352 """Returns the configuration object.
356 raise RuntimeError("Configuration not yet loaded")
361 def get(name, default=None):
362 """Wrapper for L{_QaConfig.get}.
365 return GetConfig().get(name, default=default)
369 def __init__(self, tests):
370 """Initializes this class.
372 @type tests: list or string
373 @param tests: List of test names
374 @see: L{TestEnabled} for details
380 def _MakeSequence(value):
381 """Make sequence of single argument.
383 If the single argument is not already a list or tuple, a list with the
384 argument as a single item is returned.
387 if isinstance(value, (list, tuple)):
393 def _TestEnabledInner(check_fn, names, fn):
394 """Evaluate test conditions.
396 @type check_fn: callable
397 @param check_fn: Callback to check whether a test is enabled
398 @type names: sequence or string
399 @param names: Test name(s)
401 @param fn: Aggregation function
403 @return: Whether test is enabled
406 names = _MakeSequence(names)
411 if isinstance(name, Either):
412 value = _TestEnabledInner(check_fn, name.tests, compat.any)
413 elif isinstance(name, (list, tuple)):
414 value = _TestEnabledInner(check_fn, name, compat.all)
416 value = check_fn(name)
423 def TestEnabled(tests, _cfg=None):
424 """Returns True if the given tests are enabled.
426 @param tests: A single test as a string, or a list of tests to check; can
427 contain L{Either} for OR conditions, AND is default
435 # Get settings for all tests
436 cfg_tests = cfg.get("tests", {})
438 # Get default setting
439 default = cfg_tests.get("default", True)
441 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
445 def GetInstanceCheckScript(*args):
446 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
449 return GetConfig().GetInstanceCheckScript(*args)
452 def GetEnabledHypervisors(*args):
453 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
456 return GetConfig().GetEnabledHypervisors(*args)
459 def GetDefaultHypervisor(*args):
460 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
463 return GetConfig().GetDefaultHypervisor(*args)
467 """Wrapper for L{_QaConfig.GetMasterNode}.
470 return GetConfig().GetMasterNode()
473 def AcquireInstance(_cfg=None):
474 """Returns an instance which isn't in use.
482 # Filter out unwanted instances
483 instances = filter(lambda inst: not inst.used, cfg["instances"])
486 raise qa_error.OutOfInstancesError("No instances left")
491 assert inst.disk_template is None
498 def SetExclusiveStorage(value):
499 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
502 return GetConfig().SetExclusiveStorage(value)
505 def GetExclusiveStorage():
506 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
509 return GetConfig().GetExclusiveStorage()
512 def IsTemplateSupported(templ):
513 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
516 return GetConfig().IsTemplateSupported(templ)
519 def AcquireNode(exclude=None, _cfg=None):
520 """Returns the least used node.
528 master = cfg.GetMasterNode()
530 # Filter out unwanted nodes
531 # TODO: Maybe combine filters
533 nodes = cfg["nodes"][:]
534 elif isinstance(exclude, (list, tuple)):
535 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
537 nodes = filter(lambda node: node != exclude, cfg["nodes"])
539 nodes = filter(lambda node: node.added or node == master, nodes)
542 raise qa_error.OutOfNodesError("No nodes left")
544 # Get node with least number of uses
545 # TODO: Switch to computing sort key instead of comparing directly
547 result = cmp(a.use_count, b.use_count)
549 result = cmp(a.primary, b.primary)
552 nodes.sort(cmp=compare)
554 return nodes[0].Use()
557 def AcquireManyNodes(num, exclude=None):
558 """Return the least used nodes.
561 @param num: Number of nodes; can be 0.
562 @type exclude: list of nodes or C{None}
563 @param exclude: nodes to be excluded from the choice
564 @rtype: list of nodes
565 @return: C{num} different nodes
571 elif isinstance(exclude, (list, tuple)):
572 # Don't modify the incoming argument
573 exclude = list(exclude)
578 for _ in range(0, num):
579 n = AcquireNode(exclude=exclude)
582 except qa_error.OutOfNodesError:
583 ReleaseManyNodes(nodes)
588 def ReleaseManyNodes(nodes):