qa_config.py 23.5 KB
Newer Older
1
2
3
#
#

4
# Copyright (C) 2007, 2011, 2012, 2013 Google Inc.
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.


"""QA configuration.

"""

26
import os
27

28
from ganeti import constants
29
30
from ganeti import utils
from ganeti import serializer
Iustin Pop's avatar
Iustin Pop committed
31
from ganeti import compat
32
from ganeti import ht
33
34

import qa_error
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
35
import qa_logging
36
37


38
_INSTANCE_CHECK_KEY = "instance-check"
39
_ENABLED_HV_KEY = "enabled-hypervisors"
40
41
_VCLUSTER_MASTER_KEY = "vcluster-master"
_VCLUSTER_BASEDIR_KEY = "vcluster-basedir"
42
_ENABLED_DISK_TEMPLATES_KEY = "enabled-disk-templates"
43

Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
44
# The constants related to JSON patching (as per RFC6902) that modifies QA's
45
# configuration.
46
47
_QA_BASE_PATH = os.path.dirname(__file__)
_QA_DEFAULT_PATCH = "qa-patch.json"
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
48
_QA_PATCH_DIR = "patch"
49
_QA_PATCH_ORDER_FILE = "order"
50

51
52
#: QA configuration (L{_QaConfig})
_config = None
53
54


55
56
57
58
class _QaInstance(object):
  __slots__ = [
    "name",
    "nicmac",
59
    "_used",
60
    "_disk_template",
61
62
63
64
65
66
67
68
    ]

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

    """
    self.name = name
    self.nicmac = nicmac
69
    self._used = None
70
    self._disk_template = None
71
72
73
74
75
76
77
78
79
80
81
82
83
84

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

    """
    nicmac = []

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

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

85
86
87
88
89
90
91
92
93
94
95
  def __repr__(self):
    status = [
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
      "name=%s" % self.name,
      "nicmac=%s" % self.nicmac,
      "used=%s" % self._used,
      "disk_template=%s" % self._disk_template,
      ]

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

96
97
98
99
100
101
102
103
104
  def Use(self):
    """Marks instance as being in use.

    """
    assert not self._used
    assert self._disk_template is None

    self._used = True

105
106
107
108
  def Release(self):
    """Releases instance and makes it available again.

    """
109
    assert self._used, \
110
111
112
      ("Instance '%s' was never acquired or released more than once" %
       self.name)

113
    self._used = False
114
    self._disk_template = None
115

116
117
118
119
120
121
122
123
124
125
126
127
128
  def GetNicMacAddr(self, idx, default):
    """Returns MAC address for NIC.

    @type idx: int
    @param idx: NIC index
    @param default: Default value

    """
    if len(self.nicmac) > idx:
      return self.nicmac[idx]
    else:
      return default

129
130
131
132
133
134
135
136
  def SetDiskTemplate(self, template):
    """Set the disk template.

    """
    assert template in constants.DISK_TEMPLATES

    self._disk_template = template

137
138
139
140
141
142
143
  @property
  def used(self):
    """Returns boolean denoting whether instance is in use.

    """
    return self._used

144
145
146
147
148
149
150
  @property
  def disk_template(self):
    """Returns the current disk template.

    """
    return self._disk_template

151

152
153
154
155
156
class _QaNode(object):
  __slots__ = [
    "primary",
    "secondary",
    "_added",
157
    "_use_count",
158
159
160
161
162
163
164
165
166
    ]

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

    """
    self.primary = primary
    self.secondary = secondary
    self._added = False
167
    self._use_count = 0
168
169
170
171
172
173
174
175

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

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

176
177
178
179
180
181
182
183
184
185
186
  def __repr__(self):
    status = [
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
      "primary=%s" % self.primary,
      "secondary=%s" % self.secondary,
      "added=%s" % self._added,
      "use_count=%s" % self._use_count,
      ]

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

187
188
189
190
  def Use(self):
    """Marks a node as being in use.

    """
191
    assert self._use_count >= 0
192

193
    self._use_count += 1
194
195
196

    return self

197
198
199
200
201
202
203
204
  def Release(self):
    """Release a node (opposite of L{Use}).

    """
    assert self.use_count > 0

    self._use_count -= 1

205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
  def MarkAdded(self):
    """Marks node as having been added to a cluster.

    """
    assert not self._added
    self._added = True

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

    """
    assert self._added
    self._added = False

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

    """
    return self._added

226
227
228
229
230
231
232
  @property
  def use_count(self):
    """Returns number of current uses (controlled by L{Use} and L{Release}).

    """
    return self._use_count

233

234
235
_RESOURCE_CONVERTER = {
  "instances": _QaInstance.FromDict,
236
  "nodes": _QaNode.FromDict,
237
238
239
240
241
242
243
244
245
246
247
248
249
250
  }


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

  """
  fn = _RESOURCE_CONVERTER.get(key, None)
  if fn:
    return (key, map(fn, value))
  else:
    return (key, value)


