QA: Set disk template directly via instance object
[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 Release(self):
74     """Releases instance and makes it available again.
75
76     """
77     assert self.used, \
78       ("Instance '%s' was never acquired or released more than once" %
79        self.name)
80
81     self.used = False
82     self._disk_template = None
83
84   def GetNicMacAddr(self, idx, default):
85     """Returns MAC address for NIC.
86
87     @type idx: int
88     @param idx: NIC index
89     @param default: Default value
90
91     """
92     if len(self.nicmac) > idx:
93       return self.nicmac[idx]
94     else:
95       return default
96
97   def SetDiskTemplate(self, template):
98     """Set the disk template.
99
100     """
101     assert template in constants.DISK_TEMPLATES
102
103     self._disk_template = template
104
105   @property
106   def disk_template(self):
107     """Returns the current disk template.
108
109     """
110     return self._disk_template
111
112
113 class _QaNode(object):
114   __slots__ = [
115     "primary",
116     "secondary",
117     "_added",
118     "_use_count",
119     ]
120
121   def __init__(self, primary, secondary):
122     """Initializes instances of this class.
123
124     """
125     self.primary = primary
126     self.secondary = secondary
127     self._added = False
128     self._use_count = 0
129
130   @classmethod
131   def FromDict(cls, data):
132     """Creates node object from JSON dictionary.
133
134     """
135     return cls(primary=data["primary"], secondary=data.get("secondary"))
136
137   def Use(self):
138     """Marks a node as being in use.
139
140     """
141     assert self._use_count >= 0
142
143     self._use_count += 1
144
145     return self
146
147   def Release(self):
148     """Release a node (opposite of L{Use}).
149
150     """
151     assert self.use_count > 0
152
153     self._use_count -= 1
154
155   def MarkAdded(self):
156     """Marks node as having been added to a cluster.
157
158     """
159     assert not self._added
160     self._added = True
161
162   def MarkRemoved(self):
163     """Marks node as having been removed from a cluster.
164
165     """
166     assert self._added
167     self._added = False
168
169   @property
170   def added(self):
171     """Returns whether a node is part of a cluster.
172
173     """
174     return self._added
175
176   @property
177   def use_count(self):
178     """Returns number of current uses (controlled by L{Use} and L{Release}).
179
180     """
181     return self._use_count
182
183
184 _RESOURCE_CONVERTER = {
185   "instances": _QaInstance.FromDict,
186   "nodes": _QaNode.FromDict,
187   }
188
189
190 def _ConvertResources((key, value)):
191   """Converts cluster resources in configuration to Python objects.
192
193   """
194   fn = _RESOURCE_CONVERTER.get(key, None)
195   if fn:
196     return (key, map(fn, value))
197   else:
198     return (key, value)
199
200
201 class _QaConfig(object):
202   def __init__(self, data):
203     """Initializes instances of this class.
204
205     """
206     self._data = data
207
208     #: Cluster-wide run-time value of the exclusive storage flag
209     self._exclusive_storage = None
210
211   @classmethod
212   def Load(cls, filename):
213     """Loads a configuration file and produces a configuration object.
214
215     @type filename: string
216     @param filename: Path to configuration file
217     @rtype: L{_QaConfig}
218
219     """
220     data = serializer.LoadJson(utils.ReadFile(filename))
221
222     result = cls(dict(map(_ConvertResources,
223                           data.items()))) # pylint: disable=E1103
224     result.Validate()
225
226     return result
227
228   def Validate(self):
229     """Validates loaded configuration data.
230
231     """
232     if not self.get("nodes"):
233       raise qa_error.Error("Need at least one node")
234
235     if not self.get("instances"):
236       raise qa_error.Error("Need at least one instance")
237
238     if (self.get("disk") is None or
239         self.get("disk-growth") is None or
240         len(self.get("disk")) != len(self.get("disk-growth"))):
241       raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
242                            " and have the same number of items")
243
244     check = self.GetInstanceCheckScript()
245     if check:
246       try:
247         os.stat(check)
248       except EnvironmentError, err:
249         raise qa_error.Error("Can't find instance check script '%s': %s" %
250                              (check, err))
251
252     enabled_hv = frozenset(self.GetEnabledHypervisors())
253     if not enabled_hv:
254       raise qa_error.Error("No hypervisor is enabled")
255
256     difference = enabled_hv - constants.HYPER_TYPES
257     if difference:
258       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
259                            utils.CommaJoin(difference))
260
261   def __getitem__(self, name):
262     """Returns configuration value.
263
264     @type name: string
265     @param name: Name of configuration entry
266
267     """
268     return self._data[name]
269
270   def get(self, name, default=None):
271     """Returns configuration value.
272
273     @type name: string
274     @param name: Name of configuration entry
275     @param default: Default value
276
277     """
278     return self._data.get(name, default)
279
280   def GetMasterNode(self):
281     """Returns the default master node for the cluster.
282
283     """
284     return self["nodes"][0]
285
286   def GetInstanceCheckScript(self):
287     """Returns path to instance check script or C{None}.
288
289     """
290     return self._data.get(_INSTANCE_CHECK_KEY, None)
291
292   def GetEnabledHypervisors(self):
293     """Returns list of enabled hypervisors.
294
295     @rtype: list
296
297     """
298     try:
299       value = self._data[_ENABLED_HV_KEY]
300     except KeyError:
301       return [constants.DEFAULT_ENABLED_HYPERVISOR]
302     else:
303       if value is None:
304         return []
305       elif isinstance(value, basestring):
306         # The configuration key ("enabled-hypervisors") implies there can be
307         # multiple values. Multiple hypervisors are comma-separated on the
308         # command line option to "gnt-cluster init", so we need to handle them
309         # equally here.
310         return value.split(",")
311       else:
312         return value
313
314   def GetDefaultHypervisor(self):
315     """Returns the default hypervisor to be used.
316
317     """
318     return self.GetEnabledHypervisors()[0]
319
320   def SetExclusiveStorage(self, value):
321     """Set the expected value of the C{exclusive_storage} flag for the cluster.
322
323     """
324     self._exclusive_storage = bool(value)
325
326   def GetExclusiveStorage(self):
327     """Get the expected value of the C{exclusive_storage} flag for the cluster.
328
329     """
330     value = self._exclusive_storage
331     assert value is not None
332     return value
333
334   def IsTemplateSupported(self, templ):
335     """Is the given disk template supported by the current configuration?
336
337     """
338     return (not self.GetExclusiveStorage() or
339             templ in constants.DTS_EXCL_STORAGE)
340
341
342 def Load(path):
343   """Loads the passed configuration file.
344
345   """
346   global _config # pylint: disable=W0603
347
348   _config = _QaConfig.Load(path)
349
350
351 def GetConfig():
352   """Returns the configuration object.
353
354   """
355   if _config is None:
356     raise RuntimeError("Configuration not yet loaded")
357
358   return _config
359
360
361 def get(name, default=None):
362   """Wrapper for L{_QaConfig.get}.
363
364   """
365   return GetConfig().get(name, default=default)
366
367
368 class Either:
369   def __init__(self, tests):
370     """Initializes this class.
371
372     @type tests: list or string
373     @param tests: List of test names
374     @see: L{TestEnabled} for details
375
376     """
377     self.tests = tests
378
379
380 def _MakeSequence(value):
381   """Make sequence of single argument.
382
383   If the single argument is not already a list or tuple, a list with the
384   argument as a single item is returned.
385
386   """
387   if isinstance(value, (list, tuple)):
388     return value
389   else:
390     return [value]
391
392
393 def _TestEnabledInner(check_fn, names, fn):
394   """Evaluate test conditions.
395
396   @type check_fn: callable
397   @param check_fn: Callback to check whether a test is enabled
398   @type names: sequence or string
399   @param names: Test name(s)
400   @type fn: callable
401   @param fn: Aggregation function
402   @rtype: bool
403   @return: Whether test is enabled
404
405   """
406   names = _MakeSequence(names)
407
408   result = []
409
410   for name in names:
411     if isinstance(name, Either):
412       value = _TestEnabledInner(check_fn, name.tests, compat.any)
413     elif isinstance(name, (list, tuple)):
414       value = _TestEnabledInner(check_fn, name, compat.all)
415     else:
416       value = check_fn(name)
417
418     result.append(value)
419
420   return fn(result)
421
422
423 def TestEnabled(tests, _cfg=None):
424   """Returns True if the given tests are enabled.
425
426   @param tests: A single test as a string, or a list of tests to check; can
427     contain L{Either} for OR conditions, AND is default
428
429   """
430   if _cfg is None:
431     cfg = GetConfig()
432   else:
433     cfg = _cfg
434
435   # Get settings for all tests
436   cfg_tests = cfg.get("tests", {})
437
438   # Get default setting
439   default = cfg_tests.get("default", True)
440
441   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
442                            tests, compat.all)
443
444
445 def GetInstanceCheckScript(*args):
446   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
447
448   """
449   return GetConfig().GetInstanceCheckScript(*args)
450
451
452 def GetEnabledHypervisors(*args):
453   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
454
455   """
456   return GetConfig().GetEnabledHypervisors(*args)
457
458
459 def GetDefaultHypervisor(*args):
460   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
461
462   """
463   return GetConfig().GetDefaultHypervisor(*args)
464
465
466 def GetMasterNode():
467   """Wrapper for L{_QaConfig.GetMasterNode}.
468
469   """
470   return GetConfig().GetMasterNode()
471
472
473 def AcquireInstance(_cfg=None):
474   """Returns an instance which isn't in use.
475
476   """
477   if _cfg is None:
478     cfg = GetConfig()
479   else:
480     cfg = _cfg
481
482   # Filter out unwanted instances
483   instances = filter(lambda inst: not inst.used, cfg["instances"])
484
485   if not instances:
486     raise qa_error.OutOfInstancesError("No instances left")
487
488   inst = instances[0]
489
490   assert not inst.used
491   assert inst.disk_template is None
492
493   inst.used = True
494
495   return inst
496
497
498 def SetExclusiveStorage(value):
499   """Wrapper for L{_QaConfig.SetExclusiveStorage}.
500
501   """
502   return GetConfig().SetExclusiveStorage(value)
503
504
505 def GetExclusiveStorage():
506   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
507
508   """
509   return GetConfig().GetExclusiveStorage()
510
511
512 def IsTemplateSupported(templ):
513   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
514
515   """
516   return GetConfig().IsTemplateSupported(templ)
517
518
519 def AcquireNode(exclude=None, _cfg=None):
520   """Returns the least used node.
521
522   """
523   if _cfg is None:
524     cfg = GetConfig()
525   else:
526     cfg = _cfg
527
528   master = cfg.GetMasterNode()
529
530   # Filter out unwanted nodes
531   # TODO: Maybe combine filters
532   if exclude is None:
533     nodes = cfg["nodes"][:]
534   elif isinstance(exclude, (list, tuple)):
535     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
536   else:
537     nodes = filter(lambda node: node != exclude, cfg["nodes"])
538
539   nodes = filter(lambda node: node.added or node == master, nodes)
540
541   if not nodes:
542     raise qa_error.OutOfNodesError("No nodes left")
543
544   # Get node with least number of uses
545   # TODO: Switch to computing sort key instead of comparing directly
546   def compare(a, b):
547     result = cmp(a.use_count, b.use_count)
548     if result == 0:
549       result = cmp(a.primary, b.primary)
550     return result
551
552   nodes.sort(cmp=compare)
553
554   return nodes[0].Use()
555
556
557 def AcquireManyNodes(num, exclude=None):
558   """Return the least used nodes.
559
560   @type num: int
561   @param num: Number of nodes; can be 0.
562   @type exclude: list of nodes or C{None}
563   @param exclude: nodes to be excluded from the choice
564   @rtype: list of nodes
565   @return: C{num} different nodes
566
567   """
568   nodes = []
569   if exclude is None:
570     exclude = []
571   elif isinstance(exclude, (list, tuple)):
572     # Don't modify the incoming argument
573     exclude = list(exclude)
574   else:
575     exclude = [exclude]
576
577   try:
578     for _ in range(0, num):
579       n = AcquireNode(exclude=exclude)
580       nodes.append(n)
581       exclude.append(n)
582   except qa_error.OutOfNodesError:
583     ReleaseManyNodes(nodes)
584     raise
585   return nodes
586
587
588 def ReleaseManyNodes(nodes):
589   for node in nodes:
590     node.Release()