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