Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 25e9c2ce

History | View | Annotate | Download (23.6 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
import qa_logging
36

    
37

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

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

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

    
54

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

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

66
    """
67
    self.name = name
68
    self.nicmac = nicmac
69
    self._used = None
70
    self._disk_template = None
71

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

76
    """
77
    nicmac = []
78

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

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

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

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

    
96
  def Use(self):
97
    """Marks instance as being in use.
98

99
    """
100
    assert not self._used
101
    assert self._disk_template is None
102

    
103
    self._used = True
104

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

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

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

    
116
  def GetNicMacAddr(self, idx, default):
117
    """Returns MAC address for NIC.
118

119
    @type idx: int
120
    @param idx: NIC index
121
    @param default: Default value
122

123
    """
124
    if len(self.nicmac) > idx:
125
      return self.nicmac[idx]
126
    else:
127
      return default
128

    
129
  def SetDiskTemplate(self, template):
130
    """Set the disk template.
131

132
    """
133
    assert template in constants.DISK_TEMPLATES
134

    
135
    self._disk_template = template
136

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

141
    """
142
    return self._used
143

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

148
    """
149
    return self._disk_template
150

    
151

    
152
class _QaNode(object):
153
  __slots__ = [
154
    "primary",
155
    "secondary",
156
    "_added",
157
    "_use_count",
158
    ]
159

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

163
    """
164
    self.primary = primary
165
    self.secondary = secondary
166
    self._added = False
167
    self._use_count = 0
168

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

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

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

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

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

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

    
193
    self._use_count += 1
194

    
195
    return self
196

    
197
  def Release(self):
198
    """Release a node (opposite of L{Use}).
199

200
    """
201
    assert self.use_count > 0
202

    
203
    self._use_count -= 1
204

    
205
  def MarkAdded(self):
206
    """Marks node as having been added to a cluster.
207

208
    """
209
    assert not self._added
210
    self._added = True
211

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

215
    """
216
    assert self._added
217
    self._added = False
218

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

223
    """
224
    return self._added
225

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

230
    """
231
    return self._use_count
232

    
233

    
234
_RESOURCE_CONVERTER = {
235
  "instances": _QaInstance.FromDict,
236
  "nodes": _QaNode.FromDict,
237
  }
238

    
239

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

243
  """
244
  fn = _RESOURCE_CONVERTER.get(key, None)
245
  if fn:
246
    return (key, map(fn, value))
247
  else:
248
    return (key, value)
249

    
250

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

255
    """
256
    self._data = data
257

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

    
261
  @staticmethod
262
  def LoadPatch(patch_dict, rel_path):
263
    """ Loads a single patch.
264

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

270
    """
271
    try:
272
      full_path = os.path.join(_QA_BASE_PATH, rel_path)
273
      patch = serializer.LoadJson(utils.ReadFile(full_path))
274
      patch_dict[rel_path] = patch
275
    except IOError:
276
      pass
277

    
278
  @staticmethod
279
  def LoadPatches():
280
    """ Finds and loads all patches supported by the QA.
281

282
    @rtype: dict of string to dict
283
    @return: A dictionary of relative path to patch content.
284

285
    """
286
    patches = {}
287
    _QaConfig.LoadPatch(patches, _QA_DEFAULT_PATCH)
288
    patch_dir_path = os.path.join(_QA_BASE_PATH, _QA_PATCH_DIR)
289
    if os.path.exists(patch_dir_path):
290
      for filename in os.listdir(patch_dir_path):
291
        if filename.endswith(".json"):
292
          _QaConfig.LoadPatch(patches, os.path.join(_QA_PATCH_DIR, filename))
293
    return patches
294

    
295
  @staticmethod
296
  def ApplyPatch(data, patch_module, patches, patch_path):
297
    """Applies a single patch.
298

299
    @type data: dict (deserialized json)
300
    @param data: The QA configuration
301
    @type patch_module: module
302
    @param patch_module: The json patch module, loaded dynamically
303
    @type patches: dict of string to dict
304
    @param patches: The dictionary of patch path to content
305
    @type patch_path: string
306
    @param patch_path: The path to the patch, relative to the QA directory
307

308
    @return: The modified configuration data.
309

310
    """
311
    patch_content = patches[patch_path]
312
    print qa_logging.FormatInfo("Applying patch %s" % patch_path)
313
    if not patch_content and patch_path != _QA_DEFAULT_PATCH:
314
      print qa_logging.FormatWarning("The patch %s added by the user is empty" %
315
                                     patch_path)
316
    data = patch_module.apply_patch(data, patch_content)
317

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

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

328
    @type data: dict (deserialized json)
329
    @param data: The QA configuration
330
    @type patch_module: module
331
    @param patch_module: The json patch module, loaded dynamically
332
    @type patches: dict of string to dict
333
    @param patches: The dictionary of patch path to content
334

335
    @return: The modified configuration data.
336

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

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

    
351
    # First the ordered patches
352
    for patch in ordered_patches:
353
      if patch not in patches:
354
        raise qa_error.Error("Patch %s specified in the ordering file does not "
355
                             "exist" % patch)
356
      _QaConfig.ApplyPatch(data, patch_module, patches, patch)
357

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

    
363
    # Finally the default one
364
    if _QA_DEFAULT_PATCH in patches:
365
      _QaConfig.ApplyPatch(data, patch_module, patches, _QA_DEFAULT_PATCH)
366

    
367
    return data
368

    
369
  @classmethod
370
  def Load(cls, filename):
371
    """Loads a configuration file and produces a configuration object.
372

373
    @type filename: string
374
    @param filename: Path to configuration file
375
    @rtype: L{_QaConfig}
376

377
    """
378
    data = serializer.LoadJson(utils.ReadFile(filename))
379

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

    
395
    result = cls(dict(map(_ConvertResources,
396
                          data.items()))) # pylint: disable=E1103
397
    result.Validate()
398

    
399
    return result
400

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

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

    
408
    if not self.get("nodes"):
409
      raise qa_error.Error("Need at least one node")
410

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

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

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

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

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

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

    
449
  def __getitem__(self, name):
450
    """Returns configuration value.
451

452
    @type name: string
453
    @param name: Name of configuration entry
454

455
    """
456
    return self._data[name]
457

    
458
  def __setitem__(self, key, value):
459
    """Sets a configuration value.
460

461
    """
462
    self._data[key] = value
463

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

467
    """
468
    del(self._data[key])
469

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

473
    """
474
    return len(self._data)
475

    
476
  def get(self, name, default=None):
477
    """Returns configuration value.
478

479
    @type name: string
480
    @param name: Name of configuration entry
481
    @param default: Default value
482

483
    """
484
    return self._data.get(name, default)
485

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

489
    """
490
    return self["nodes"][0]
491

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

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

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

501
    @rtype: list
502

503
    """
504
    return self._GetStringListParameter(
505
      _ENABLED_HV_KEY,
506
      [constants.DEFAULT_ENABLED_HYPERVISOR])
507

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

511
    """
512
    return self.GetEnabledHypervisors()[0]
513

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

517
    @rtype: list
518

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

    
524
  def GetEnabledStorageTypes(self):
525
    """Returns the list of enabled storage types.
526

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

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

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

544
    """
545
    return self.GetEnabledDiskTemplates()[0]
546

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

550
    @rtype: list
551

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

    
565
  def SetExclusiveStorage(self, value):
566
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
567

568
    """
569
    self._exclusive_storage = bool(value)
570

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

574
    """
575
    value = self._exclusive_storage
576
    assert value is not None
577
    return value
578

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

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

    
587
  def IsStorageTypeSupported(self, storage_type):
588
    """Is the given storage type supported by the current configuration?
589

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

593
    """
594
    enabled_disk_templates = self.GetEnabledDiskTemplates()
595
    if storage_type == constants.ST_LVM_PV:
596
      disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
597
    else:
598
      disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
599
    return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
600

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

604
    """
605
    return self.GetExclusiveStorage()
606

    
607
  def GetVclusterSettings(self):
608
    """Returns settings for virtual cluster.
609

610
    """
611
    master = self.get(_VCLUSTER_MASTER_KEY)
612
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
613

    
614
    return (master, basedir)
615

    
616
  def GetDiskOptions(self):
617
    """Return options for the disks of the instances.
618

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

622
    """
623
    try:
624
      return self._data["disks"]
625
    except KeyError:
626
      pass
627

    
628
    # Legacy interface
629
    sizes = self._data.get("disk")
630
    growths = self._data.get("disk-growth")
631
    if sizes or growths:
632
      if (sizes is None or growths is None or len(sizes) != len(growths)):
633
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
634
                             " exist and have the same number of items")