251
252
253
class _QaConfig(object):
  def __init__(self, data):
    """Initializes instances of this class.
254

255
256
257
    """
    self._data = data

258
259
260
    #: Cluster-wide run-time value of the exclusive storage flag
    self._exclusive_storage = None

261
262
263
264
265
266
267
268
269
270
271
272
273
  @staticmethod
  def LoadPatch(patch_dict, rel_path):
    """ Loads a single patch.

    @type patch_dict: dict of string to dict
    @param patch_dict: A dictionary storing patches by relative path.
    @type rel_path: string
    @param rel_path: The relative path to the patch, might or might not exist.

    """
    try:
      full_path = os.path.join(_QA_BASE_PATH, rel_path)
      patch = serializer.LoadJson(utils.ReadFile(full_path))
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
274
      patch_dict[rel_path] = patch
275
276
277
278
279
    except IOError:
      pass

  @staticmethod
  def LoadPatches():
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
280
    """ Finds and loads all patches supported by the QA.
281
282

    @rtype: dict of string to dict
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
283
    @return: A dictionary of relative path to patch content.
284
285
286
287

    """
    patches = {}
    _QaConfig.LoadPatch(patches, _QA_DEFAULT_PATCH)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
288
289
290
291
292
    patch_dir_path = os.path.join(_QA_BASE_PATH, _QA_PATCH_DIR)
    if os.path.exists(patch_dir_path):
      for filename in os.listdir(patch_dir_path):
        if filename.endswith(".json"):
          _QaConfig.LoadPatch(patches, os.path.join(_QA_PATCH_DIR, filename))
293
294
    return patches

Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
295
296
297
298
299
  @staticmethod
  def ApplyPatch(data, patch_module, patches, patch_path):
    """Applies a single patch.

    @type data: dict (deserialized json)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
300
    @param data: The QA configuration to modify
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
301
302
303
304
305
306
307
308
309
310
311
312
313
    @type patch_module: module
    @param patch_module: The json patch module, loaded dynamically
    @type patches: dict of string to dict
    @param patches: The dictionary of patch path to content
    @type patch_path: string
    @param patch_path: The path to the patch, relative to the QA directory

    """
    patch_content = patches[patch_path]
    print qa_logging.FormatInfo("Applying patch %s" % patch_path)
    if not patch_content and patch_path != _QA_DEFAULT_PATCH:
      print qa_logging.FormatWarning("The patch %s added by the user is empty" %
                                     patch_path)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
314
    patch_module.apply_patch(data, patch_content, in_place=True)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
315

Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
316
317
318
319
  @staticmethod
  def ApplyPatches(data, patch_module, patches):
    """Applies any patches present, and returns the modified QA configuration.

320
321
322
323
324
    First, patches from the patch directory are applied. They are ordered
    alphabetically, unless there is an ``order`` file present - any patches
    listed within are applied in that order, and any remaining ones in
    alphabetical order again. Finally, the default patch residing in the
    top-level QA directory is applied.
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
325
326

    @type data: dict (deserialized json)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
327
    @param data: The QA configuration to modify
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
328
329
330
331
332
333
    @type patch_module: module
    @param patch_module: The json patch module, loaded dynamically
    @type patches: dict of string to dict
    @param patches: The dictionary of patch path to content

    """
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
    ordered_patches = []
    order_path = os.path.join(_QA_BASE_PATH, _QA_PATCH_DIR,
                              _QA_PATCH_ORDER_FILE)
    if os.path.exists(order_path):
      order_file = open(order_path, 'r')
      ordered_patches = order_file.read().splitlines()
      # Removes empty lines
      ordered_patches = filter(None, ordered_patches)

    # Add the patch dir
    ordered_patches = map(lambda x: os.path.join(_QA_PATCH_DIR, x),
                          ordered_patches)

    # First the ordered patches
    for patch in ordered_patches:
      if patch not in patches:
        raise qa_error.Error("Patch %s specified in the ordering file does not "
                             "exist" % patch)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
352
      _QaConfig.ApplyPatch(data, patch_module, patches, patch)
353
354

    # Then the other non-default ones
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
355
    for patch in sorted(patches):
