Bump new upstream version
[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     disks = self.GetDiskOptions()
284     if disks is None:
285       raise qa_error.Error("Config option 'disks' must exist")
286     else:
287       for d in disks:
288         if d.get("size") is None or d.get("growth") is None:
289           raise qa_error.Error("Config options `size` and `growth` must exist"
290                                " for all `disks` items")
291     check = self.GetInstanceCheckScript()
292     if check:
293       try:
294         os.stat(check)
295       except EnvironmentError, err:
296         raise qa_error.Error("Can't find instance check script '%s': %s" %
297                              (check, err))
298
299     enabled_hv = frozenset(self.GetEnabledHypervisors())
300     if not enabled_hv:
301       raise qa_error.Error("No hypervisor is enabled")
302
303     difference = enabled_hv - constants.HYPER_TYPES
304     if difference:
305       raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
306                            utils.CommaJoin(difference))
307
308     (vc_master, vc_basedir) = self.GetVclusterSettings()
309     if bool(vc_master) != bool(vc_basedir):
310       raise qa_error.Error("All or none of the config options '%s' and '%s'"
311                            " must be set" %
312                            (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
313
314     if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
315       raise qa_error.Error("Path given in option '%s' must be absolute and"
316                            " normalized" % _VCLUSTER_BASEDIR_KEY)
317
318   def __getitem__(self, name):
319     """Returns configuration value.
320
321     @type name: string
322     @param name: Name of configuration entry
323
324     """
325     return self._data[name]
326
327   def get(self, name, default=None):
328     """Returns configuration value.
329
330     @type name: string
331     @param name: Name of configuration entry
332     @param default: Default value
333
334     """
335     return self._data.get(name, default)
336
337   def GetMasterNode(self):
338     """Returns the default master node for the cluster.
339
340     """
341     return self["nodes"][0]
342
343   def GetInstanceCheckScript(self):
344     """Returns path to instance check script or C{None}.
345
346     """
347     return self._data.get(_INSTANCE_CHECK_KEY, None)
348
349   def GetEnabledHypervisors(self):
350     """Returns list of enabled hypervisors.
351
352     @rtype: list
353
354     """
355     return self._GetStringListParameter(
356       _ENABLED_HV_KEY,
357       [constants.DEFAULT_ENABLED_HYPERVISOR])
358
359   def GetDefaultHypervisor(self):
360     """Returns the default hypervisor to be used.
361
362     """
363     return self.GetEnabledHypervisors()[0]
364
365   def GetEnabledDiskTemplates(self):
366     """Returns the list of enabled disk templates.
367
368     @rtype: list
369
370     """
371     return self._GetStringListParameter(
372       _ENABLED_DISK_TEMPLATES_KEY,
373       list(constants.DEFAULT_ENABLED_DISK_TEMPLATES))
374
375   def GetDefaultDiskTemplate(self):
376     """Returns the default disk template to be used.
377
378     """
379     return self.GetEnabledDiskTemplates()[0]
380
381   def _GetStringListParameter(self, key, default_values):
382     """Retrieves a parameter's value that is supposed to be a list of strings.
383
384     @rtype: list
385
386     """
387     try:
388       value = self._data[key]
389     except KeyError:
390       return default_values
391     else:
392       if value is None:
393         return []
394       elif isinstance(value, basestring):
395         return value.split(",")
396       else:
397         return value
398
399   def SetExclusiveStorage(self, value):
400     """Set the expected value of the C{exclusive_storage} flag for the cluster.
401
402     """
403     self._exclusive_storage = bool(value)
404
405   def GetExclusiveStorage(self):
406     """Get the expected value of the C{exclusive_storage} flag for the cluster.
407
408     """
409     value = self._exclusive_storage
410     assert value is not None
411     return value
412
413   def IsTemplateSupported(self, templ):
414     """Is the given disk template supported by the current configuration?
415
416     """
417     enabled = templ in self.GetEnabledDiskTemplates()
418     return enabled and (not self.GetExclusiveStorage() or
419                         templ in constants.DTS_EXCL_STORAGE)
420
421   def GetVclusterSettings(self):
422     """Returns settings for virtual cluster.
423
424     """
425     master = self.get(_VCLUSTER_MASTER_KEY)
426     basedir = self.get(_VCLUSTER_BASEDIR_KEY)
427
428     return (master, basedir)
429
430   def GetDiskOptions(self):
431     """Return options for the disks of the instances.
432
433     Get 'disks' parameter from the configuration data. If 'disks' is missing,
434     try to create it from the legacy 'disk' and 'disk-growth' parameters.
435
436     """
437     try:
438       return self._data["disks"]
439     except KeyError:
440       pass
441
442     # Legacy interface
443     sizes = self._data.get("disk")
444     growths = self._data.get("disk-growth")
445     if sizes or growths:
446       if (sizes is None or growths is None or len(sizes) != len(growths)):
447         raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
448                              " exist and have the same number of items")
449       disks = []
450       for (size, growth) in zip(sizes, growths):
451         disks.append({"size": size, "growth": growth})
452       return disks
453     else:
454       return None
455
456
457 def Load(path):
458   """Loads the passed configuration file.
459
460   """
461   global _config # pylint: disable=W0603
462
463   _config = _QaConfig.Load(path)
464
465
466 def GetConfig():
467   """Returns the configuration object.
468
469   """
470   if _config is None:
471     raise RuntimeError("Configuration not yet loaded")
472
473   return _config
474
475
476 def get(name, default=None):
477   """Wrapper for L{_QaConfig.get}.
478
479   """
480   return GetConfig().get(name, default=default)
481
482
483 class Either:
484   def __init__(self, tests):
485     """Initializes this class.
486
487     @type tests: list or string
488     @param tests: List of test names
489     @see: L{TestEnabled} for details
490
491     """
492     self.tests = tests
493
494
495 def _MakeSequence(value):
496   """Make sequence of single argument.
497
498   If the single argument is not already a list or tuple, a list with the
499   argument as a single item is returned.
500
501   """
502   if isinstance(value, (list, tuple)):
503     return value
504   else:
505     return [value]
506
507
508 def _TestEnabledInner(check_fn, names, fn):
509   """Evaluate test conditions.
510
511   @type check_fn: callable
512   @param check_fn: Callback to check whether a test is enabled
513   @type names: sequence or string
514   @param names: Test name(s)
515   @type fn: callable
516   @param fn: Aggregation function
517   @rtype: bool
518   @return: Whether test is enabled
519
520   """
521   names = _MakeSequence(names)
522
523   result = []
524
525   for name in names:
526     if isinstance(name, Either):
527       value = _TestEnabledInner(check_fn, name.tests, compat.any)
528     elif isinstance(name, (list, tuple)):
529       value = _TestEnabledInner(check_fn, name, compat.all)
530     elif callable(name):
531       value = name()
532     else:
533       value = check_fn(name)
534
535     result.append(value)
536
537   return fn(result)
538
539
540 def TestEnabled(tests, _cfg=None):
541   """Returns True if the given tests are enabled.
542
543   @param tests: A single test as a string, or a list of tests to check; can
544     contain L{Either} for OR conditions, AND is default
545
546   """
547   if _cfg is None:
548     cfg = GetConfig()
549   else:
550     cfg = _cfg
551
552   # Get settings for all tests
553   cfg_tests = cfg.get("tests", {})
554
555   # Get default setting
556   default = cfg_tests.get("default", True)
557
558   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
559                            tests, compat.all)
560
561
562 def GetInstanceCheckScript(*args):
563   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
564
565   """
566   return GetConfig().GetInstanceCheckScript(*args)
567
568
569 def GetEnabledHypervisors(*args):
570   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
571
572   """
573   return GetConfig().GetEnabledHypervisors(*args)
574
575
576 def GetDefaultHypervisor(*args):
577   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
578
579   """
580   return GetConfig().GetDefaultHypervisor(*args)
581
582
583 def GetEnabledDiskTemplates(*args):
584   """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
585
586   """
587   return GetConfig().GetEnabledDiskTemplates(*args)
588
589
590 def GetDefaultDiskTemplate(*args):
591   """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
592
593   """
594   return GetConfig().GetDefaultDiskTemplate(*args)
595
596
597 def GetMasterNode():
598   """Wrapper for L{_QaConfig.GetMasterNode}.
599
600   """
601   return GetConfig().GetMasterNode()
602
603
604 def AcquireInstance(_cfg=None):
605   """Returns an instance which isn't in use.
606
607   """
608   if _cfg is None:
609     cfg = GetConfig()
610   else:
611     cfg = _cfg
612
613   # Filter out unwanted instances
614   instances = filter(lambda inst: not inst.used, cfg["instances"])
615
616   if not instances:
617     raise qa_error.OutOfInstancesError("No instances left")
618
619   instance = instances[0]
620   instance.Use()
621
622   return instance
623
624
625 def SetExclusiveStorage(value):
626   """Wrapper for L{_QaConfig.SetExclusiveStorage}.
627
628   """
629   return GetConfig().SetExclusiveStorage(value)
630
631
632 def GetExclusiveStorage():
633   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
634
635   """
636   return GetConfig().GetExclusiveStorage()
637
638
639 def IsTemplateSupported(templ):
640   """Wrapper for L{_QaConfig.IsTemplateSupported}.
641
642   """
643   return GetConfig().IsTemplateSupported(templ)
644
645
646 def _NodeSortKey(node):
647   """Returns sort key for a node.
648
649   @type node: L{_QaNode}
650
651   """
652   return (node.use_count, utils.NiceSortKey(node.primary))
653
654
655 def AcquireNode(exclude=None, _cfg=None):
656   """Returns the least used node.
657
658   """
659   if _cfg is None:
660     cfg = GetConfig()
661   else:
662     cfg = _cfg
663
664   master = cfg.GetMasterNode()
665
666   # Filter out unwanted nodes
667   # TODO: Maybe combine filters
668   if exclude is None:
669     nodes = cfg["nodes"][:]
670   elif isinstance(exclude, (list, tuple)):
671     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
672   else:
673     nodes = filter(lambda node: node != exclude, cfg["nodes"])
674
675   nodes = filter(lambda node: node.added or node == master, nodes)
676
677   if not nodes:
678     raise qa_error.OutOfNodesError("No nodes left")
679
680   # Return node with least number of uses
681   return sorted(nodes, key=_NodeSortKey)[0].Use()
682
683
684 def AcquireManyNodes(num, exclude=None):
685   """Return the least used nodes.
686
687   @type num: int
688   @param num: Number of nodes; can be 0.
689   @type exclude: list of nodes or C{None}
690   @param exclude: nodes to be excluded from the choice
691   @rtype: list of nodes
692   @return: C{num} different nodes
693
694   """
695   nodes = []
696   if exclude is None:
697     exclude = []
698   elif isinstance(exclude, (list, tuple)):
699     # Don't modify the incoming argument
700     exclude = list(exclude)
701   else:
702     exclude = [exclude]
703
704   try:
705     for _ in range(0, num):
706       n = AcquireNode(exclude=exclude)
707       nodes.append(n)
708       exclude.append(n)
709   except qa_error.OutOfNodesError:
710     ReleaseManyNodes(nodes)
711     raise
712   return nodes
713
714
715 def ReleaseManyNodes(nodes):
716   for node in nodes:
717     node.Release()
718
719
720 def GetVclusterSettings():
721   """Wrapper for L{_QaConfig.GetVclusterSettings}.
722
723   """
724   return GetConfig().GetVclusterSettings()
725
726
727 def UseVirtualCluster(_cfg=None):
728   """Returns whether a virtual cluster is used.
729
730   @rtype: bool
731
732   """
733   if _cfg is None:
734     cfg = GetConfig()
735   else:
736     cfg = _cfg
737
738   (master, _) = cfg.GetVclusterSettings()
739
740   return bool(master)
741
742
743 @ht.WithDesc("No virtual cluster")
744 def NoVirtualCluster():
745   """Used to disable tests for virtual clusters.
746
747   """
748   return not UseVirtualCluster()
749
750
751 def GetDiskOptions():
752   """Wrapper for L{_QaConfig.GetDiskOptions}.
753
754   """
755   return GetConfig().GetDiskOptions()