1c6a5430c5805aa119ac66f1c2dc090ddc965082
[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 from ganeti import ht
33
34 import qa_error
35
36
37 _INSTANCE_CHECK_KEY = "instance-check"
38 _ENABLED_HV_KEY = "enabled-hypervisors"
39 _VCLUSTER_MASTER_KEY = "vcluster-master"
40 _VCLUSTER_BASEDIR_KEY = "vcluster-basedir"
41 _ENABLED_DISK_TEMPLATES_KEY = "enabled-disk-templates"
42
43 #: QA configuration (L{_QaConfig})
44 _config = None
45
46
47 class _QaInstance(object):
48   __slots__ = [
49     "name",
50     "nicmac",
51     "_used",
52     "_disk_template",
53     ]
54
55   def __init__(self, name, nicmac):
56     """Initializes instances of this class.
57
58     """
59     self.name = name
60     self.nicmac = nicmac
61     self._used = None
62     self._disk_template = None
63
64   @classmethod
65   def FromDict(cls, data):
66     """Creates instance object from JSON dictionary.
67
68     """
69     nicmac = []
70
71     macaddr = data.get("nic.mac/0")
72     if macaddr:
73       nicmac.append(macaddr)
74
75     return cls(name=data["name"], nicmac=nicmac)
76
77   def __repr__(self):
78     status = [
79       "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
80       "name=%s" % self.name,
81       "nicmac=%s" % self.nicmac,
82       "used=%s" % self._used,
83       "disk_template=%s" % self._disk_template,
84       ]
85
86     return "<%s at %#x>" % (" ".join(status), id(self))
87
88   def Use(self):
89     """Marks instance as being in use.
90
91     """
92     assert not self._used
93     assert self._disk_template is None
94
95     self._used = True
96
97   def Release(self):
98     """Releases instance and makes it available again.
99
100     """
101     assert self._used, \
102       ("Instance '%s' was never acquired or released more than once" %
103        self.name)
104
105     self._used = False
106     self._disk_template = None
107
108   def GetNicMacAddr(self, idx, default):
109     """Returns MAC address for NIC.
110
111     @type idx: int
112     @param idx: NIC index
113     @param default: Default value
114
115     """
116     if len(self.nicmac) > idx:
117       return self.nicmac[idx]
118     else:
119       return default
120
121   def SetDiskTemplate(self, template):
122     """Set the disk template.
123
124     """
125     assert template in constants.DISK_TEMPLATES
126
127     self._disk_template = template
128
129   @property
130   def used(self):
131     """Returns boolean denoting whether instance is in use.
132
133     """
134     return self._used
135
136   @property
137   def disk_template(self):
138     """Returns the current disk template.
139
140     """
141     return self._disk_template
142
143
144 class _QaNode(object):
145   __slots__ = [
146     "primary",
147     "secondary",
148     "_added",
149     "_use_count",
150     ]
151
152   def __init__(self, primary, secondary):
153     """Initializes instances of this class.
154
155     """
156     self.primary = primary
157     self.secondary = secondary
158     self._added = False
159     self._use_count = 0
160
161   @classmethod
162   def FromDict(cls, data):
163     """Creates node object from JSON dictionary.
164
165     """
166     return cls(primary=data["primary"], secondary=data.get("secondary"))
167
168   def __repr__(self):
169     status = [
170       "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
171       "primary=%s" % self.primary,
172       "secondary=%s" % self.secondary,
173       "added=%s" % self._added,
174       "use_count=%s" % self._use_count,
175       ]
176
177     return "<%s at %#x>" % (" ".join(status), id(self))
178
179   def Use(self):
180     """Marks a node as being in use.
181
182     """
183     assert self._use_count >= 0
184
185     self._use_count += 1
186
187     return self
188
189   def Release(self):
190     """Release a node (opposite of L{Use}).
191
192     """
193     assert self.use_count > 0
194
195     self._use_count -= 1
196
197   def MarkAdded(self):
198     """Marks node as having been added to a cluster.
199
200     """
201     assert not self._added
202     self._added = True
203
204   def MarkRemoved(self):
205     """Marks node as having been removed from a cluster.
206
207     """
208     assert self._added
209     self._added = False
210
211   @property
212   def added(self):
213     """Returns whether a node is part of a cluster.
214
215     """
216     return self._added
217
218   @property
219   def use_count(self):
220     """Returns number of current uses (controlled by L{Use} and L{Release}).
221
222     """
223     return self._use_count
224
225
226 _RESOURCE_CONVERTER = {
227   "instances": _QaInstance.FromDict,
228   "nodes": _QaNode.FromDict,
229   }
230
231
232 def _ConvertResources((key, value)):
233   """Converts cluster resources in configuration to Python objects.
234
235   """
236   fn = _RESOURCE_CONVERTER.get(key, None)
237   if fn:
238     return (key, map(fn, value))
239   else:
240     return (key, value)
241
242
243 class _QaConfig(object):
244   def __init__(self, data):
245     """Initializes instances of this class.
246
247     """
248     self._data = data
249
250     #: Cluster-wide run-time value of the exclusive storage flag
251     self._exclusive_storage = None
252
253   @classmethod
254   def Load(cls, filename):
255     """Loads a configuration file and produces a configuration object.
256
257     @type filename: string
258     @param filename: Path to configuration file
259     @rtype: L{_QaConfig}
260
261     """
262     data = serializer.LoadJson(utils.ReadFile(filename))
263
264     result = cls(dict(map(_ConvertResources,
265                           data.items()))) # pylint: disable=E1103
266     result.Validate()
267
268     return result
269
270   def Validate(self):
271     """Validates loaded configuration data.
272
273     """
274     if not self.get("name"):
275       raise qa_error.Error("Cluster name is required")
276
277     if not self.get("nodes"):
278       raise qa_error.Error("Need at least one node")
279
280     if not self.get("instances"):
281       raise qa_error.Error("Need at least one instance")
282
283     if (self.get("disk") is None or
284         self.get("disk-growth") is None or
285         len(self.get("disk")) != len(self.get("disk-growth"))):
286       raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
287                            " and have the same number of items")
288
289     check = self.GetInstanceCheckScript()
290     if check:
291       try:
292         os.stat(check)
293       except EnvironmentError, err:
294         raise qa_error.Error("Can't find instance check script '%s': %s" %
295                              (check, err))
296
297     enabled_hv = frozenset(self.GetEnabledHypervisors())
298     if not enabled_hv:
299       raise qa_error.Error("No hypervisor is enabled")
300
301     difference = enabled_hv - constants.HYPER_TYPES
302     if difference:
303       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
304                            utils.CommaJoin(difference))
305
306     (vc_master, vc_basedir) = self.GetVclusterSettings()
307     if bool(vc_master) != bool(vc_basedir):
308       raise qa_error.Error("All or none of the config options '%s' and '%s'"
309                            " must be set" %
310                            (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
311
312     if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
313       raise qa_error.Error("Path given in option '%s' must be absolute and"
314                            " normalized" % _VCLUSTER_BASEDIR_KEY)
315
316   def __getitem__(self, name):
317     """Returns configuration value.
318
319     @type name: string
320     @param name: Name of configuration entry
321
322     """
323     return self._data[name]
324
325   def get(self, name, default=None):
326     """Returns configuration value.
327
328     @type name: string
329     @param name: Name of configuration entry
330     @param default: Default value
331
332     """
333     return self._data.get(name, default)
334
335   def GetMasterNode(self):
336     """Returns the default master node for the cluster.
337
338     """
339     return self["nodes"][0]
340
341   def GetInstanceCheckScript(self):
342     """Returns path to instance check script or C{None}.
343
344     """
345     return self._data.get(_INSTANCE_CHECK_KEY, None)
346
347   def GetEnabledHypervisors(self):
348     """Returns list of enabled hypervisors.
349
350     @rtype: list
351
352     """
353     return self._GetStringListParameter(
354       _ENABLED_HV_KEY,
355       [constants.DEFAULT_ENABLED_HYPERVISOR])
356
357   def GetDefaultHypervisor(self):
358     """Returns the default hypervisor to be used.
359
360     """
361     return self.GetEnabledHypervisors()[0]
362
363   def GetEnabledDiskTemplates(self):
364     """Returns the list of enabled disk templates.
365
366     @rtype: list
367
368     """
369     return self._GetStringListParameter(
370       _ENABLED_DISK_TEMPLATES_KEY,
371       list(constants.DEFAULT_ENABLED_DISK_TEMPLATES))
372
373   def GetDefaultDiskTemplate(self):
374     """Returns the default disk template to be used.
375
376     """
377     return self.GetEnabledDiskTemplates()[0]
378
379   def _GetStringListParameter(self, key, default_values):
380     """Retrieves a parameter's value that is supposed to be a list of strings.
381
382     @rtype: list
383
384     """
385     try:
386       value = self._data[key]
387     except KeyError:
388       return default_values
389     else:
390       if value is None:
391         return []
392       elif isinstance(value, basestring):
393         return value.split(",")
394       else:
395         return value
396
397   def SetExclusiveStorage(self, value):
398     """Set the expected value of the C{exclusive_storage} flag for the cluster.
399
400     """
401     self._exclusive_storage = bool(value)
402
403   def GetExclusiveStorage(self):
404     """Get the expected value of the C{exclusive_storage} flag for the cluster.
405
406     """
407     value = self._exclusive_storage
408     assert value is not None
409     return value
410
411   def IsTemplateSupported(self, templ):
412     """Is the given disk template supported by the current configuration?
413
414     """
415     enabled = templ in self.GetEnabledDiskTemplates()
416     return enabled and (not self.GetExclusiveStorage() or
417                         templ in constants.DTS_EXCL_STORAGE)
418
419   def GetVclusterSettings(self):
420     """Returns settings for virtual cluster.
421
422     """
423     master = self.get(_VCLUSTER_MASTER_KEY)
424     basedir = self.get(_VCLUSTER_BASEDIR_KEY)
425
426     return (master, basedir)
427
428
429 def Load(path):
430   """Loads the passed configuration file.
431
432   """
433   global _config # pylint: disable=W0603
434
435   _config = _QaConfig.Load(path)
436
437
438 def GetConfig():
439   """Returns the configuration object.
440
441   """
442   if _config is None:
443     raise RuntimeError("Configuration not yet loaded")
444
445   return _config
446
447
448 def get(name, default=None):
449   """Wrapper for L{_QaConfig.get}.
450
451   """
452   return GetConfig().get(name, default=default)
453
454
455 class Either:
456   def __init__(self, tests):
457     """Initializes this class.
458
459     @type tests: list or string
460     @param tests: List of test names
461     @see: L{TestEnabled} for details
462
463     """
464     self.tests = tests
465
466
467 def _MakeSequence(value):
468   """Make sequence of single argument.
469
470   If the single argument is not already a list or tuple, a list with the
471   argument as a single item is returned.
472
473   """
474   if isinstance(value, (list, tuple)):
475     return value
476   else:
477     return [value]
478
479
480 def _TestEnabledInner(check_fn, names, fn):
481   """Evaluate test conditions.
482
483   @type check_fn: callable
484   @param check_fn: Callback to check whether a test is enabled
485   @type names: sequence or string
486   @param names: Test name(s)
487   @type fn: callable
488   @param fn: Aggregation function
489   @rtype: bool
490   @return: Whether test is enabled
491
492   """
493   names = _MakeSequence(names)
494
495   result = []
496
497   for name in names:
498     if isinstance(name, Either):
499       value = _TestEnabledInner(check_fn, name.tests, compat.any)
500     elif isinstance(name, (list, tuple)):
501       value = _TestEnabledInner(check_fn, name, compat.all)
502     elif callable(name):
503       value = name()
504     else:
505       value = check_fn(name)
506
507     result.append(value)
508
509   return fn(result)
510
511
512 def TestEnabled(tests, _cfg=None):
513   """Returns True if the given tests are enabled.
514
515   @param tests: A single test as a string, or a list of tests to check; can
516     contain L{Either} for OR conditions, AND is default
517
518   """
519   if _cfg is None:
520     cfg = GetConfig()
521   else:
522     cfg = _cfg
523
524   # Get settings for all tests
525   cfg_tests = cfg.get("tests", {})
526
527   # Get default setting
528   default = cfg_tests.get("default", True)
529
530   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
531                            tests, compat.all)
532
533
534 def GetInstanceCheckScript(*args):
535   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
536
537   """
538   return GetConfig().GetInstanceCheckScript(*args)
539
540
541 def GetEnabledHypervisors(*args):
542   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
543
544   """
545   return GetConfig().GetEnabledHypervisors(*args)
546
547
548 def GetDefaultHypervisor(*args):
549   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
550
551   """
552   return GetConfig().GetDefaultHypervisor(*args)
553
554
555 def GetEnabledDiskTemplates(*args):
556   """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
557
558   """
559   return GetConfig().GetEnabledDiskTemplates(*args)
560
561
562 def GetDefaultDiskTemplate(*args):
563   """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
564
565   """
566   return GetConfig().GetDefaultDiskTemplate(*args)
567
568
569 def GetMasterNode():
570   """Wrapper for L{_QaConfig.GetMasterNode}.
571
572   """
573   return GetConfig().GetMasterNode()
574
575
576 def AcquireInstance(_cfg=None):
577   """Returns an instance which isn't in use.
578
579   """
580   if _cfg is None:
581     cfg = GetConfig()
582   else:
583     cfg = _cfg
584
585   # Filter out unwanted instances
586   instances = filter(lambda inst: not inst.used, cfg["instances"])
587
588   if not instances:
589     raise qa_error.OutOfInstancesError("No instances left")
590
591   instance = instances[0]
592   instance.Use()
593
594   return instance
595
596
597 def SetExclusiveStorage(value):
598   """Wrapper for L{_QaConfig.SetExclusiveStorage}.
599
600   """
601   return GetConfig().SetExclusiveStorage(value)
602
603
604 def GetExclusiveStorage():
605   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
606
607   """
608   return GetConfig().GetExclusiveStorage()
609
610
611 def IsTemplateSupported(templ):
612   """Wrapper for L{_QaConfig.IsTemplateSupported}.
613
614   """
615   return GetConfig().IsTemplateSupported(templ)
616
617
618 def _NodeSortKey(node):
619   """Returns sort key for a node.
620
621   @type node: L{_QaNode}
622
623   """
624   return (node.use_count, utils.NiceSortKey(node.primary))
625
626
627 def AcquireNode(exclude=None, _cfg=None):
628   """Returns the least used node.
629
630   """
631   if _cfg is None:
632     cfg = GetConfig()
633   else:
634     cfg = _cfg
635
636   master = cfg.GetMasterNode()
637
638   # Filter out unwanted nodes
639   # TODO: Maybe combine filters
640   if exclude is None:
641     nodes = cfg["nodes"][:]
642   elif isinstance(exclude, (list, tuple)):
643     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
644   else:
645     nodes = filter(lambda node: node != exclude, cfg["nodes"])
646
647   nodes = filter(lambda node: node.added or node == master, nodes)
648
649   if not nodes:
650     raise qa_error.OutOfNodesError("No nodes left")
651
652   # Return node with least number of uses
653   return sorted(nodes, key=_NodeSortKey)[0].Use()
654
655
656 def AcquireManyNodes(num, exclude=None):
657   """Return the least used nodes.
658
659   @type num: int
660   @param num: Number of nodes; can be 0.
661   @type exclude: list of nodes or C{None}
662   @param exclude: nodes to be excluded from the choice
663   @rtype: list of nodes
664   @return: C{num} different nodes
665
666   """
667   nodes = []
668   if exclude is None:
669     exclude = []
670   elif isinstance(exclude, (list, tuple)):
671     # Don't modify the incoming argument
672     exclude = list(exclude)
673   else:
674     exclude = [exclude]
675
676   try:
677     for _ in range(0, num):
678       n = AcquireNode(exclude=exclude)
679       nodes.append(n)
680       exclude.append(n)
681   except qa_error.OutOfNodesError:
682     ReleaseManyNodes(nodes)
683     raise
684   return nodes
685
686
687 def ReleaseManyNodes(nodes):
688   for node in nodes:
689     node.Release()
690
691
692 def GetVclusterSettings():
693   """Wrapper for L{_QaConfig.GetVclusterSettings}.
694
695   """
696   return GetConfig().GetVclusterSettings()
697
698
699 def UseVirtualCluster(_cfg=None):
700   """Returns whether a virtual cluster is used.
701
702   @rtype: bool
703
704   """
705   if _cfg is None:
706     cfg = GetConfig()
707   else:
708     cfg = _cfg
709
710   (master, _) = cfg.GetVclusterSettings()
711
712   return bool(master)
713
714
715 @ht.WithDesc("No virtual cluster")
716 def NoVirtualCluster():
717   """Used to disable tests for virtual clusters.
718
719   """
720   return not UseVirtualCluster()