356
      if patch != _QA_DEFAULT_PATCH and patch not in ordered_patches:
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
357
        _QaConfig.ApplyPatch(data, patch_module, patches, patch)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
358

359
    # Finally the default one
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
360
    if _QA_DEFAULT_PATCH in patches:
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
361
      _QaConfig.ApplyPatch(data, patch_module, patches, _QA_DEFAULT_PATCH)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
362

363
364
365
366
367
368
369
370
371
372
373
  @classmethod
  def Load(cls, filename):
    """Loads a configuration file and produces a configuration object.

    @type filename: string
    @param filename: Path to configuration file
    @rtype: L{_QaConfig}

    """
    data = serializer.LoadJson(utils.ReadFile(filename))

374
375
376
    # Patch the document using JSON Patch (RFC6902) in file _PATCH_JSON, if
    # available
    try:
377
      patches = _QaConfig.LoadPatches()
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
378
379
      # Try to use the module only if there is a non-empty patch present
      if any(patches.values()):
380
        mod = __import__("jsonpatch", fromlist=[])
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
381
        _QaConfig.ApplyPatches(data, mod, patches)
382
383
384
    except IOError:
      pass
    except ImportError:
385
386
387
      raise qa_error.Error("For the QA JSON patching feature to work, you "
                           "need to install Python modules 'jsonpatch' and "
                           "'jsonpointer'.")
388

389
390
    result = cls(dict(map(_ConvertResources,
                          data.items()))) # pylint: disable=E1103
391
392
393
394
395
396
397
398
    result.Validate()

    return result

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

    """
399
400
401
    if not self.get("name"):
      raise qa_error.Error("Cluster name is required")

402
403
404
405
406
407
    if not self.get("nodes"):
      raise qa_error.Error("Need at least one node")

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

408
409
410
411
412
413
414
415
    disks = self.GetDiskOptions()
    if disks is None:
      raise qa_error.Error("Config option 'disks' must exist")
    else:
      for d in disks:
        if d.get("size") is None or d.get("growth") is None:
          raise qa_error.Error("Config options `size` and `growth` must exist"
                               " for all `disks` items")
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
    check = self.GetInstanceCheckScript()
    if check:
      try:
        os.stat(check)
      except EnvironmentError, err:
        raise qa_error.Error("Can't find instance check script '%s': %s" %
                             (check, err))

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

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

433
434
435
436
437
438
439
440
441
442
    (vc_master, vc_basedir) = self.GetVclusterSettings()
    if bool(vc_master) != bool(vc_basedir):
      raise qa_error.Error("All or none of the config options '%s' and '%s'"
                           " must be set" %
                           (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))

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

443
444
445
446
447
448
449
450
451
  def __getitem__(self, name):
    """Returns configuration value.

    @type name: string
    @param name: Name of configuration entry

    """
    return self._data[name]

452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
  def __setitem__(self, key, value):
    """Sets a configuration value.

    """
    self._data[key] = value

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

    """
    del(self._data[key])

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

    """
    return len(self._data)

470
471
472
473
474
475
476
477
478
  def get(self, name, default=None):
    """Returns configuration value.

    @type name: string
    @param name: Name of configuration entry
    @param default: Default value

    """
    return self._data.get(name, default)
479

480
481
  def GetMasterNode(self):
    """Returns the default master node for the cluster.
482

483
484
485
486
487
488
489
490
    """
    return self["nodes"][0]

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

    """
    return self._data.get(_INSTANCE_CHECK_KEY, None)
491

492
493
  def GetEnabledHypervisors(self):
    """Returns list of enabled hypervisors.
494

495
    @rtype: list
496

497
498
499
500
501
502
503
504
505
506
507
    """
    return self._GetStringListParameter(
      _ENABLED_HV_KEY,
      [constants.DEFAULT_ENABLED_HYPERVISOR])

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

    """
    return self.GetEnabledHypervisors()[0]

508
509
  def GetEnabledDiskTemplates(self):
    """Returns the list of enabled disk templates.
510
511
512
513
514

    @rtype: list

    """
    return self._GetStringListParameter(
515
      _ENABLED_DISK_TEMPLATES_KEY,
516
      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
517

518
519
520
521
522
523
524
525
  def GetEnabledStorageTypes(self):
    """Returns the list of enabled storage types.

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

    """
    enabled_disk_templates = self.GetEnabledDiskTemplates()
526
527
528
    enabled_storage_types = list(
        set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
             for dt in enabled_disk_templates]))
529
530
531
532
533
534
    # Storage type 'lvm-pv' cannot be activated via a disk template,
    # therefore we add it if 'lvm-vg' is present.
    if constants.ST_LVM_VG in enabled_storage_types:
      enabled_storage_types.append(constants.ST_LVM_PV)
    return enabled_storage_types

535
536
  def GetDefaultDiskTemplate(self):
    """Returns the default disk template to be used.
