Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ aac832d2

History | View | Annotate | Download (18.9 kB)

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 __setitem__(self, key, value):
328
    """Sets a configuration value.
329

330
    """
331
    self._data[key] = value
332

    
333
  def __delitem__(self, key):
334
    """Deletes a value from the configuration.
335

336
    """
337
    del(self._data[key])
338

    
339
  def __len__(self):
340
    """Return the number of configuration items.
341

342
    """
343
    return len(self._data)
344

    
345
  def get(self, name, default=None):
346
    """Returns configuration value.
347

348
    @type name: string
349
    @param name: Name of configuration entry
350
    @param default: Default value
351

352
    """
353
    return self._data.get(name, default)
354

    
355
  def GetMasterNode(self):
356
    """Returns the default master node for the cluster.
357

358
    """
359
    return self["nodes"][0]
360

    
361
  def GetInstanceCheckScript(self):
362
    """Returns path to instance check script or C{None}.
363

364
    """
365
    return self._data.get(_INSTANCE_CHECK_KEY, None)
366

    
367
  def GetEnabledHypervisors(self):
368
    """Returns list of enabled hypervisors.
369

370
    @rtype: list
371

372
    """
373
    return self._GetStringListParameter(
374
      _ENABLED_HV_KEY,
375
      [constants.DEFAULT_ENABLED_HYPERVISOR])
376

    
377
  def GetDefaultHypervisor(self):
378
    """Returns the default hypervisor to be used.
379

380
    """
381
    return self.GetEnabledHypervisors()[0]
382

    
383
  def GetEnabledDiskTemplates(self):
384
    """Returns the list of enabled disk templates.
385

386
    @rtype: list
387

388
    """
389
    return self._GetStringListParameter(
390
      _ENABLED_DISK_TEMPLATES_KEY,
391
      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
392

    
393
  def GetEnabledStorageTypes(self):
394
    """Returns the list of enabled storage types.
395

396
    @rtype: list
397
    @returns: the list of storage types enabled for QA
398

399
    """
400
    enabled_disk_templates = self.GetEnabledDiskTemplates()
401
    enabled_storage_types = list(
402
        set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
403
             for dt in enabled_disk_templates]))
404
    # Storage type 'lvm-pv' cannot be activated via a disk template,
405
    # therefore we add it if 'lvm-vg' is present.
406
    if constants.ST_LVM_VG in enabled_storage_types:
407
      enabled_storage_types.append(constants.ST_LVM_PV)
408
    return enabled_storage_types
409

    
410
  def GetDefaultDiskTemplate(self):
411
    """Returns the default disk template to be used.
412

413
    """
414
    return self.GetEnabledDiskTemplates()[0]
415

    
416
  def _GetStringListParameter(self, key, default_values):
417
    """Retrieves a parameter's value that is supposed to be a list of strings.
418

419
    @rtype: list
420

421
    """
422
    try:
423
      value = self._data[key]
424
    except KeyError:
425
      return default_values
426
    else:
427
      if value is None:
428
        return []
429
      elif isinstance(value, basestring):
430
        return value.split(",")
431
      else:
432
        return value
433

    
434
  def SetExclusiveStorage(self, value):
435
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
436

437
    """
438
    self._exclusive_storage = bool(value)
439

    
440
  def GetExclusiveStorage(self):
441
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
442

443
    """
444
    value = self._exclusive_storage
445
    assert value is not None
446
    return value
447

    
448
  def IsTemplateSupported(self, templ):
449
    """Is the given disk template supported by the current configuration?
450

451
    """
452
    enabled = templ in self.GetEnabledDiskTemplates()
453
    return enabled and (not self.GetExclusiveStorage() or
454
                        templ in constants.DTS_EXCL_STORAGE)
455

    
456
  def IsStorageTypeSupported(self, storage_type):
457
    """Is the given storage type supported by the current configuration?
458

459
    This is determined by looking if at least one of the disk templates
460
    which is associated with the storage type is enabled in the configuration.
461

462
    """
463
    enabled_disk_templates = self.GetEnabledDiskTemplates()
464
    if storage_type == constants.ST_LVM_PV:
465
      disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
466
    else:
467
      disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
468
    return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
469

    
470
  def AreSpindlesSupported(self):
471
    """Are spindles supported by the current configuration?
472

473
    """
474
    return self.GetExclusiveStorage()
475

    
476
  def GetVclusterSettings(self):
477
    """Returns settings for virtual cluster.
478

479
    """
480
    master = self.get(_VCLUSTER_MASTER_KEY)
481
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
482

    
483
    return (master, basedir)
