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)
73 def __getitem__(self, key):
74 """Legacy dict-like interface.
82 def get(self, key, default):
83 """Legacy dict-like interface.
91 def GetNicMacAddr(self, idx, default):
92 """Returns MAC address for NIC.
96 @param default: Default value
99 if len(self.nicmac) > idx:
100 return self.nicmac[idx]
105 _RESOURCE_CONVERTER = {
106 "instances": _QaInstance.FromDict,
110 def _ConvertResources((key, value)):
111 """Converts cluster resources in configuration to Python objects.
114 fn = _RESOURCE_CONVERTER.get(key, None)
116 return (key, map(fn, value))
121 class _QaConfig(object):
122 def __init__(self, data):
123 """Initializes instances of this class.
128 #: Cluster-wide run-time value of the exclusive storage flag
129 self._exclusive_storage = None
132 def Load(cls, filename):
133 """Loads a configuration file and produces a configuration object.
135 @type filename: string
136 @param filename: Path to configuration file
140 data = serializer.LoadJson(utils.ReadFile(filename))
142 result = cls(dict(map(_ConvertResources,
143 data.items()))) # pylint: disable=E1103
149 """Validates loaded configuration data.
152 if not self.get("nodes"):
153 raise qa_error.Error("Need at least one node")
155 if not self.get("instances"):
156 raise qa_error.Error("Need at least one instance")
158 if (self.get("disk") is None or
159 self.get("disk-growth") is None or
160 len(self.get("disk")) != len(self.get("disk-growth"))):
161 raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
162 " and have the same number of items")
164 check = self.GetInstanceCheckScript()
168 except EnvironmentError, err:
169 raise qa_error.Error("Can't find instance check script '%s': %s" %
172 enabled_hv = frozenset(self.GetEnabledHypervisors())
174 raise qa_error.Error("No hypervisor is enabled")
176 difference = enabled_hv - constants.HYPER_TYPES
178 raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
179 utils.CommaJoin(difference))
181 def __getitem__(self, name):
182 """Returns configuration value.
185 @param name: Name of configuration entry
188 return self._data[name]
190 def get(self, name, default=None):
191 """Returns configuration value.
194 @param name: Name of configuration entry
195 @param default: Default value
198 return self._data.get(name, default)
200 def GetMasterNode(self):
201 """Returns the default master node for the cluster.
204 return self["nodes"][0]
206 def GetInstanceCheckScript(self):
207 """Returns path to instance check script or C{None}.
210 return self._data.get(_INSTANCE_CHECK_KEY, None)
212 def GetEnabledHypervisors(self):
213 """Returns list of enabled hypervisors.
219 value = self._data[_ENABLED_HV_KEY]
221 return [constants.DEFAULT_ENABLED_HYPERVISOR]
225 elif isinstance(value, basestring):
226 # The configuration key ("enabled-hypervisors") implies there can be
227 # multiple values. Multiple hypervisors are comma-separated on the
228 # command line option to "gnt-cluster init", so we need to handle them
230 return value.split(",")
234 def GetDefaultHypervisor(self):
235 """Returns the default hypervisor to be used.
238 return self.GetEnabledHypervisors()[0]
240 def SetExclusiveStorage(self, value):
241 """Set the expected value of the C{exclusive_storage} flag for the cluster.
244 self._exclusive_storage = bool(value)
246 def GetExclusiveStorage(self):
247 """Get the expected value of the C{exclusive_storage} flag for the cluster.
250 value = self._exclusive_storage
251 assert value is not None
254 def IsTemplateSupported(self, templ):
255 """Is the given disk template supported by the current configuration?
258 return (not self.GetExclusiveStorage() or
259 templ in constants.DTS_EXCL_STORAGE)
263 """Loads the passed configuration file.
266 global _config # pylint: disable=W0603
268 _config = _QaConfig.Load(path)
272 """Returns the configuration object.
276 raise RuntimeError("Configuration not yet loaded")
281 def get(name, default=None):
282 """Wrapper for L{_QaConfig.get}.
285 return GetConfig().get(name, default=default)
289 def __init__(self, tests):
290 """Initializes this class.
292 @type tests: list or string
293 @param tests: List of test names
294 @see: L{TestEnabled} for details
300 def _MakeSequence(value):
301 """Make sequence of single argument.
303 If the single argument is not already a list or tuple, a list with the
304 argument as a single item is returned.
307 if isinstance(value, (list, tuple)):
313 def _TestEnabledInner(check_fn, names, fn):
314 """Evaluate test conditions.
316 @type check_fn: callable
317 @param check_fn: Callback to check whether a test is enabled
318 @type names: sequence or string
319 @param names: Test name(s)
321 @param fn: Aggregation function
323 @return: Whether test is enabled
326 names = _MakeSequence(names)
331 if isinstance(name, Either):
332 value = _TestEnabledInner(check_fn, name.tests, compat.any)
333 elif isinstance(name, (list, tuple)):
334 value = _TestEnabledInner(check_fn, name, compat.all)
336 value = check_fn(name)
343 def TestEnabled(tests, _cfg=None):
344 """Returns True if the given tests are enabled.
346 @param tests: A single test as a string, or a list of tests to check; can
347 contain L{Either} for OR conditions, AND is default
355 # Get settings for all tests
356 cfg_tests = cfg.get("tests", {})
358 # Get default setting
359 default = cfg_tests.get("default", True)
361 return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
365 def GetInstanceCheckScript(*args):
366 """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
369 return GetConfig().GetInstanceCheckScript(*args)
372 def GetEnabledHypervisors(*args):
373 """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
376 return GetConfig().GetEnabledHypervisors(*args)
379 def GetDefaultHypervisor(*args):
380 """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
383 return GetConfig().GetDefaultHypervisor(*args)
386 def GetInstanceNicMac(inst, default=None):
387 """Returns MAC address for instance's network interface.
390 return inst.GetNicMacAddr(0, default)
394 """Wrapper for L{_QaConfig.GetMasterNode}.
397 return GetConfig().GetMasterNode()
400 def AcquireInstance(_cfg=None):
401 """Returns an instance which isn't in use.
409 # Filter out unwanted instances
410 instances = filter(lambda inst: not inst.used, cfg["instances"])
413 raise qa_error.OutOfInstancesError("No instances left")
418 assert inst.disk_template is None
425 def ReleaseInstance(inst):
427 inst.disk_template = None
430 def GetInstanceTemplate(inst):
431 """Return the disk template of an instance.
434 templ = inst.disk_template
435 assert templ is not None
439 def SetInstanceTemplate(inst, template):
440 """Set the disk template for an instance.
443 inst.disk_template = template
446 def SetExclusiveStorage(value):
447 """Wrapper for L{_QaConfig.SetExclusiveStorage}.
450 return GetConfig().SetExclusiveStorage(value)
453 def GetExclusiveStorage():
454 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
457 return GetConfig().GetExclusiveStorage()
460 def IsTemplateSupported(templ):
461 """Wrapper for L{_QaConfig.GetExclusiveStorage}.
464 return GetConfig().IsTemplateSupported(templ)
467 def AcquireNode(exclude=None):
468 """Returns the least used node.
471 master = GetMasterNode()
474 # Filter out unwanted nodes
475 # TODO: Maybe combine filters
477 nodes = cfg["nodes"][:]
478 elif isinstance(exclude, (list, tuple)):
479 nodes = filter(lambda node: node not in exclude, cfg["nodes"])
481 nodes = filter(lambda node: node != exclude, cfg["nodes"])
483 tmp_flt = lambda node: node.get("_added", False) or node == master
484 nodes = filter(tmp_flt, nodes)
488 raise qa_error.OutOfNodesError("No nodes left")
490 # Get node with least number of uses
492 result = cmp(a.get("_count", 0), b.get("_count", 0))
494 result = cmp(a["primary"], b["primary"])
497 nodes.sort(cmp=compare)
500 node["_count"] = node.get("_count", 0) + 1
504 def AcquireManyNodes(num, exclude=None):
505 """Return the least used nodes.
508 @param num: Number of nodes; can be 0.
509 @type exclude: list of nodes or C{None}
510 @param exclude: nodes to be excluded from the choice
511 @rtype: list of nodes
512 @return: C{num} different nodes
518 elif isinstance(exclude, (list, tuple)):
519 # Don't modify the incoming argument
520 exclude = list(exclude)
525 for _ in range(0, num):
526 n = AcquireNode(exclude=exclude)
529 except qa_error.OutOfNodesError:
530 ReleaseManyNodes(nodes)
535 def ReleaseNode(node):
536 node["_count"] = node.get("_count", 0) - 1
539 def ReleaseManyNodes(nodes):