537
538

    """
539
    return self.GetEnabledDiskTemplates()[0]
540
541
542
543
544
545

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

    @rtype: list

546
    """
547
    try:
548
      value = self._data[key]
549
    except KeyError:
550
      return default_values
551
552
553
554
555
556
557
558
    else:
      if value is None:
        return []
      elif isinstance(value, basestring):
        return value.split(",")
      else:
        return value

559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
  def SetExclusiveStorage(self, value):
    """Set the expected value of the C{exclusive_storage} flag for the cluster.

    """
    self._exclusive_storage = bool(value)

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

    """
    value = self._exclusive_storage
    assert value is not None
    return value

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

    """
577
578
579
    enabled = templ in self.GetEnabledDiskTemplates()
    return enabled and (not self.GetExclusiveStorage() or
                        templ in constants.DTS_EXCL_STORAGE)
580

581
582
583
584
585
586
587
588
589
590
591
592
593
594
  def IsStorageTypeSupported(self, storage_type):
    """Is the given storage type supported by the current configuration?

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

    """
    enabled_disk_templates = self.GetEnabledDiskTemplates()
    if storage_type == constants.ST_LVM_PV:
      disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
    else:
      disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
    return bool(set(enabled_disk_templates).intersection(set(disk_templates)))

595
596
597
598
599
600
  def AreSpindlesSupported(self):
    """Are spindles supported by the current configuration?

    """
    return self.GetExclusiveStorage()

601
602
603
604
605
606
607
608
609
  def GetVclusterSettings(self):
    """Returns settings for virtual cluster.

    """
    master = self.get(_VCLUSTER_MASTER_KEY)
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)

    return (master, basedir)

610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
  def GetDiskOptions(self):
    """Return options for the disks of the instances.

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

    """
    try:
      return self._data["disks"]
    except KeyError:
      pass

    # Legacy interface
    sizes = self._data.get("disk")
    growths = self._data.get("disk-growth")
    if sizes or growths:
      if (sizes is None or growths is None or len(sizes) != len(growths)):
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
                             " exist and have the same number of items")
      disks = []
      for (size, growth) in zip(sizes, growths):
        disks.append({"size": size, "growth": growth})
      return disks
    else:
      return None

636
637
638
639
640
641
642
643

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

  """
  global _config # pylint: disable=W0603

  _config = _QaConfig.Load(path)
644

645

646
647
648
649
650
651
652
653
def GetConfig():
  """Returns the configuration object.

  """
  if _config is None:
    raise RuntimeError("Configuration not yet loaded")

  return _config
654

655
656

def get(name, default=None):
657
658
659
660
  """Wrapper for L{_QaConfig.get}.

  """
  return GetConfig().get(name, default=default)
661
662


663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
class Either:
  def __init__(self, tests):
    """Initializes this class.

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

    """
    self.tests = tests


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

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

  """
  if isinstance(value, (list, tuple)):
    return value
  else:
    return [value]


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

  @type check_fn: callable
  @param check_fn: Callback to check whether a test is enabled
  @type names: sequence or string
  @param names: Test name(s)
  @type fn: callable
  @param fn: Aggregation function
  @rtype: bool
  @return: Whether test is enabled

  """
  names = _MakeSequence(names)

  result = []

  for name in names:
    if isinstance(name, Either):
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
    elif isinstance(name, (list, tuple)):
      value = _TestEnabledInner(check_fn, name, compat.all)
710
711
    elif callable(name):
      value = name()
712
713
714
715
716
717
718
719
720
    else:
      value = check_fn(name)

    result.append(value)

  return fn(result)


def TestEnabled(tests, _cfg=None):
Iustin Pop's avatar
Iustin Pop committed
721
722
  """Returns True if the given tests are enabled.

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

  """
727
  if _cfg is None:
728
729
730
    cfg = GetConfig()
  else:
    cfg = _cfg
731
732

  # Get settings for all tests
733
  cfg_tests = cfg.get("tests", {})
734
735

  # Get default setting
736
  default = cfg_tests.get("default", True)
737

738
739
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
                           tests, compat.all)
740
741


742
743
def GetInstanceCheckScript(*args):
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
744
745

  """
746
  return GetConfig().GetInstanceCheckScript(*args)
747

748

