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