9b50efb6e21deff2eb0b1499954602d086a96448
[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 #: Cluster-wide run-time value of the exclusive storage flag
40 _exclusive_storage = None
41
42
43 #: QA configuration (L{_QaConfig})
44 _config = None
45
46
47 class _QaConfig(object):
48   def __init__(self, data):
49     """Initializes instances of this class.
50
51     """
52     self._data = data
53
54   @classmethod
55   def Load(cls, filename):
56     """Loads a configuration file and produces a configuration object.
57
58     @type filename: string
59     @param filename: Path to configuration file
60     @rtype: L{_QaConfig}
61
62     """
63     data = serializer.LoadJson(utils.ReadFile(filename))
64
65     result = cls(data)
66     result.Validate()
67
68     return result
69
70   def Validate(self):
71     """Validates loaded configuration data.
72
73     """
74     if not self.get("nodes"):
75       raise qa_error.Error("Need at least one node")
76
77     if not self.get("instances"):
78       raise qa_error.Error("Need at least one instance")
79
80     if (self.get("disk") is None or
81         self.get("disk-growth") is None or
82         len(self.get("disk")) != len(self.get("disk-growth"))):
83       raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
84                            " and have the same number of items")
85
86     check = self.GetInstanceCheckScript()
87     if check:
88       try:
89         os.stat(check)
90       except EnvironmentError, err:
91         raise qa_error.Error("Can't find instance check script '%s': %s" %
92                              (check, err))
93
94     enabled_hv = frozenset(self.GetEnabledHypervisors())
95     if not enabled_hv:
96       raise qa_error.Error("No hypervisor is enabled")
97
98     difference = enabled_hv - constants.HYPER_TYPES
99     if difference:
100       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
101                            utils.CommaJoin(difference))
102
103   def __getitem__(self, name):
104     """Returns configuration value.
105
106     @type name: string
107     @param name: Name of configuration entry
108
109     """
110     return self._data[name]
111
112   def get(self, name, default=None):
113     """Returns configuration value.
114
115     @type name: string
116     @param name: Name of configuration entry
117     @param default: Default value
118
119     """
120     return self._data.get(name, default)
121
122   def GetMasterNode(self):
123     """Returns the default master node for the cluster.
124
125     """
126     return self["nodes"][0]
127
128   def GetInstanceCheckScript(self):
129     """Returns path to instance check script or C{None}.
130
131     """
132     return self._data.get(_INSTANCE_CHECK_KEY, None)
133
134   def GetEnabledHypervisors(self):
135     """Returns list of enabled hypervisors.
136
137     @rtype: list
138
139     """
140     try:
141       value = self._data[_ENABLED_HV_KEY]
142     except KeyError:
143       return [constants.DEFAULT_ENABLED_HYPERVISOR]
144     else:
145       if value is None:
146         return []
147       elif isinstance(value, basestring):
148         # The configuration key ("enabled-hypervisors") implies there can be
149         # multiple values. Multiple hypervisors are comma-separated on the
150         # command line option to "gnt-cluster init", so we need to handle them
151         # equally here.
152         return value.split(",")
153       else:
154         return value
155
156   def GetDefaultHypervisor(self):
157     """Returns the default hypervisor to be used.
158
159     """
160     return self.GetEnabledHypervisors()[0]
161
162
163 def Load(path):
164   """Loads the passed configuration file.
165
166   """
167   global _config # pylint: disable=W0603
168
169   _config = _QaConfig.Load(path)
170
171
172 def GetConfig():
173   """Returns the configuration object.
174
175   """
176   if _config is None:
177     raise RuntimeError("Configuration not yet loaded")
178
179   return _config
180
181
182 def get(name, default=None):
183   """Wrapper for L{_QaConfig.get}.
184
185   """
186   return GetConfig().get(name, default=default)
187
188
189 class Either:
190   def __init__(self, tests):
191     """Initializes this class.
192
193     @type tests: list or string
194     @param tests: List of test names
195     @see: L{TestEnabled} for details
196
197     """
198     self.tests = tests
199
200
201 def _MakeSequence(value):
202   """Make sequence of single argument.
203
204   If the single argument is not already a list or tuple, a list with the
205   argument as a single item is returned.
206
207   """
208   if isinstance(value, (list, tuple)):
209     return value
210   else:
211     return [value]
212
213
214 def _TestEnabledInner(check_fn, names, fn):
215   """Evaluate test conditions.
216
217   @type check_fn: callable
218   @param check_fn: Callback to check whether a test is enabled
219   @type names: sequence or string
220   @param names: Test name(s)
221   @type fn: callable
222   @param fn: Aggregation function
223   @rtype: bool
224   @return: Whether test is enabled
225
226   """
227   names = _MakeSequence(names)
228
229   result = []
230
231   for name in names:
232     if isinstance(name, Either):
233       value = _TestEnabledInner(check_fn, name.tests, compat.any)
234     elif isinstance(name, (list, tuple)):
235       value = _TestEnabledInner(check_fn, name, compat.all)
236     else:
237       value = check_fn(name)
238
239     result.append(value)
240
241   return fn(result)
242
243
244 def TestEnabled(tests, _cfg=None):
245   """Returns True if the given tests are enabled.
246
247   @param tests: A single test as a string, or a list of tests to check; can
248     contain L{Either} for OR conditions, AND is default
249
250   """
251   if _cfg is None:
252     cfg = GetConfig()
253   else:
254     cfg = _cfg
255
256   # Get settings for all tests
257   cfg_tests = cfg.get("tests", {})
258
259   # Get default setting
260   default = cfg_tests.get("default", True)
261
262   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
263                            tests, compat.all)
264
265
266 def GetInstanceCheckScript(*args):
267   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
268
269   """
270   return GetConfig().GetInstanceCheckScript(*args)
271
272
273 def GetEnabledHypervisors(*args):
274   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
275
276   """
277   return GetConfig().GetEnabledHypervisors(*args)
278
279
280 def GetDefaultHypervisor(*args):
281   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
282
283   """
284   return GetConfig().GetDefaultHypervisor(*args)
285
286
287 def GetInstanceNicMac(inst, default=None):
288   """Returns MAC address for instance's network interface.
289
290   """
291   return inst.get("nic.mac/0", default)
292
293
294 def GetMasterNode():
295   """Wrapper for L{_QaConfig.GetMasterNode}.
296
297   """
298   return GetConfig().GetMasterNode()
299
300
301 def AcquireInstance():
302   """Returns an instance which isn't in use.
303
304   """
305   # Filter out unwanted instances
306   tmp_flt = lambda inst: not inst.get("_used", False)
307   instances = filter(tmp_flt, GetConfig()["instances"])
308   del tmp_flt
309
310   if len(instances) == 0:
311     raise qa_error.OutOfInstancesError("No instances left")
312
313   inst = instances[0]
314   inst["_used"] = True
315   inst["_template"] = None
316   return inst
317
318
319 def ReleaseInstance(inst):
320   inst["_used"] = False
321
322
323 def GetInstanceTemplate(inst):
324   """Return the disk template of an instance.
325
326   """
327   templ = inst["_template"]
328   assert templ is not None
329   return templ
330
331
332 def SetInstanceTemplate(inst, template):
333   """Set the disk template for an instance.
334
335   """
336   inst["_template"] = template
337
338
339 def SetExclusiveStorage(value):
340   """Set the expected value of the exclusive_storage flag for the cluster.
341
342   """
343   global _exclusive_storage # pylint: disable=W0603
344
345   _exclusive_storage = bool(value)
346
347
348 def GetExclusiveStorage():
349   """Get the expected value of the exclusive_storage flag for the cluster.
350
351   """
352   val = _exclusive_storage
353   assert val is not None
354   return val
355
356
357 def IsTemplateSupported(templ):
358   """Is the given disk template supported by the current configuration?
359
360   """
361   if GetExclusiveStorage():
362     return templ in constants.DTS_EXCL_STORAGE
363   else:
364     return True
365
366
367 def AcquireNode(exclude=None):
368   """Returns the least used node.
369
370   """
371   master = GetMasterNode()
372   cfg = GetConfig()
373
374   # Filter out unwanted nodes
375   # TODO: Maybe combine filters
376   if exclude is None:
377     nodes = cfg["nodes"][:]
378   elif isinstance(exclude, (list, tuple)):
379     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
380   else:
381     nodes = filter(lambda node: node != exclude, cfg["nodes"])
382
383   tmp_flt = lambda node: node.get("_added", False) or node == master
384   nodes = filter(tmp_flt, nodes)
385   del tmp_flt
386
387   if len(nodes) == 0:
388     raise qa_error.OutOfNodesError("No nodes left")
389
390   # Get node with least number of uses
391   def compare(a, b):
392     result = cmp(a.get("_count", 0), b.get("_count", 0))
393     if result == 0:
394       result = cmp(a["primary"], b["primary"])
395     return result
396
397   nodes.sort(cmp=compare)
398
399   node = nodes[0]
400   node["_count"] = node.get("_count", 0) + 1
401   return node
402
403
404 def AcquireManyNodes(num, exclude=None):
405   """Return the least used nodes.
406
407   @type num: int
408   @param num: Number of nodes; can be 0.
409   @type exclude: list of nodes or C{None}
410   @param exclude: nodes to be excluded from the choice
411   @rtype: list of nodes
412   @return: C{num} different nodes
413
414   """
415   nodes = []
416   if exclude is None:
417     exclude = []
418   elif isinstance(exclude, (list, tuple)):
419     # Don't modify the incoming argument
420     exclude = list(exclude)
421   else:
422     exclude = [exclude]
423
424   try:
425     for _ in range(0, num):
426       n = AcquireNode(exclude=exclude)
427       nodes.append(n)
428       exclude.append(n)
429   except qa_error.OutOfNodesError:
430     ReleaseManyNodes(nodes)
431     raise
432   return nodes
433
434
435 def ReleaseNode(node):
436   node["_count"] = node.get("_count", 0) - 1
437
438
439 def ReleaseManyNodes(nodes):
440   for n in nodes:
441     ReleaseNode(n)