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