Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ b7630577

History | View | Annotate | Download (17.5 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
# The path of an optional JSON Patch file (as per RFC6902) that modifies QA's
44
# configuration.
45
_PATCH_JSON = os.path.join(os.path.dirname(__file__), "qa-patch.json")
46

    
47
#: QA configuration (L{_QaConfig})
48
_config = None
49

    
50

    
51
class _QaInstance(object):
52
  __slots__ = [
53
    "name",
54
    "nicmac",
55
    "_used",
56
    "_disk_template",
57
    ]
58

    
59
  def __init__(self, name, nicmac):
60
    """Initializes instances of this class.
61

62
    """
63
    self.name = name
64
    self.nicmac = nicmac
65
    self._used = None
66
    self._disk_template = None
67

    
68
  @classmethod
69
  def FromDict(cls, data):
70
    """Creates instance object from JSON dictionary.
71

72
    """
73
    nicmac = []
74

    
75
    macaddr = data.get("nic.mac/0")
76
    if macaddr:
77
      nicmac.append(macaddr)
78

    
79
    return cls(name=data["name"], nicmac=nicmac)
80

    
81
  def __repr__(self):
82
    status = [
83
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
84
      "name=%s" % self.name,
85
      "nicmac=%s" % self.nicmac,
86
      "used=%s" % self._used,
87
      "disk_template=%s" % self._disk_template,
88
      ]
89

    
90
    return "<%s at %#x>" % (" ".join(status), id(self))
91

    
92
  def Use(self):
93
    """Marks instance as being in use.
94

95
    """
96
    assert not self._used
97
    assert self._disk_template is None
98

    
99
    self._used = True
100

    
101
  def Release(self):
102
    """Releases instance and makes it available again.
103

104
    """
105
    assert self._used, \
106
      ("Instance '%s' was never acquired or released more than once" %
107
       self.name)
108

    
109
    self._used = False
110
    self._disk_template = None
111

    
112
  def GetNicMacAddr(self, idx, default):
113
    """Returns MAC address for NIC.
114

115
    @type idx: int
116
    @param idx: NIC index
117
    @param default: Default value
118

119
    """
120
    if len(self.nicmac) > idx:
121
      return self.nicmac[idx]
122
    else:
123
      return default
124

    
125
  def SetDiskTemplate(self, template):
126
    """Set the disk template.
127

128
    """
129
    assert template in constants.DISK_TEMPLATES
130

    
131
    self._disk_template = template
132

    
133
  @property
134
  def used(self):
135
    """Returns boolean denoting whether instance is in use.
136

137
    """
138
    return self._used
139

    
140
  @property
141
  def disk_template(self):
142
    """Returns the current disk template.
143

144
    """
145
    return self._disk_template
146

    
147

    
148
class _QaNode(object):
149
  __slots__ = [
150
    "primary",
151
    "secondary",
152
    "_added",
153
    "_use_count",
154
    ]
155

    
156
  def __init__(self, primary, secondary):
157
    """Initializes instances of this class.
158

159
    """
160
    self.primary = primary
161
    self.secondary = secondary
162
    self._added = False
163
    self._use_count = 0
164

    
165
  @classmethod
166
  def FromDict(cls, data):
167
    """Creates node object from JSON dictionary.
168

169
    """
170
    return cls(primary=data["primary"], secondary=data.get("secondary"))
171

    
172
  def __repr__(self):
173
    status = [
174
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
175
      "primary=%s" % self.primary,
176
      "secondary=%s" % self.secondary,
177
      "added=%s" % self._added,
178
      "use_count=%s" % self._use_count,
179
      ]
180

    
181
    return "<%s at %#x>" % (" ".join(status), id(self))
182

    
183
  def Use(self):
184
    """Marks a node as being in use.
185

186
    """
187
    assert self._use_count >= 0
188

    
189
    self._use_count += 1
190

    
191
    return self
192

    
193
  def Release(self):
194
    """Release a node (opposite of L{Use}).
195

196
    """
197
    assert self.use_count > 0
198

    
199
    self._use_count -= 1
200

    
201
  def MarkAdded(self):
202
    """Marks node as having been added to a cluster.
203

204
    """
205
    assert not self._added
206
    self._added = True
207

    
208
  def MarkRemoved(self):
209
    """Marks node as having been removed from a cluster.
210

211
    """
212
    assert self._added
213
    self._added = False
214

    
215
  @property
216
  def added(self):
217
    """Returns whether a node is part of a cluster.
218

219
    """
220
    return self._added
221

    
222
  @property
223
  def use_count(self):
224
    """Returns number of current uses (controlled by L{Use} and L{Release}).
225

226
    """
227
    return self._use_count
228

    
229

    
230
_RESOURCE_CONVERTER = {
231
  "instances": _QaInstance.FromDict,
232
  "nodes": _QaNode.FromDict,
233
  }
234

    
235

    
236
def _ConvertResources((key, value)):
237
  """Converts cluster resources in configuration to Python objects.
238

239
  """
240
  fn = _RESOURCE_CONVERTER.get(key, None)
241
  if fn:
242
    return (key, map(fn, value))
243
  else:
244
    return (key, value)
245

    
246

    
247
class _QaConfig(object):
248
  def __init__(self, data):
249
    """Initializes instances of this class.
250

251
    """
252
    self._data = data
253

    
254
    #: Cluster-wide run-time value of the exclusive storage flag
255
    self._exclusive_storage = None
256

    
257
  @classmethod
258
  def Load(cls, filename):
259
    """Loads a configuration file and produces a configuration object.
260

261
    @type filename: string
262
    @param filename: Path to configuration file
263
    @rtype: L{_QaConfig}
264

265
    """
266
    data = serializer.LoadJson(utils.ReadFile(filename))
267

    
268
    # Patch the document using JSON Patch (RFC6902) in file _PATCH_JSON, if
269
    # available
270
    try:
271
      patch = serializer.LoadJson(utils.ReadFile(_PATCH_JSON))
272
      if patch:
273
        mod = __import__("jsonpatch", fromlist=[])
274
        data = mod.apply_patch(data, patch)
275
    except IOError:
276
      pass
277
    except ImportError:
278
      raise qa_error.Error("If you want to use the QA JSON patching feature,"
279
                           " you need to install Python modules"
280
                           " 'jsonpatch' and 'jsonpointer'.")
281

    
282
    result = cls(dict(map(_ConvertResources,
283
                          data.items()))) # pylint: disable=E1103
284
    result.Validate()
285

    
286
    return result
287

    
288
  def Validate(self):
289
    """Validates loaded configuration data.
290

291
    """
292
    if not self.get("name"):
293
      raise qa_error.Error("Cluster name is required")
294

    
295
    if not self.get("nodes"):
296
      raise qa_error.Error("Need at least one node")
297

    
298
    if not self.get("instances"):
299
      raise qa_error.Error("Need at least one instance")
300

    
301
    disks = self.GetDiskOptions()
302
    if disks is None:
303
      raise qa_error.Error("Config option 'disks' must exist")
304
    else:
305
      for d in disks:
306
        if d.get("size") is None or d.get("growth") is None:
307
          raise qa_error.Error("Config options `size` and `growth` must exist"
308
                               " for all `disks` items")
309
    check = self.GetInstanceCheckScript()
310
    if check:
311
      try:
312
        os.stat(check)
313
      except EnvironmentError, err:
314
        raise qa_error.Error("Can't find instance check script '%s': %s" %
315
                             (check, err))
316

    
317
    enabled_hv = frozenset(self.GetEnabledHypervisors())
318
    if not enabled_hv:
319
      raise qa_error.Error("No hypervisor is enabled")
320

    
321
    difference = enabled_hv - constants.HYPER_TYPES
322
    if difference:
323
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
324
                           utils.CommaJoin(difference))