749
750
def GetEnabledHypervisors(*args):
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
751
752

  """
753
  return GetConfig().GetEnabledHypervisors(*args)
754
755


756
757
def GetDefaultHypervisor(*args):
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
758
759

  """
760
  return GetConfig().GetDefaultHypervisor(*args)
761
762


763
764
def GetEnabledDiskTemplates(*args):
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
765
766

  """
767
  return GetConfig().GetEnabledDiskTemplates(*args)
768
769


770
771
772
773
774
775
776
def GetEnabledStorageTypes(*args):
  """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.

  """
  return GetConfig().GetEnabledStorageTypes(*args)


777
778
def GetDefaultDiskTemplate(*args):
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
779
780

  """
781
  return GetConfig().GetDefaultDiskTemplate(*args)
782
783


784
def GetMasterNode():
785
786
787
788
  """Wrapper for L{_QaConfig.GetMasterNode}.

  """
  return GetConfig().GetMasterNode()
789
790


791
def AcquireInstance(_cfg=None):
792
793
794
  """Returns an instance which isn't in use.

  """
795
796
797
798
799
  if _cfg is None:
    cfg = GetConfig()
  else:
    cfg = _cfg

800
  # Filter out unwanted instances
801
  instances = filter(lambda inst: not inst.used, cfg["instances"])
802

803
  if not instances:
804
805
    raise qa_error.OutOfInstancesError("No instances left")

806
807
  instance = instances[0]
  instance.Use()
808

809
  return instance
810
811


812
def SetExclusiveStorage(value):
813
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
814
815

  """
816
  return GetConfig().SetExclusiveStorage(value)
817
818
819


def GetExclusiveStorage():
820
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
821
822

  """
823
  return GetConfig().GetExclusiveStorage()
824
825


826
def IsTemplateSupported(templ):
827
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
828
829

  """
830
  return GetConfig().IsTemplateSupported(templ)
831
832


833
834
835
836
837
838
839
def IsStorageTypeSupported(storage_type):
  """Wrapper for L{_QaConfig.IsTemplateSupported}.

  """
  return GetConfig().IsStorageTypeSupported(storage_type)


840
841
842
843
844
845
846
def AreSpindlesSupported():
  """Wrapper for L{_QaConfig.AreSpindlesSupported}.

  """
  return GetConfig().AreSpindlesSupported()


847
848
849
850
851
852
853
854
855
def _NodeSortKey(node):
  """Returns sort key for a node.

  @type node: L{_QaNode}

  """
  return (node.use_count, utils.NiceSortKey(node.primary))


856
def AcquireNode(exclude=None, _cfg=None):
857
858
859
  """Returns the least used node.

  """
860
861
862
863
864
865
  if _cfg is None:
    cfg = GetConfig()
  else:
    cfg = _cfg

  master = cfg.GetMasterNode()
866
867
868
869

  # Filter out unwanted nodes
  # TODO: Maybe combine filters
  if exclude is None:
Iustin Pop's avatar
Iustin Pop committed
870
    nodes = cfg["nodes"][:]
871
  elif isinstance(exclude, (list, tuple)):
Iustin Pop's avatar
Iustin Pop committed
872
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
873
  else:
Iustin Pop's avatar
Iustin Pop committed
874
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
875

876
  nodes = filter(lambda node: node.added or node == master, nodes)
877

878
  if not nodes:
879
880
    raise qa_error.OutOfNodesError("No nodes left")

881
882
  # Return node with least number of uses
  return sorted(nodes, key=_NodeSortKey)[0].Use()
883
884


885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
def AcquireManyNodes(num, exclude=None):
  """Return the least used nodes.

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

  """
  nodes = []
  if exclude is None:
    exclude = []
  elif isinstance(exclude, (list, tuple)):
    # Don't modify the incoming argument
    exclude = list(exclude)
  else:
    exclude = [exclude]

  try:
    for _ in range(0, num):
      n = AcquireNode(exclude=exclude)
      nodes.append(n)
      exclude.append(n)
  except qa_error.OutOfNodesError:
    ReleaseManyNodes(nodes)
    raise
  return nodes


def ReleaseManyNodes(nodes):
917
918
  for node in nodes:
    node.Release()
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949


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

  """
  return GetConfig().GetVclusterSettings()


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

  @rtype: bool

  """
  if _cfg is None:
    cfg = GetConfig()
  else:
    cfg = _cfg

  (master, _) = cfg.GetVclusterSettings()

  return bool(master)


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

  """
  return not UseVirtualCluster()
950
951
952
953
954
955
956


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

  """
  return GetConfig().GetDiskOptions()