qa_config.py 23.6 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
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
  @staticmethod
  def ApplyPatch(data, patch_module, patches, patch_path):
    """Applies a single patch.

    @type data: dict (deserialized json)
    @param data: The QA configuration
    @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

    @return: The modified configuration data.

    """
    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)
    data = patch_module.apply_patch(data, patch_content)

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

322
323
324
325
326
    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
327
328
329
330
331
332
333
334
335
336
337

    @type data: dict (deserialized json)
    @param data: The QA configuration
    @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

    @return: The modified configuration data.

    """
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
    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
356
      _QaConfig.ApplyPatch(data, patch_module, patches, patch)
357
358

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

363
    # Finally the default one
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
364
    if _QA_DEFAULT_PATCH in patches:
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
365
      _QaConfig.ApplyPatch(data, patch_module, patches, _QA_DEFAULT_PATCH)
Hrvoje Ribicic's avatar
Hrvoje Ribicic committed
366
367
368

    return data

369
370
371
372
373
374
375
376
377
378
379
  @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))

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

395
396
    result = cls(dict(map(_ConvertResources,
                          data.items()))) # pylint: disable=E1103
397
398
399
400
401
402
403
404
    result.Validate()

    return result

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

    """
405
406
407
    if not self.get("name"):
      raise qa_error.Error("Cluster name is required")

408
409
410
411
412
413
    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")

414
415
416
417
418
419
420
421
    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")
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
    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))

439
440
441
442
443
444
445
446
447
448
    (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)

449
450
451
452
453
454
455
456
457
  def __getitem__(self, name):
    """Returns configuration value.

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

    """
    return self._data[name]

458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
  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)

476
477
478
479
480
481
482
483
484
  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)
485

486
487
  def GetMasterNode(self):
    """Returns the default master node for the cluster.
488

489
490
491
492
493
494
495
496
    """
    return self["nodes"][0]

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

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

498
499
  def GetEnabledHypervisors(self):
    """Returns list of enabled hypervisors.
500

501
    @rtype: list
502

503
504
505
506
507
508
509
510
511
512
513
    """
    return self._GetStringListParameter(
      _ENABLED_HV_KEY,
      [constants.DEFAULT_ENABLED_HYPERVISOR])

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

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

514
515
  def GetEnabledDiskTemplates(self):
    """Returns the list of enabled disk templates.
516
517
518
519
520

    @rtype: list

    """
    return self._GetStringListParameter(
521
      _ENABLED_DISK_TEMPLATES_KEY,
522
      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
523

524
525
526
527
528
529
530
531
  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()
532
533
534
    enabled_storage_types = list(
        set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
             for dt in enabled_disk_templates]))
535
536
537
538
539
540
    # 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

541
542
  def GetDefaultDiskTemplate(self):
    """Returns the default disk template to be used.
543
544

    """
545
    return self.GetEnabledDiskTemplates()[0]
546
547
548
549
550
551

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

    @rtype: list

552
    """
553
    try:
554
      value = self._data[key]
555
    except KeyError:
556
      return default_values
557
558
559
560
561
562
563
564
    else:
      if value is None:
        return []
      elif isinstance(value, basestring):
        return value.split(",")
      else:
        return value

565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
  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?

    """
583
584
585
    enabled = templ in self.GetEnabledDiskTemplates()
    return enabled and (not self.GetExclusiveStorage() or
                        templ in constants.DTS_EXCL_STORAGE)
586

587
588
589
590
591
592
593
594
595
596
597
598
599
600
  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)))

601
602
603
604
605
606
  def AreSpindlesSupported(self):
    """Are spindles supported by the current configuration?

    """
    return self.GetExclusiveStorage()

607
608
609
610
611
612
613
614
615
  def GetVclusterSettings(self):
    """Returns settings for virtual cluster.

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

    return (master, basedir)

616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
  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

642
643
644
645
646
647
648
649

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

  """
  global _config # pylint: disable=W0603

  _config = _QaConfig.Load(path)
650

651