635
      disks = []
636
      for (size, growth) in zip(sizes, growths):
637
        disks.append({"size": size, "growth": growth})
638
      return disks
639
    else:
640
      return None
641

    
642

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

646
  """
647
  global _config # pylint: disable=W0603
648

    
649
  _config = _QaConfig.Load(path)
650

    
651

    
652
def GetConfig():
653
  """Returns the configuration object.
654

655
  """
656
  if _config is None:
657
    raise RuntimeError("Configuration not yet loaded")
658

    
659
  return _config
660

    
661

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

665
  """
666
  return GetConfig().get(name, default=default)
667

    
668

    
669
class Either:
670
  def __init__(self, tests):
671
    """Initializes this class.
672

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

677
    """
678
    self.tests = tests
679

    
680

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

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

687
  """
688
  if isinstance(value, (list, tuple)):
689
    return value
690
  else:
691
    return [value]
692

    
693

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

697
  @type check_fn: callable
698
  @param check_fn: Callback to check whether a test is enabled
699
  @type names: sequence or string
700
  @param names: Test name(s)
701
  @type fn: callable
702
  @param fn: Aggregation function
703
  @rtype: bool
704
  @return: Whether test is enabled
705

706
  """
707
  names = _MakeSequence(names)
708

    
709
  result = []
