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