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