710

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

    
721
    result.append(value)
722

    
723
  return fn(result)
724

    
725

    
726
def TestEnabled(tests, _cfg=None):
727
  """Returns True if the given tests are enabled.
728

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

732
  """
733
  if _cfg is None:
734
    cfg = GetConfig()
735
  else:
736
    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
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
745
                           tests, compat.all)
746

    
747

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

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

    
754

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

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

    
761

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

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

    
768

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

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

    
775

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

779
  """
780
  return GetConfig().GetEnabledStorageTypes(*args)
781

    
782

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

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

    
789

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

793
  """
794
  return GetConfig().GetMasterNode()
795

    
796

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

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

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

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

    
812
  instance = instances[0]
813
  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
def IsStorageTypeSupported(storage_type):
840
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
841

842
  """
843
  return GetConfig().IsStorageTypeSupported(storage_type)
844

    
845

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

849
  """
850
  return GetConfig().AreSpindlesSupported()
851

    
852

    
853
def _NodeSortKey(node):
854
  """Returns sort key for a node.
855

856
  @type node: L{_QaNode}
857

858
  """
859
  return (node.use_count, utils.NiceSortKey(node.primary))
860

    
861

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

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

    
871
  master = cfg.GetMasterNode()
872

    
873
  # Filter out unwanted nodes
874
  # TODO: Maybe combine filters
875
  if exclude is None:
876
    nodes = cfg["nodes"][:]
877
  elif isinstance(exclude, (list, tuple)):
878
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
879
  else:
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
    raise qa_error.OutOfNodesError("No nodes left")
886

    
887
  # Return node with least number of uses
888
  return sorted(nodes, key=_NodeSortKey)[0].Use()
889

    
890

    
891
def AcquireManyNodes(num, exclude=None):
892
  """Return the least used nodes.
893

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

901
  """
902
  nodes = []
903
  if exclude is None:
904
    exclude = []
905
  elif isinstance(exclude, (list, tuple)):
906
    # Don't modify the incoming argument
907
    exclude = list(exclude)
908
  else:
909
    exclude = [exclude]
910

    
911
  try:
912
    for _ in range(0, num):
913
      n = AcquireNode(exclude=exclude)
914
      nodes.append(n)
915
      exclude.append(n)
916
  except qa_error.OutOfNodesError:
917
    ReleaseManyNodes(nodes)
918
    raise
919
  return nodes
920

    
921

    
922
def ReleaseManyNodes(nodes):
923
  for node in nodes:
924
    node.Release()
925

    
926

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

930
  """
931
  return GetConfig().GetVclusterSettings()
932

    
933

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

937
  @rtype: bool
938

939
  """
940
  if _cfg is None:
941
    cfg = GetConfig()
942
  else:
943
    cfg = _cfg
944

    
945
  (master, _) = cfg.GetVclusterSettings()
946

    
947
  return bool(master)
948

    
949

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

954
  """
955
  return not UseVirtualCluster()
956

    
957

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

961
  """
962
  return GetConfig().GetDiskOptions()