484

    
485
  def GetDiskOptions(self):
486
    """Return options for the disks of the instances.
487

488
    Get 'disks' parameter from the configuration data. If 'disks' is missing,
489
    try to create it from the legacy 'disk' and 'disk-growth' parameters.
490

491
    """
492
    try:
493
      return self._data["disks"]
494
    except KeyError:
495
      pass
496

    
497
    # Legacy interface
498
    sizes = self._data.get("disk")
499
    growths = self._data.get("disk-growth")
500
    if sizes or growths:
501
      if (sizes is None or growths is None or len(sizes) != len(growths)):
502
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
503
                             " exist and have the same number of items")
504
      disks = []
505
      for (size, growth) in zip(sizes, growths):
506
        disks.append({"size": size, "growth": growth})
507
      return disks
508
    else:
509
      return None
510

    
511

    
512
def Load(path):
513
  """Loads the passed configuration file.
514

515
  """
516
  global _config # pylint: disable=W0603
517

    
518
  _config = _QaConfig.Load(path)
519

    
520

    
521
def GetConfig():
522
  """Returns the configuration object.
523

524
  """
525
  if _config is None:
526
    raise RuntimeError("Configuration not yet loaded")
527

    
528
  return _config
529

    
530

    
531
def get(name, default=None):
532
  """Wrapper for L{_QaConfig.get}.
533

534
  """
535
  return GetConfig().get(name, default=default)
536

    
537

    
538
class Either:
539
  def __init__(self, tests):
540
    """Initializes this class.
541

542
    @type tests: list or string
543
    @param tests: List of test names
544
    @see: L{TestEnabled} for details
545

546
    """
547
    self.tests = tests
548

    
549

    
550
def _MakeSequence(value):
551
  """Make sequence of single argument.
552

553
  If the single argument is not already a list or tuple, a list with the
554
  argument as a single item is returned.
555

556
  """
557
  if isinstance(value, (list, tuple)):
558
    return value
559
  else:
560
    return [value]
561

    
562

    
563
def _TestEnabledInner(check_fn, names, fn):
564
  """Evaluate test conditions.
565

566
  @type check_fn: callable
567
  @param check_fn: Callback to check whether a test is enabled
568
  @type names: sequence or string
569
  @param names: Test name(s)
570
  @type fn: callable
571
  @param fn: Aggregation function
572
  @rtype: bool
573
  @return: Whether test is enabled
574

575
  """
576
  names = _MakeSequence(names)
577

    
578
  result = []
579

    
580
  for name in names:
581
    if isinstance(name, Either):
582
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
583
    elif isinstance(name, (list, tuple)):
584
      value = _TestEnabledInner(check_fn, name, compat.all)
585
    elif callable(name):
586
      value = name()
587
    else:
588
      value = check_fn(name)
589

    
590
    result.append(value)
591

    
592
  return fn(result)
593

    
594

    
595
def TestEnabled(tests, _cfg=None):
596
  """Returns True if the given tests are enabled.
597

598
  @param tests: A single test as a string, or a list of tests to check; can
599
    contain L{Either} for OR conditions, AND is default
600

601
  """
602
  if _cfg is None:
603
    cfg = GetConfig()
604
  else:
605
    cfg = _cfg
606

    
607
  # Get settings for all tests
608
  cfg_tests = cfg.get("tests", {})
609

    
610
  # Get default setting
611
  default = cfg_tests.get("default", True)
612

    
613
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
614
                           tests, compat.all)
615

    
616

    
617
def GetInstanceCheckScript(*args):
618
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
619

620
  """
621
  return GetConfig().GetInstanceCheckScript(*args)
622

    
623

    
624
def GetEnabledHypervisors(*args):
625
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
626

627
  """
628
  return GetConfig().GetEnabledHypervisors(*args)
629

    
630

    
631
def GetDefaultHypervisor(*args):
632
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
633

634
  """
635
  return GetConfig().GetDefaultHypervisor(*args)
636

    
637

    
638
def GetEnabledDiskTemplates(*args):
639
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
640

641
  """
642
  return GetConfig().GetEnabledDiskTemplates(*args)
643

    
644

    
645
def GetEnabledStorageTypes(*args):
646
  """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
647

648
  """
649
  return GetConfig().GetEnabledStorageTypes(*args)
650

    
651

    
652
def GetDefaultDiskTemplate(*args):
653
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
654

655
  """
656
  return GetConfig().GetDefaultDiskTemplate(*args)
657

    
658

    
659
def GetMasterNode():
660
  """Wrapper for L{_QaConfig.GetMasterNode}.
661

