Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 090128b6

History | View | Annotate | Download (16.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
      list(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 GetVclusterSettings(self):
422
    """Returns settings for virtual cluster.
423

424
    """
425
    master = self.get(_VCLUSTER_MASTER_KEY)
426
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
427

    
428
    return (master, basedir)
429

    
430
  def GetDiskOptions(self):
431
    """Return options for the disks of the instances.
432

433
    Get 'disks' parameter from the configuration data. If 'disks' is missing,
434
    try to create it from the legacy 'disk' and 'disk-growth' parameters.
435

436
    """
437
    try:
438
      return self._data["disks"]
439
    except KeyError:
440
      pass
441

    
442
    # Legacy interface
443
    sizes = self._data.get("disk")
444
    growths = self._data.get("disk-growth")
445
    if sizes or growths:
446
      if (sizes is None or growths is None or len(sizes) != len(growths)):
447
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
448
                             " exist and have the same number of items")
449
      disks = []
450
      for (size, growth) in zip(sizes, growths):
451
        disks.append({"size": size, "growth": growth})
452
      return disks
453
    else:
454
      return None
455

    
456

    
457
def Load(path):
458
  """Loads the passed configuration file.
459

460
  """
461
  global _config # pylint: disable=W0603
462

    
463
  _config = _QaConfig.Load(path)
464

    
465

    
466
def GetConfig():
467
  """Returns the configuration object.
468

469
  """
470
  if _config is None:
471
    raise RuntimeError("Configuration not yet loaded")
472

    
473
  return _config
474

    
475

    
476
def get(name, default=None):
477
  """Wrapper for L{_QaConfig.get}.
478

479
  """
480
  return GetConfig().get(name, default=default)
481

    
482

    
483
class Either:
484
  def __init__(self, tests):
485
    """Initializes this class.
486

487
    @type tests: list or string
488
    @param tests: List of test names
489
    @see: L{TestEnabled} for details
490

491
    """
492
    self.tests = tests
493

    
494

    
495
def _MakeSequence(value):
496
  """Make sequence of single argument.
497

498
  If the single argument is not already a list or tuple, a list with the
499
  argument as a single item is returned.
500

501
  """
502
  if isinstance(value, (list, tuple)):
503
    return value
504
  else:
505
    return [value]
506

    
507

    
508
def _TestEnabledInner(check_fn, names, fn):
509
  """Evaluate test conditions.
510

511
  @type check_fn: callable
512
  @param check_fn: Callback to check whether a test is enabled
513
  @type names: sequence or string
514
  @param names: Test name(s)
515
  @type fn: callable
516
  @param fn: Aggregation function
517
  @rtype: bool
518
  @return: Whether test is enabled
519

520
  """
521
  names = _MakeSequence(names)
522

    
523
  result = []
524

    
525
  for name in names:
526
    if isinstance(name, Either):
527
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
528
    elif isinstance(name, (list, tuple)):
529
      value = _TestEnabledInner(check_fn, name, compat.all)
530
    elif callable(name):
531
      value = name()
532
    else:
533
      value = check_fn(name)
534

    
535
    result.append(value)
536

    
537
  return fn(result)
538

    
539

    
540
def TestEnabled(tests, _cfg=None):
541
  """Returns True if the given tests are enabled.
542

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

546
  """
547
  if _cfg is None:
548
    cfg = GetConfig()
549
  else:
550
    cfg = _cfg
551

    
552
  # Get settings for all tests
553
  cfg_tests = cfg.get("tests", {})
554

    
555
  # Get default setting
556
  default = cfg_tests.get("default", True)
557

    
558
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
559
                           tests, compat.all)
560

    
561

    
562
def GetInstanceCheckScript(*args):
563
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
564

565
  """
566
  return GetConfig().GetInstanceCheckScript(*args)
567

    
568

    
569
def GetEnabledHypervisors(*args):
570
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
571

572
  """
573
  return GetConfig().GetEnabledHypervisors(*args)
574

    
575

    
576
def GetDefaultHypervisor(*args):
577
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
578

579
  """
580
  return GetConfig().GetDefaultHypervisor(*args)
581

    
582

    
583
def GetEnabledDiskTemplates(*args):
584
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
585

586
  """
587
  return GetConfig().GetEnabledDiskTemplates(*args)
588

    
589

    
590
def GetDefaultDiskTemplate(*args):
591
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
592