652
653
654
655
656
657
658
659
def GetConfig():
  """Returns the configuration object.

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

  return _config
660

661
662

def get(name, default=None):
663
664
665
666
  """Wrapper for L{_QaConfig.get}.

  """
  return GetConfig().get(name, default=default)
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
710
711
712
713
714
715
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)
716
717
    elif callable(name):
      value = name()
718
719
720
721
722
723
724
725
726
    else:
      value = check_fn(name)

    result.append(value)

  return fn(result)


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

729
730
  @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
731
732

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

  # Get settings for all tests
739
  cfg_tests = cfg.get("tests", {})
740
741

  # Get default setting
742
  default = cfg_tests.get("default", True)
743

744
745
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
                           tests, compat.all)
746
747


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

  """
752
  return GetConfig().GetInstanceCheckScript(*args)
753

754

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

  """
759
  return GetConfig().GetEnabledHypervisors(*args)
760
761


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

  """
766
  return GetConfig().GetDefaultHypervisor(*args)
767
768


769
770
def GetEnabledDiskTemplates(*args):
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
771
772

  """
773
  return GetConfig().GetEnabledDiskTemplates(*args)
774
775


776
777
778
779
780
781
782
def GetEnabledStorageTypes(*args):
  """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.

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


783
784
def GetDefaultDiskTemplate(*args):
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
785
786

  """
787
  return GetConfig().GetDefaultDiskTemplate(*args)
788
789


790
def GetMasterNode():
791
792
793
794
  """Wrapper for L{_QaConfig.GetMasterNode}.

  """
  return GetConfig().GetMasterNode()
795
796


797
def AcquireInstance(_cfg=None):
798
799
800
  """Returns an instance which isn't in use.

  """
801
802
803
804
805
  if _cfg is None:
    cfg = GetConfig()
  else:
    cfg = _cfg

806
  # Filter out unwanted instances
807
  instances = filter(lambda inst: not inst.used, cfg["instances"])
808

809
  if not instances:
810
811
    raise qa_error.OutOfInstancesError("No instances left")

812
813
  instance = instances[0]
  instance.Use()
814

815
  return instance
816
817


818
def SetExclusiveStorage(value):
819
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
820
821

  """
822
  return GetConfig().SetExclusiveStorage(value)
823
824
825


def GetExclusiveStorage():
826
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
827
828

  """
829
  return GetConfig().GetExclusiveStorage()
830
831


832
def IsTemplateSupported(templ):
833
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
834
835

  """
836
  return GetConfig().IsTemplateSupported(templ)
837
838


839
840
841
842
843
844
845
def IsStorageTypeSupported(storage_type):
  """Wrapper for L{_QaConfig.IsTemplateSupported}.

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


846
847
848
849
850
851
852
def AreSpindlesSupported():
  """Wrapper for L{_QaConfig.AreSpindlesSupported}.

  """
  return GetConfig().AreSpindlesSupported()


853
854
855
856
857
858
859
860
861
def _NodeSortKey(node):
  """Returns sort key for a node.

  @type node: L{_QaNode}

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


862
def AcquireNode(exclude=None, _cfg=None):
863
864
865
  """Returns the least used node.

  """
866
867
868
869
870
871
  if _cfg is None:
    cfg = GetConfig()
  else:
    cfg = _cfg

  master = cfg.GetMasterNode()
872
873
874
875

  # Filter out unwanted nodes
  # TODO: Maybe combine filters
  if exclude is None:
Iustin Pop's avatar
Iustin Pop committed
876
    nodes = cfg["nodes"][:]
877
  elif isinstance(exclude, (list, tuple)):
Iustin Pop's avatar
Iustin Pop committed
878
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
879
  else:
Iustin Pop's avatar
Iustin Pop committed
880
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
881

882
  nodes = filter(lambda node: node.added or node == master, nodes)
883

884
  if not nodes:
885
886
    raise qa_error.OutOfNodesError("No nodes left")

887
888
  # Return node with least number of uses
  return sorted(nodes, key=_NodeSortKey)[0].Use()
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
917
918
919
920
921
922
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):
923
924
  for node in nodes:
    node.Release()
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
950
951
952
953
954
955


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()
956
957
958
959
960
961
962


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

  """
  return GetConfig().GetDiskOptions()