QA: Convert instances to objects
[ganeti-local] / qa / qa_config.py
1 #
2 #
3
4 # Copyright (C) 2007, 2011, 2012, 2013 Google Inc.
5 #
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.
10 #
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.
15 #
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
19 # 02110-1301, USA.
20
21
22 """QA configuration.
23
24 """
25
26 import os
27
28 from ganeti import constants
29 from ganeti import utils
30 from ganeti import serializer
31 from ganeti import compat
32
33 import qa_error
34
35
36 _INSTANCE_CHECK_KEY = "instance-check"
37 _ENABLED_HV_KEY = "enabled-hypervisors"
38
39 #: QA configuration (L{_QaConfig})
40 _config = None
41
42
43 class _QaInstance(object):
44   __slots__ = [
45     "name",
46     "nicmac",
47     "used",
48     "disk_template",
49     ]
50
51   def __init__(self, name, nicmac):
52     """Initializes instances of this class.
53
54     """
55     self.name = name
56     self.nicmac = nicmac
57     self.used = None
58     self.disk_template = None
59
60   @classmethod
61   def FromDict(cls, data):
62     """Creates instance object from JSON dictionary.
63
64     """
65     nicmac = []
66
67     macaddr = data.get("nic.mac/0")
68     if macaddr:
69       nicmac.append(macaddr)
70
71     return cls(name=data["name"], nicmac=nicmac)
72
73   def __getitem__(self, key):
74     """Legacy dict-like interface.
75
76     """
77     if key == "name":
78       return self.name
79     else:
80       raise KeyError(key)
81
82   def get(self, key, default):
83     """Legacy dict-like interface.
84
85     """
86     try:
87       return self[key]
88     except KeyError:
89       return default
90
91   def GetNicMacAddr(self, idx, default):
92     """Returns MAC address for NIC.
93
94     @type idx: int
95     @param idx: NIC index
96     @param default: Default value
97
98     """
99     if len(self.nicmac) > idx:
100       return self.nicmac[idx]
101     else:
102       return default
103
104
105 _RESOURCE_CONVERTER = {
106   "instances": _QaInstance.FromDict,
107   }
108
109
110 def _ConvertResources((key, value)):
111   """Converts cluster resources in configuration to Python objects.
112
113   """
114   fn = _RESOURCE_CONVERTER.get(key, None)
115   if fn:
116     return (key, map(fn, value))
117   else:
118     return (key, value)
119
120
121 class _QaConfig(object):
122   def __init__(self, data):
123     """Initializes instances of this class.
124
125     """
126     self._data = data
127
128     #: Cluster-wide run-time value of the exclusive storage flag
129     self._exclusive_storage = None
130
131   @classmethod
132   def Load(cls, filename):
133     """Loads a configuration file and produces a configuration object.
134
135     @type filename: string
136     @param filename: Path to configuration file
137     @rtype: L{_QaConfig}
138
139     """
140     data = serializer.LoadJson(utils.ReadFile(filename))
141
142     result = cls(dict(map(_ConvertResources,
143                           data.items()))) # pylint: disable=E1103
144     result.Validate()
145
146     return result
147
148   def Validate(self):
149     """Validates loaded configuration data.
150
151     """
152     if not self.get("nodes"):
153       raise qa_error.Error("Need at least one node")
154
155     if not self.get("instances"):
156       raise qa_error.Error("Need at least one instance")
157
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")
163
164     check = self.GetInstanceCheckScript()
165     if check:
166       try:
167         os.stat(check)
168       except EnvironmentError, err:
169         raise qa_error.Error("Can't find instance check script '%s': %s" %
170                              (check, err))
171
172     enabled_hv = frozenset(self.GetEnabledHypervisors())
173     if not enabled_hv:
174       raise qa_error.Error("No hypervisor is enabled")
175
176     difference = enabled_hv - constants.HYPER_TYPES
177     if difference:
178       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
179                            utils.CommaJoin(difference))
180
181   def __getitem__(self, name):
182     """Returns configuration value.
183
184     @type name: string
185     @param name: Name of configuration entry
186
187     """
188     return self._data[name]
189
190   def get(self, name, default=None):
191     """Returns configuration value.
192
193     @type name: string
194     @param name: Name of configuration entry
195     @param default: Default value
196
197     """
198     return self._data.get(name, default)
199
200   def GetMasterNode(self):
201     """Returns the default master node for the cluster.
202
203     """
204     return self["nodes"][0]
205
206   def GetInstanceCheckScript(self):
207     """Returns path to instance check script or C{None}.
208
209     """
210     return self._data.get(_INSTANCE_CHECK_KEY, None)
211
212   def GetEnabledHypervisors(self):
213     """Returns list of enabled hypervisors.
214
215     @rtype: list
216
217     """
218     try:
219       value = self._data[_ENABLED_HV_KEY]
220     except KeyError:
221       return [constants.DEFAULT_ENABLED_HYPERVISOR]
222     else:
223       if value is None:
224         return []
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
229         # equally here.
230         return value.split(",")
231       else:
232         return value
233
234   def GetDefaultHypervisor(self):
235     """Returns the default hypervisor to be used.
236
237     """
238     return self.GetEnabledHypervisors()[0]
239
240   def SetExclusiveStorage(self, value):
241     """Set the expected value of the C{exclusive_storage} flag for the cluster.
242
243     """
244     self._exclusive_storage = bool(value)
245
246   def GetExclusiveStorage(self):
247     """Get the expected value of the C{exclusive_storage} flag for the cluster.
248
249     """
250     value = self._exclusive_storage
251     assert value is not None
252     return value
253
254   def IsTemplateSupported(self, templ):
255     """Is the given disk template supported by the current configuration?
256
257     """
258     return (not self.GetExclusiveStorage() or
259             templ in constants.DTS_EXCL_STORAGE)
260
261
262 def Load(path):
263   """Loads the passed configuration file.
264
265   """
266   global _config # pylint: disable=W0603
267
268   _config = _QaConfig.Load(path)
269
270
271 def GetConfig():
272   """Returns the configuration object.
273
274   """
275   if _config is None:
276     raise RuntimeError("Configuration not yet loaded")
277
278   return _config
279
280
281 def get(name, default=None):
282   """Wrapper for L{_QaConfig.get}.
283
284   """
285   return GetConfig().get(name, default=default)
286
287
288 class Either:
289   def __init__(self, tests):
290     """Initializes this class.
291
292     @type tests: list or string
293     @param tests: List of test names
294     @see: L{TestEnabled} for details
295
296     """
297     self.tests = tests
298
299
300 def _MakeSequence(value):
301   """Make sequence of single argument.
302
303   If the single argument is not already a list or tuple, a list with the
304   argument as a single item is returned.
305
306   """
307   if isinstance(value, (list, tuple)):
308     return value
309   else:
310     return [value]
311
312
313 def _TestEnabledInner(check_fn, names, fn):
314   """Evaluate test conditions.
315
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)
320   @type fn: callable
321   @param fn: Aggregation function
322   @rtype: bool
323   @return: Whether test is enabled
324
325   """
326   names = _MakeSequence(names)
327
328   result = []
329
330   for name in 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)
335     else:
336       value = check_fn(name)
337
338     result.append(value)
339
340   return fn(result)
341
342
343 def TestEnabled(tests, _cfg=None):
344   """Returns True if the given tests are enabled.
345
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
348
349   """
350   if _cfg is None:
351     cfg = GetConfig()
352   else:
353     cfg = _cfg
354
355   # Get settings for all tests
356   cfg_tests = cfg.get("tests", {})
357
358   # Get default setting
359   default = cfg_tests.get("default", True)
360
361   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
362                            tests, compat.all)
363
364
365 def GetInstanceCheckScript(*args):
366   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
367
368   """
369   return GetConfig().GetInstanceCheckScript(*args)
370
371
372 def GetEnabledHypervisors(*args):
373   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
374
375   """
376   return GetConfig().GetEnabledHypervisors(*args)
377
378
379 def GetDefaultHypervisor(*args):
380   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
381
382   """
383   return GetConfig().GetDefaultHypervisor(*args)
384
385
386 def GetInstanceNicMac(inst, default=None):
387   """Returns MAC address for instance's network interface.
388
389   """
390   return inst.GetNicMacAddr(0, default)
391
392
393 def GetMasterNode():
394   """Wrapper for L{_QaConfig.GetMasterNode}.
395
396   """
397   return GetConfig().GetMasterNode()
398
399
400 def AcquireInstance(_cfg=None):
401   """Returns an instance which isn't in use.
402
403   """
404   if _cfg is None:
405     cfg = GetConfig()
406   else:
407     cfg = _cfg
408
409   # Filter out unwanted instances
410   instances = filter(lambda inst: not inst.used, cfg["instances"])
411
412   if not instances:
413     raise qa_error.OutOfInstancesError("No instances left")
414
415   inst = instances[0]
416
417   assert not inst.used
418   assert inst.disk_template is None
419
420   inst.used = True
421
422   return inst
423
424
425 def ReleaseInstance(inst):
426   inst.used = False
427   inst.disk_template = None
428
429
430 def GetInstanceTemplate(inst):
431   """Return the disk template of an instance.
432
433   """
434   templ = inst.disk_template
435   assert templ is not None
436   return templ
437
438
439 def SetInstanceTemplate(inst, template):
440   """Set the disk template for an instance.
441
442   """
443   inst.disk_template = template
444
445
446 def SetExclusiveStorage(value):
447   """Wrapper for L{_QaConfig.SetExclusiveStorage}.
448
449   """
450   return GetConfig().SetExclusiveStorage(value)
451
452
453 def GetExclusiveStorage():
454   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
455
456   """
457   return GetConfig().GetExclusiveStorage()
458
459
460 def IsTemplateSupported(templ):
461   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
462
463   """
464   return GetConfig().IsTemplateSupported(templ)
465
466
467 def AcquireNode(exclude=None):
468   """Returns the least used node.
469
470   """
471   master = GetMasterNode()
472   cfg = GetConfig()
473
474   # Filter out unwanted nodes
475   # TODO: Maybe combine filters
476   if exclude is None:
477     nodes = cfg["nodes"][:]
478   elif isinstance(exclude, (list, tuple)):
479     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
480   else:
481     nodes = filter(lambda node: node != exclude, cfg["nodes"])
482
483   tmp_flt = lambda node: node.get("_added", False) or node == master
484   nodes = filter(tmp_flt, nodes)
485   del tmp_flt
486
487   if len(nodes) == 0:
488     raise qa_error.OutOfNodesError("No nodes left")
489
490   # Get node with least number of uses
491   def compare(a, b):
492     result = cmp(a.get("_count", 0), b.get("_count", 0))
493     if result == 0:
494       result = cmp(a["primary"], b["primary"])
495     return result
496
497   nodes.sort(cmp=compare)
498
499   node = nodes[0]
500   node["_count"] = node.get("_count", 0) + 1
501   return node
502
503
504 def AcquireManyNodes(num, exclude=None):
505   """Return the least used nodes.
506
507   @type num: int
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
513
514   """
515   nodes = []
516   if exclude is None:
517     exclude = []
518   elif isinstance(exclude, (list, tuple)):
519     # Don't modify the incoming argument
520     exclude = list(exclude)
521   else:
522     exclude = [exclude]
523
524   try:
525     for _ in range(0, num):
526       n = AcquireNode(exclude=exclude)
527       nodes.append(n)
528       exclude.append(n)
529   except qa_error.OutOfNodesError:
530     ReleaseManyNodes(nodes)
531     raise
532   return nodes
533
534
535 def ReleaseNode(node):
536   node["_count"] = node.get("_count", 0) - 1
537
538
539 def ReleaseManyNodes(nodes):
540   for n in nodes:
541     ReleaseNode(n)