593
  """
594
  return GetConfig().GetDefaultDiskTemplate(*args)
595

    
596

    
597
def GetMasterNode():
598
  """Wrapper for L{_QaConfig.GetMasterNode}.
599

600
  """
601
  return GetConfig().GetMasterNode()
602

    
603

    
604
def AcquireInstance(_cfg=None):
605
  """Returns an instance which isn't in use.
606

607
  """
608
  if _cfg is None:
609
    cfg = GetConfig()
610
  else:
611
    cfg = _cfg
612

    
613
  # Filter out unwanted instances
614
  instances = filter(lambda inst: not inst.used, cfg["instances"])
615

    
616
  if not instances:
617
    raise qa_error.OutOfInstancesError("No instances left")
618

    
619
  instance = instances[0]
620
  instance.Use()
621

    
622
  return instance
623

    
624

    
625
def SetExclusiveStorage(value):
626
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
627

628
  """
629
  return GetConfig().SetExclusiveStorage(value)
630

    
631

    
632
def GetExclusiveStorage():
633
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
634

635
  """
636
  return GetConfig().GetExclusiveStorage()
637

    
638

    
639
def IsTemplateSupported(templ):
640
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
641

642
  """
643
  return GetConfig().IsTemplateSupported(templ)
644

    
645

    
646
def _NodeSortKey(node):
647
  """Returns sort key for a node.
648

649
  @type node: L{_QaNode}
650

651
  """
652
  return (node.use_count, utils.NiceSortKey(node.primary))
653

    
654

    
655
def AcquireNode(exclude=None, _cfg=None):
656
  """Returns the least used node.
657

658
  """
659
  if _cfg is None:
660
    cfg = GetConfig()
661
  else:
662
    cfg = _cfg
663

    
664
  master = cfg.GetMasterNode()
665

    
666
  # Filter out unwanted nodes
667
  # TODO: Maybe combine filters
668
  if exclude is None:
669
    nodes = cfg["nodes"][:]
670
  elif isinstance(exclude, (list, tuple)):
671
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
672
  else:
673
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
674

    
675
  nodes = filter(lambda node: node.added or node == master, nodes)
676

    
677
  if not nodes:
678
    raise qa_error.OutOfNodesError("No nodes left")
679

    
680
  # Return node with least number of uses
681
  return sorted(nodes, key=_NodeSortKey)[0].Use()
682

    
683

    
684
def AcquireManyNodes(num, exclude=None):
685
  """Return the least used nodes.
686

687
  @type num: int
688
  @param num: Number of nodes; can be 0.
689
  @type exclude: list of nodes or C{None}
690
  @param exclude: nodes to be excluded from the choice
691
  @rtype: list of nodes
692
  @return: C{num} different nodes
693

694
  """
695
  nodes = []
696
  if exclude is None:
697
    exclude = []
698
  elif isinstance(exclude, (list, tuple)):
699
    # Don't modify the incoming argument
700
    exclude = list(exclude)
701
  else:
702
    exclude = [exclude]
703

    
704
  try:
705
    for _ in range(0, num):
706
      n = AcquireNode(exclude=exclude)
707
      nodes.append(n)
708
      exclude.append(n)
709
  except qa_error.OutOfNodesError:
710
    ReleaseManyNodes(nodes)
711
    raise
712
  return nodes
713

    
714

    
715
def ReleaseManyNodes(nodes):
716
  for node in nodes:
717
    node.Release()
718

    
719

    
720
def GetVclusterSettings():
721
  """Wrapper for L{_QaConfig.GetVclusterSettings}.
722

723
  """
724
  return GetConfig().GetVclusterSettings()
725

    
726

    
727
def UseVirtualCluster(_cfg=None):
728
  """Returns whether a virtual cluster is used.
729

730
  @rtype: bool
731

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

    
738
  (master, _) = cfg.GetVclusterSettings()
739

    
740
  return bool(master)
741

    
742

    
743
@ht.WithDesc("No virtual cluster")
744
def NoVirtualCluster():
745
  """Used to disable tests for virtual clusters.
746

747
  """
748
  return not UseVirtualCluster()
749

    
750

    
751
def GetDiskOptions():
752
  """Wrapper for L{_QaConfig.GetDiskOptions}.
753

754
  """
755
  return GetConfig().GetDiskOptions()