662
  """
663
  return GetConfig().GetMasterNode()
664

    
665

    
666
def AcquireInstance(_cfg=None):
667
  """Returns an instance which isn't in use.
668

669
  """
670
  if _cfg is None:
671
    cfg = GetConfig()
672
  else:
673
    cfg = _cfg
674

    
675
  # Filter out unwanted instances
676
  instances = filter(lambda inst: not inst.used, cfg["instances"])
677

    
678
  if not instances:
679
    raise qa_error.OutOfInstancesError("No instances left")
680

    
681
  instance = instances[0]
682
  instance.Use()
683

    
684
  return instance
685

    
686

    
687
def SetExclusiveStorage(value):
688
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
689

690
  """
691
  return GetConfig().SetExclusiveStorage(value)
692

    
693

    
694
def GetExclusiveStorage():
695
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
696

697
  """
698
  return GetConfig().GetExclusiveStorage()
699

    
700

    
701
def IsTemplateSupported(templ):
702
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
703

704
  """
705
  return GetConfig().IsTemplateSupported(templ)
706

    
707

    
708
def IsStorageTypeSupported(storage_type):
709
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
710

711
  """
712
  return GetConfig().IsStorageTypeSupported(storage_type)
713

    
714

    
715
def AreSpindlesSupported():
716
  """Wrapper for L{_QaConfig.AreSpindlesSupported}.
717

718
  """
719
  return GetConfig().AreSpindlesSupported()
720

    
721

    
722
def _NodeSortKey(node):
723
  """Returns sort key for a node.
724

725
  @type node: L{_QaNode}
726

727
  """
728
  return (node.use_count, utils.NiceSortKey(node.primary))
729

    
730

    
731
def AcquireNode(exclude=None, _cfg=None):
732
  """Returns the least used node.
733

734
  """
735
  if _cfg is None:
736
    cfg = GetConfig()
737
  else:
738
    cfg = _cfg
739

    
740
  master = cfg.GetMasterNode()
741

    
742
  # Filter out unwanted nodes
743
  # TODO: Maybe combine filters
744
  if exclude is None:
745
    nodes = cfg["nodes"][:]
746
  elif isinstance(exclude, (list, tuple)):
747
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
748
  else:
749
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
750

    
751
  nodes = filter(lambda node: node.added or node == master, nodes)
752

    
753
  if not nodes:
754
    raise qa_error.OutOfNodesError("No nodes left")
755

    
756
  # Return node with least number of uses
757
  return sorted(nodes, key=_NodeSortKey)[0].Use()
758

    
759

    
760
def AcquireManyNodes(num, exclude=None):
761
  """Return the least used nodes.
762

763
  @type num: int
764
  @param num: Number of nodes; can be 0.
765
  @type exclude: list of nodes or C{None}
766
  @param exclude: nodes to be excluded from the choice
767
  @rtype: list of nodes
768
  @return: C{num} different nodes
769

770
  """
771
  nodes = []
772
  if exclude is None:
773
    exclude = []
774
  elif isinstance(exclude, (list, tuple)):
775
    # Don't modify the incoming argument
776
    exclude = list(exclude)
777
  else:
778
    exclude = [exclude]
779

    
780
  try:
781
    for _ in range(0, num):
782
      n = AcquireNode(exclude=exclude)
783
      nodes.append(n)
784
      exclude.append(n)
785
  except qa_error.OutOfNodesError:
786
    ReleaseManyNodes(nodes)
787
    raise
788
  return nodes
789

    
790

    
791
def ReleaseManyNodes(nodes):
792
  for node in nodes:
793
    node.Release()
794

    
795

    
796
def GetVclusterSettings():
797
  """Wrapper for L{_QaConfig.GetVclusterSettings}.
798

799
  """
800
  return GetConfig().GetVclusterSettings()
801

    
802

    
803
def UseVirtualCluster(_cfg=None):
804
  """Returns whether a virtual cluster is used.
805

806
  @rtype: bool
807

808
  """
809
  if _cfg is None:
810
    cfg = GetConfig()
811
  else:
812
    cfg = _cfg
813

    
814
  (master, _) = cfg.GetVclusterSettings()
815

    
816
  return bool(master)
817

    
818

    
819
@ht.WithDesc("No virtual cluster")
820
def NoVirtualCluster():
821
  """Used to disable tests for virtual clusters.
822

823
  """
824
  return not UseVirtualCluster()
825

    
826

    
827
def GetDiskOptions():
828
  """Wrapper for L{_QaConfig.GetDiskOptions}.
829

830
  """
831
  return GetConfig().GetDiskOptions()