QA: skip lvm-based tests if lvm disabled
[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       constants.DEFAULT_ENABLED_DISK_TEMPLATES)
374
375   def GetEnabledStorageTypes(self):
376     """Returns the list of enabled storage types.
377
378     @rtype: list
379     @returns: the list of storage types enabled for QA
380
381     """
382     enabled_disk_templates = self.GetEnabledDiskTemplates()
383     enabled_storage_types = list(
384         set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
385              for dt in enabled_disk_templates]))
386     # Storage type 'lvm-pv' cannot be activated via a disk template,
387     # therefore we add it if 'lvm-vg' is present.
388     if constants.ST_LVM_VG in enabled_storage_types:
389       enabled_storage_types.append(constants.ST_LVM_PV)
390     return enabled_storage_types
391
392   def GetDefaultDiskTemplate(self):
393     """Returns the default disk template to be used.
394
395     """
396     return self.GetEnabledDiskTemplates()[0]
397
398   def _GetStringListParameter(self, key, default_values):
399     """Retrieves a parameter's value that is supposed to be a list of strings.
400
401     @rtype: list
402
403     """
404     try:
405       value = self._data[key]
406     except KeyError:
407       return default_values
408     else:
409       if value is None:
410         return []
411       elif isinstance(value, basestring):
412         return value.split(",")
413       else:
414         return value
415
416   def SetExclusiveStorage(self, value):
417     """Set the expected value of the C{exclusive_storage} flag for the cluster.
418
419     """
420     self._exclusive_storage = bool(value)
421
422   def GetExclusiveStorage(self):
423     """Get the expected value of the C{exclusive_storage} flag for the cluster.
424
425     """
426     value = self._exclusive_storage
427     assert value is not None
428     return value
429
430   def IsTemplateSupported(self, templ):
431     """Is the given disk template supported by the current configuration?
432
433     """
434     enabled = templ in self.GetEnabledDiskTemplates()
435     return enabled and (not self.GetExclusiveStorage() or
436                         templ in constants.DTS_EXCL_STORAGE)
437
438   def IsStorageTypeSupported(self, storage_type):
439     """Is the given storage type supported by the current configuration?
440
441     This is determined by looking if at least one of the disk templates
442     which is associated with the storage type is enabled in the configuration.
443
444     """
445     enabled_disk_templates = self.GetEnabledDiskTemplates()
446     if storage_type == constants.ST_LVM_PV:
447       disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
448     else:
449       disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
450     return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
451
452   def AreSpindlesSupported(self):
453     """Are spindles supported by the current configuration?
454
455     """
456     return self.GetExclusiveStorage()
457
458   def GetVclusterSettings(self):
459     """Returns settings for virtual cluster.
460
461     """
462     master = self.get(_VCLUSTER_MASTER_KEY)
463     basedir = self.get(_VCLUSTER_BASEDIR_KEY)
464
465     return (master, basedir)
466
467   def GetDiskOptions(self):
468     """Return options for the disks of the instances.
469
470     Get 'disks' parameter from the configuration data. If 'disks' is missing,
471     try to create it from the legacy 'disk' and 'disk-growth' parameters.
472
473     """
474     try:
475       return self._data["disks"]
476     except KeyError:
477       pass
478
479     # Legacy interface
480     sizes = self._data.get("disk")
481     growths = self._data.get("disk-growth")
482     if sizes or growths:
483       if (sizes is None or growths is None or len(sizes) != len(growths)):
484         raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
485                              " exist and have the same number of items")
486       disks = []
487       for (size, growth) in zip(sizes, growths):
488         disks.append({"size": size, "growth": growth})
489       return disks
490     else:
491       return None
492
493
494 def Load(path):
495   """Loads the passed configuration file.
496
497   """
498   global _config # pylint: disable=W0603
499
500   _config = _QaConfig.Load(path)
501
502
503 def GetConfig():
504   """Returns the configuration object.
505
506   """
507   if _config is None:
508     raise RuntimeError("Configuration not yet loaded")
509
510   return _config
511
512
513 def get(name, default=None):
514   """Wrapper for L{_QaConfig.get}.
515
516   """
517   return GetConfig().get(name, default=default)
518
519
520 class Either:
521   def __init__(self, tests):
522     """Initializes this class.
523
524     @type tests: list or string
525     @param tests: List of test names
526     @see: L{TestEnabled} for details
527
528     """
529     self.tests = tests
530
531
532 def _MakeSequence(value):
533   """Make sequence of single argument.
534
535   If the single argument is not already a list or tuple, a list with the
536   argument as a single item is returned.
537
538   """
539   if isinstance(value, (list, tuple)):
540     return value
541   else:
542     return [value]
543
544
545 def _TestEnabledInner(check_fn, names, fn):
546   """Evaluate test conditions.
547
548   @type check_fn: callable
549   @param check_fn: Callback to check whether a test is enabled
550   @type names: sequence or string
551   @param names: Test name(s)
552   @type fn: callable
553   @param fn: Aggregation function
554   @rtype: bool
555   @return: Whether test is enabled
556
557   """
558   names = _MakeSequence(names)
559
560   result = []
561
562   for name in names:
563     if isinstance(name, Either):
564       value = _TestEnabledInner(check_fn, name.tests, compat.any)
565     elif isinstance(name, (list, tuple)):
566       value = _TestEnabledInner(check_fn, name, compat.all)
567     elif callable(name):
568       value = name()
569     else:
570       value = check_fn(name)
571
572     result.append(value)
573
574   return fn(result)
575
576
577 def TestEnabled(tests, _cfg=None):
578   """Returns True if the given tests are enabled.
579
580   @param tests: A single test as a string, or a list of tests to check; can
581     contain L{Either} for OR conditions, AND is default
582
583   """
584   if _cfg is None:
585     cfg = GetConfig()
586   else:
587     cfg = _cfg
588
589   # Get settings for all tests
590   cfg_tests = cfg.get("tests", {})
591
592   # Get default setting
593   default = cfg_tests.get("default", True)
594
595   return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
596                            tests, compat.all)
597
598
599 def GetInstanceCheckScript(*args):
600   """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
601
602   """
603   return GetConfig().GetInstanceCheckScript(*args)
604
605
606 def GetEnabledHypervisors(*args):
607   """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
608
609   """
610   return GetConfig().GetEnabledHypervisors(*args)
611
612
613 def GetDefaultHypervisor(*args):
614   """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
615
616   """
617   return GetConfig().GetDefaultHypervisor(*args)
618
619
620 def GetEnabledDiskTemplates(*args):
621   """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
622
623   """
624   return GetConfig().GetEnabledDiskTemplates(*args)
625
626
627 def GetEnabledStorageTypes(*args):
628   """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
629
630   """
631   return GetConfig().GetEnabledStorageTypes(*args)
632
633
634 def GetDefaultDiskTemplate(*args):
635   """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
636
637   """
638   return GetConfig().GetDefaultDiskTemplate(*args)
639
640
641 def GetMasterNode():
642   """Wrapper for L{_QaConfig.GetMasterNode}.
643
644   """
645   return GetConfig().GetMasterNode()
646
647
648 def AcquireInstance(_cfg=None):
649   """Returns an instance which isn't in use.
650
651   """
652   if _cfg is None:
653     cfg = GetConfig()
654   else:
655     cfg = _cfg
656
657   # Filter out unwanted instances
658   instances = filter(lambda inst: not inst.used, cfg["instances"])
659
660   if not instances:
661     raise qa_error.OutOfInstancesError("No instances left")
662
663   instance = instances[0]
664   instance.Use()
665
666   return instance
667
668
669 def SetExclusiveStorage(value):
670   """Wrapper for L{_QaConfig.SetExclusiveStorage}.
671
672   """
673   return GetConfig().SetExclusiveStorage(value)
674
675
676 def GetExclusiveStorage():
677   """Wrapper for L{_QaConfig.GetExclusiveStorage}.
678
679   """
680   return GetConfig().GetExclusiveStorage()
681
682
683 def IsTemplateSupported(templ):
684   """Wrapper for L{_QaConfig.IsTemplateSupported}.
685
686   """
687   return GetConfig().IsTemplateSupported(templ)
688
689
690 def IsStorageTypeSupported(storage_type):
691   """Wrapper for L{_QaConfig.IsTemplateSupported}.
692
693   """
694   return GetConfig().IsStorageTypeSupported(storage_type)
695
696
697 def AreSpindlesSupported():
698   """Wrapper for L{_QaConfig.AreSpindlesSupported}.
699
700   """
701   return GetConfig().AreSpindlesSupported()
702
703
704 def _NodeSortKey(node):
705   """Returns sort key for a node.
706
707   @type node: L{_QaNode}
708
709   """
710   return (node.use_count, utils.NiceSortKey(node.primary))
711
712
713 def AcquireNode(exclude=None, _cfg=None):
714   """Returns the least used node.
715
716   """
717   if _cfg is None:
718     cfg = GetConfig()
719   else:
720     cfg = _cfg
721
722   master = cfg.GetMasterNode()
723
724   # Filter out unwanted nodes
725   # TODO: Maybe combine filters
726   if exclude is None:
727     nodes = cfg["nodes"][:]
728   elif isinstance(exclude, (list, tuple)):
729     nodes = filter(lambda node: node not in exclude, cfg["nodes"])
730   else:
731     nodes = filter(lambda node: node != exclude, cfg["nodes"])
732
733   nodes = filter(lambda node: node.added or node == master, nodes)
734
735   if not nodes:
736     raise qa_error.OutOfNodesError("No nodes left")
737
738   # Return node with least number of uses
739   return sorted(nodes, key=_NodeSortKey)[0].Use()
740
741
742 def AcquireManyNodes(num, exclude=None):
743   """Return the least used nodes.
744
745   @type num: int
746   @param num: Number of nodes; can be 0.
747   @type exclude: list of nodes or C{None}
748   @param exclude: nodes to be excluded from the choice
749   @rtype: list of nodes
750   @return: C{num} different nodes
751
752   """
753   nodes = []
754   if exclude is None:
755     exclude = []
756   elif isinstance(exclude, (list, tuple)):
757     # Don't modify the incoming argument
758     exclude = list(exclude)
759   else:
760     exclude = [exclude]
761
762   try:
763     for _ in range(0, num):
764       n = AcquireNode(exclude=exclude)
765       nodes.append(n)
766       exclude.append(n)
767   except qa_error.OutOfNodesError:
768     ReleaseManyNodes(nodes)
769     raise
770   return nodes
771
772
773 def ReleaseManyNodes(nodes):
774   for node in nodes:
775     node.Release()
776
777
778 def GetVclusterSettings():
779   """Wrapper for L{_QaConfig.GetVclusterSettings}.
780
781   """
782   return GetConfig().GetVclusterSettings()
783
784
785 def UseVirtualCluster(_cfg=None):
786   """Returns whether a virtual cluster is used.
787
788   @rtype: bool
789
790   """
791   if _cfg is None:
792     cfg = GetConfig()
793   else:
794     cfg = _cfg
795
796   (master, _) = cfg.GetVclusterSettings()
797
798   return bool(master)
799
800
801 @ht.WithDesc("No virtual cluster")
802 def NoVirtualCluster():
803   """Used to disable tests for virtual clusters.
804
805   """
806   return not UseVirtualCluster()
807
808
809 def GetDiskOptions():
810   """Wrapper for L{_QaConfig.GetDiskOptions}.
811
812   """
813   return GetConfig().GetDiskOptions()