325

    
326
    (vc_master, vc_basedir) = self.GetVclusterSettings()
327
    if bool(vc_master) != bool(vc_basedir):
328
      raise qa_error.Error("All or none of the config options '%s' and '%s'"
329
                           " must be set" %
330
                           (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
331

    
332
    if vc_basedir and not utils.IsNormAbsPath(vc_basedir):
333
      raise qa_error.Error("Path given in option '%s' must be absolute and"
334
                           " normalized" % _VCLUSTER_BASEDIR_KEY)
335

    
336
  def __getitem__(self, name):
337
    """Returns configuration value.
338

339
    @type name: string
340
    @param name: Name of configuration entry
341

342
    """
343
    return self._data[name]
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
      list(constants.DEFAULT_ENABLED_DISK_TEMPLATES))
392

    
393
  def GetDefaultDiskTemplate(self):
394
    """Returns the default disk template to be used.
395

396
    """
397
    return self.GetEnabledDiskTemplates()[0]
398

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

402
    @rtype: list
403

404
    """
405
    try:
406
      value = self._data[key]
407
    except KeyError:
408
      return default_values
409
    else:
410
      if value is None:
411
        return []
412
      elif isinstance(value, basestring):
413
        return value.split(",")
414
      else:
415
        return value
416

    
417
  def SetExclusiveStorage(self, value):
418
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
419

420
    """
421
    self._exclusive_storage = bool(value)
422

    
423
  def GetExclusiveStorage(self):
424
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
425

426
    """
427
    value = self._exclusive_storage
428
    assert value is not None
429
    return value
430

    
431
  def IsTemplateSupported(self, templ):
432
    """Is the given disk template supported by the current configuration?
433

434
    """
435
    enabled = templ in self.GetEnabledDiskTemplates()
436
    return enabled and (not self.GetExclusiveStorage() or
437
                        templ in constants.DTS_EXCL_STORAGE)
438

    
439
  def GetVclusterSettings(self):
440
    """Returns settings for virtual cluster.
441

442
    """
443
    master = self.get(_VCLUSTER_MASTER_KEY)
444
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
445

    
446
    return (master, basedir)
447

    
448
  def GetDiskOptions(self):
449
    """Return options for the disks of the instances.
450

451
    Get 'disks' parameter from the configuration data. If 'disks' is missing,
452
    try to create it from the legacy 'disk' and 'disk-growth' parameters.
453

454
    """
455
    try:
456
      return self._data["disks"]
457
    except KeyError:
458
      pass
459

    
460
    # Legacy interface
461
    sizes = self._data.get("disk")
462
    growths = self._data.get("disk-growth")
463
    if sizes or growths:
464
      if (sizes is None or growths is None or len(sizes) != len(growths)):
465
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
466
                             " exist and have the same number of items")
467
      disks = []
468
      for (size, growth) in zip(sizes, growths):
469
        disks.append({"size": size, "growth": growth})
470
      return disks
471
    else:
472
      return None
473

    
474

    
475
def Load(path):
476
  """Loads the passed configuration file.
477

478
  """
479
  global _config # pylint: disable=W0603
480

    
481
  _config = _QaConfig.Load(path)
482

    
483

    
484
def GetConfig():
485
  """Returns the configuration object.
486

487
  """
488
  if _config is None:
489
    raise RuntimeError("Configuration not yet loaded")
490

    
491
  return _config
492

    
493

    
494
def get(name, default=None):
495
  """Wrapper for L{_QaConfig.get}.
496

497
  """
498
  return GetConfig().get(name, default=default)
499

    
500

    
501
class Either:
502
  def __init__(self, tests):
503
    """Initializes this class.
504

505
    @type tests: list or string
506
    @param tests: List of test names
507
    @see: L{TestEnabled} for details
508

509
    """
510
    self.tests = tests
511

    
512

    
513
def _MakeSequence(value):
514
  """Make sequence of single argument.
515

516
  If the single argument is not already a list or tuple, a list with the
517
  argument as a single item is returned.
518

519
  """
520
  if isinstance(value, (list, tuple)):
521
    return value
522
  else:
523
    return [value]
524

    
525

    
526
def _TestEnabledInner(check_fn, names, fn):
527
  """Evaluate test conditions.
528

529
  @type check_fn: callable
530
  @param check_fn: Callback to check whether a test is enabled
531
  @type names: sequence or string
532
  @param names: Test name(s)
533
  @type fn: callable
534
  @param fn: Aggregation function
535
  @rtype: bool
536
  @return: Whether test is enabled
537

538
  """
539
  names = _MakeSequence(names)
540

    
541
  result = []
542

    
543
  for name in names:
544
    if isinstance(name, Either):
545
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
546
    elif isinstance(name, (list, tuple)):
547
      value = _TestEnabledInner(check_fn, name, compat.all)
548
    elif callable(name):
549
      value = name()
550
    else:
551
      value = check_fn(name)
552

    
553
    result.append(value)
554

    
555
  return fn(result)
556

    
557

    
558
def TestEnabled(tests, _cfg=None):
559
  """Returns True if the given tests are enabled.
560

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

564
  """
565
  if _cfg is None:
566
    cfg = GetConfig()
567
  else:
568
    cfg = _cfg
569

    
570
  # Get settings for all tests
571
  cfg_tests = cfg.get("tests", {})
572

    
573
  # Get default setting
574
  default = cfg_tests.get("default", True)
575

    
576
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
577
                           tests, compat.all)
578

    
579

    
580
def GetInstanceCheckScript(*args):
581
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
582

583
  """
584
  return GetConfig().GetInstanceCheckScript(*args)
585

    
586

    
587
def GetEnabledHypervisors(*args):
588
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
589

590
  """
591
  return GetConfig().GetEnabledHypervisors(*args)
592

    
593

    
594
def GetDefaultHypervisor(*args):
595
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
596

597
  """
598
  return GetConfig().GetDefaultHypervisor(*args)
599

    
600

    
601
def GetEnabledDiskTemplates(*args):
602
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
603

604
  """
605
  return GetConfig().GetEnabledDiskTemplates(*args)
606

    
607

    
608
def GetDefaultDiskTemplate(*args):
609
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
610

611
  """
612
  return GetConfig().GetDefaultDiskTemplate(*args)
613

    
614

    
615
def GetMasterNode():
616
  """Wrapper for L{_QaConfig.GetMasterNode}.
617

618
  """
619
  return GetConfig().GetMasterNode()
620

    
621

    
622
def AcquireInstance(_cfg=None):
623
  """Returns an instance which isn't in use.
624

625
  """
626
  if _cfg is None:
627
    cfg = GetConfig()
628
  else:
629
    cfg = _cfg
630

    
631
  # Filter out unwanted instances
632
  instances = filter(lambda inst: not inst.used, cfg["instances"])
633

    
634
  if not instances:
635
    raise qa_error.OutOfInstancesError("No instances left")
636

    
637
  instance = instances[0]
638
  instance.Use()
639

    
640
  return instance
641

    
642

    
643
def SetExclusiveStorage(value):
644
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
645

646
  """
647
  return GetConfig().SetExclusiveStorage(value)
648

    
649

    
650
def GetExclusiveStorage():
651
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
652

653
  """
654
  return GetConfig().GetExclusiveStorage()
655

    
656

    
657
def IsTemplateSupported(templ):
658
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
659

660
  """
661
  return GetConfig().IsTemplateSupported(templ)
662

    
663

    
664
def _NodeSortKey(node):
665
  """Returns sort key for a node.
666

667
  @type node: L{_QaNode}
668

669
  """
670
  return (node.use_count, utils.NiceSortKey(node.primary))
671

    
672

    
673
def AcquireNode(exclude=None, _cfg=None):
674
  """Returns the least used node.
675

676
  """
677
  if _cfg is None:
678
    cfg = GetConfig()
679
  else:
680
    cfg = _cfg
681

    
682
  master = cfg.GetMasterNode()
683

    
684
  # Filter out unwanted nodes
685
  # TODO: Maybe combine filters
686
  if exclude is None:
687
    nodes = cfg["nodes"][:]
688
  elif isinstance(exclude, (list, tuple)):
689
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
690
  else:
691
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
692

    
693
  nodes = filter(lambda node: node.added or node == master, nodes)
694

    
695
  if not nodes:
696
    raise qa_error.OutOfNodesError("No nodes left")
697

    
698
  # Return node with least number of uses
699
  return sorted(nodes, key=_NodeSortKey)[0].Use()
700

    
701

    
702
def AcquireManyNodes(num, exclude=None):
703
  """Return the least used nodes.
704

705
  @type num: int
706
  @param num: Number of nodes; can be 0.
707
  @type exclude: list of nodes or C{None}
708
  @param exclude: nodes to be excluded from the choice
709
  @rtype: list of nodes
710
  @return: C{num} different nodes
711

712
  """
713
  nodes = []
714
  if exclude is None:
715
    exclude = []
716
  elif isinstance(exclude, (list, tuple)):
717
    # Don't modify the incoming argument
718
    exclude = list(exclude)
719
  else:
720
    exclude = [exclude]
721

    
722
  try:
723
    for _ in range(0, num):
724
      n = AcquireNode(exclude=exclude)
725
      nodes.append(n)
726
      exclude.append(n)
727
  except qa_error.OutOfNodesError:
728
    ReleaseManyNodes(nodes)
729
    raise
730
  return nodes
731

    
732

    
733
def ReleaseManyNodes(nodes):
734
  for node in nodes:
735
    node.Release()
736

    
737

    
738
def GetVclusterSettings():
739
  """Wrapper for L{_QaConfig.GetVclusterSettings}.
740

741
  """
742
  return GetConfig().GetVclusterSettings()
743

    
744

    
745
def UseVirtualCluster(_cfg=None):
746
  """Returns whether a virtual cluster is used.
747

748
  @rtype: bool
749

750
  """
751
  if _cfg is None:
752
    cfg = GetConfig()
753
  else:
754
    cfg = _cfg
755

    
756
  (master, _) = cfg.GetVclusterSettings()
757

    
758
  return bool(master)
759

    
760

    
761
@ht.WithDesc("No virtual cluster")
762
def NoVirtualCluster():
763
  """Used to disable tests for virtual clusters.
764

765
  """
766
  return not UseVirtualCluster()
767

    
768

    
769
def GetDiskOptions():
770
  """Wrapper for L{_QaConfig.GetDiskOptions}.
771

772
  """
773
  return GetConfig().GetDiskOptions()