Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 615551b2

History | View | Annotate | Download (17.8 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 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()