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