Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ e5398c3a

History | View | Annotate | Download (20.1 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 __setitem__(self, key, value):
346
    """Sets a configuration value.
347

348
    """
349
    self._data[key] = value
350

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

354
    """
355
    del(self._data[key])
356

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

360
    """
361
    return len(self._data)
362

    
363
  def get(self, name, default=None):
364
    """Returns configuration value.
365

366
    @type name: string
367
    @param name: Name of configuration entry
368
    @param default: Default value
369

370
    """
371
    return self._data.get(name, default)
372

    
373
  def GetMasterNode(self):
374
    """Returns the default master node for the cluster.
375

376
    """
377
    return self["nodes"][0]
378

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

382
    """
383
    return self._data.get(_INSTANCE_CHECK_KEY, None)
384

    
385
  def GetEnabledHypervisors(self):
386
    """Returns list of enabled hypervisors.
387

388
    @rtype: list
389

390
    """
391
    return self._GetStringListParameter(
392
      _ENABLED_HV_KEY,
393
      [constants.DEFAULT_ENABLED_HYPERVISOR])
394

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

398
    """
399
    return self.GetEnabledHypervisors()[0]
400

    
401
  def GetEnabledDiskTemplates(self):
402
    """Returns the list of enabled disk templates.
403

404
    @rtype: list
405

406
    """
407
    return self._GetStringListParameter(
408
      _ENABLED_DISK_TEMPLATES_KEY,
409
      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
410

    
411
  def GetEnabledStorageTypes(self):
412
    """Returns the list of enabled storage types.
413

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

417
    """
418
    enabled_disk_templates = self.GetEnabledDiskTemplates()
419
    enabled_storage_types = list(
420
        set([constants.MAP_DISK_TEMPLATE_STORAGE_TYPE[dt]
421
             for dt in enabled_disk_templates]))
422
    # Storage type 'lvm-pv' cannot be activated via a disk template,
423
    # therefore we add it if 'lvm-vg' is present.
424
    if constants.ST_LVM_VG in enabled_storage_types:
425
      enabled_storage_types.append(constants.ST_LVM_PV)
426
    return enabled_storage_types
427

    
428
  def GetDefaultDiskTemplate(self):
429
    """Returns the default disk template to be used.
430

431
    """
432
    return self.GetEnabledDiskTemplates()[0]
433

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

437
    @rtype: list
438

439
    """
440
    try:
441
      value = self._data[key]
442
    except KeyError:
443
      return default_values
444
    else:
445
      if value is None:
446
        return []
447
      elif isinstance(value, basestring):
448
        return value.split(",")
449
      else:
450
        return value
451

    
452
  def SetExclusiveStorage(self, value):
453
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
454

455
    """
456
    self._exclusive_storage = bool(value)
457

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

461
    """
462
    value = self._exclusive_storage
463
    assert value is not None
464
    return value
465

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

469
    """
470
    enabled = templ in self.GetEnabledDiskTemplates()
471
    return enabled and (not self.GetExclusiveStorage() or
472
                        templ in constants.DTS_EXCL_STORAGE)
473

    
474
  def IsStorageTypeSupported(self, storage_type):
475
    """Is the given storage type supported by the current configuration?
476

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

480
    """
481
    enabled_disk_templates = self.GetEnabledDiskTemplates()
482
    if storage_type == constants.ST_LVM_PV:
483
      disk_templates = utils.GetDiskTemplatesOfStorageType(constants.ST_LVM_VG)
484
    else:
485
      disk_templates = utils.GetDiskTemplatesOfStorageType(storage_type)
486
    return bool(set(enabled_disk_templates).intersection(set(disk_templates)))
487

    
488
  def AreSpindlesSupported(self):
489
    """Are spindles supported by the current configuration?
490

491
    """
492
    return self.GetExclusiveStorage()
493

    
494
  def GetVclusterSettings(self):
495
    """Returns settings for virtual cluster.
496

497
    """
498
    master = self.get(_VCLUSTER_MASTER_KEY)
499
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
500

    
501
    return (master, basedir)
502

    
503
  def GetDiskOptions(self):
504
    """Return options for the disks of the instances.
505

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

509
    """
510
    try:
511
      return self._data["disks"]
512
    except KeyError:
513
      pass
514

    
515
    # Legacy interface
516
    sizes = self._data.get("disk")
517
    growths = self._data.get("disk-growth")
518
    if sizes or growths:
519
      if (sizes is None or growths is None or len(sizes) != len(growths)):
520
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
521
                             " exist and have the same number of items")
522
      disks = []
523
      for (size, growth) in zip(sizes, growths):
524
        disks.append({"size": size, "growth": growth})
525
      return disks
526
    else:
527
      return None
528

    
529

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

533
  """
534
  global _config # pylint: disable=W0603
535

    
536
  _config = _QaConfig.Load(path)
537

    
538

    
539
def GetConfig():
540
  """Returns the configuration object.
541

542
  """
543
  if _config is None:
544
    raise RuntimeError("Configuration not yet loaded")
545

    
546
  return _config
547

    
548

    
549
def get(name, default=None):
550
  """Wrapper for L{_QaConfig.get}.
551

552
  """
553
  return GetConfig().get(name, default=default)
554

    
555

    
556
class Either:
557
  def __init__(self, tests):
558
    """Initializes this class.
559

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

564
    """
565
    self.tests = tests
566

    
567

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

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

574
  """
575
  if isinstance(value, (list, tuple)):
576
    return value
577
  else:
578
    return [value]
579

    
580

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

584
  @type check_fn: callable
585
  @param check_fn: Callback to check whether a test is enabled
586
  @type names: sequence or string
587
  @param names: Test name(s)
588
  @type fn: callable
589
  @param fn: Aggregation function
590
  @rtype: bool
591
  @return: Whether test is enabled
592

593
  """
594
  names = _MakeSequence(names)
595

    
596
  result = []
597

    
598
  for name in names:
599
    if isinstance(name, Either):
600
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
601
    elif isinstance(name, (list, tuple)):
602
      value = _TestEnabledInner(check_fn, name, compat.all)
603
    elif callable(name):
604
      value = name()
605
    else:
606
      value = check_fn(name)
607

    
608
    result.append(value)
609

    
610
  return fn(result)
611

    
612

    
613
def TestEnabled(tests, _cfg=None):
614
  """Returns True if the given tests are enabled.
615

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

619
  """
620
  if _cfg is None:
621
    cfg = GetConfig()
622
  else:
623
    cfg = _cfg
624

    
625
  # Get settings for all tests
626
  cfg_tests = cfg.get("tests", {})
627

    
628
  # Get default setting
629
  default = cfg_tests.get("default", True)
630

    
631
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
632
                           tests, compat.all)
633

    
634

    
635
def GetInstanceCheckScript(*args):
636
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
637

638
  """
639
  return GetConfig().GetInstanceCheckScript(*args)
640

    
641

    
642
def GetEnabledHypervisors(*args):
643
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
644

645
  """
646
  return GetConfig().GetEnabledHypervisors(*args)
647

    
648

    
649
def GetDefaultHypervisor(*args):
650
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
651

652
  """
653
  return GetConfig().GetDefaultHypervisor(*args)
654

    
655

    
656
def GetEnabledDiskTemplates(*args):
657
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
658

659
  """
660
  return GetConfig().GetEnabledDiskTemplates(*args)
661

    
662

    
663
def GetEnabledStorageTypes(*args):
664
  """Wrapper for L{_QaConfig.GetEnabledStorageTypes}.
665

666
  """
667
  return GetConfig().GetEnabledStorageTypes(*args)
668

    
669

    
670
def GetDefaultDiskTemplate(*args):
671
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
672

673
  """
674
  return GetConfig().GetDefaultDiskTemplate(*args)
675

    
676

    
677
def GetMasterNode():
678
  """Wrapper for L{_QaConfig.GetMasterNode}.
679

680
  """
681
  return GetConfig().GetMasterNode()
682

    
683

    
684
def AcquireInstance(_cfg=None):
685
  """Returns an instance which isn't in use.
686

687
  """
688
  if _cfg is None:
689
    cfg = GetConfig()
690
  else:
691
    cfg = _cfg
692

    
693
  # Filter out unwanted instances
694
  instances = filter(lambda inst: not inst.used, cfg["instances"])
695

    
696
  if not instances:
697
    raise qa_error.OutOfInstancesError("No instances left")
698

    
699
  instance = instances[0]
700
  instance.Use()
701

    
702
  return instance
703

    
704

    
705
def SetExclusiveStorage(value):
706
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
707

708
  """
709
  return GetConfig().SetExclusiveStorage(value)
710

    
711

    
712
def GetExclusiveStorage():
713
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
714

715
  """
716
  return GetConfig().GetExclusiveStorage()
717

    
718

    
719
def IsTemplateSupported(templ):
720
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
721

722
  """
723
  return GetConfig().IsTemplateSupported(templ)
724

    
725

    
726
def IsStorageTypeSupported(storage_type):
727
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
728

729
  """
730
  return GetConfig().IsStorageTypeSupported(storage_type)
731

    
732

    
733
def AreSpindlesSupported():
734
  """Wrapper for L{_QaConfig.AreSpindlesSupported}.
735

736
  """
737
  return GetConfig().AreSpindlesSupported()
738

    
739

    
740
def _NodeSortKey(node):
741
  """Returns sort key for a node.
742

743
  @type node: L{_QaNode}
744

745
  """
746
  return (node.use_count, utils.NiceSortKey(node.primary))
747

    
748

    
749
def AcquireNode(exclude=None, _cfg=None):
750
  """Returns the least used node.
751

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

    
758
  master = cfg.GetMasterNode()
759

    
760
  # Filter out unwanted nodes
761
  # TODO: Maybe combine filters
762
  if exclude is None:
763
    nodes = cfg["nodes"][:]
764
  elif isinstance(exclude, (list, tuple)):
765
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
766
  else:
767
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
768

    
769
  nodes = filter(lambda node: node.added or node == master, nodes)
770

    
771
  if not nodes:
772
    raise qa_error.OutOfNodesError("No nodes left")
773

    
774
  # Return node with least number of uses
775
  return sorted(nodes, key=_NodeSortKey)[0].Use()
776

    
777

    
778
class AcquireManyNodesCtx(object):
779
  """Returns the least used nodes for use with a `with` block
780

781
  """
782
  def __init__(self, num, exclude=None, cfg=None):
783
    self._num = num
784
    self._exclude = exclude
785
    self._cfg = cfg
786

    
787
  def __enter__(self):
788
    self._nodes = AcquireManyNodes(self._num, exclude=self._exclude,
789
                                   cfg=self._cfg)
790
    return self._nodes
791

    
792
  def __exit__(self, exc_type, exc_value, exc_tb):
793
    ReleaseManyNodes(self._nodes)
794

    
795

    
796
def AcquireManyNodes(num, exclude=None, cfg=None):
797
  """Return the least used nodes.
798

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

806
  """
807
  nodes = []
808
  if exclude is None:
809
    exclude = []
810
  elif isinstance(exclude, (list, tuple)):
811
    # Don't modify the incoming argument
812
    exclude = list(exclude)
813
  else:
814
    exclude = [exclude]
815

    
816
  try:
817
    for _ in range(0, num):
818
      n = AcquireNode(exclude=exclude, _cfg=cfg)
819
      nodes.append(n)
820
      exclude.append(n)
821
  except qa_error.OutOfNodesError:
822
    ReleaseManyNodes(nodes)
823
    raise
824
  return nodes
825

    
826

    
827
def ReleaseManyNodes(nodes):
828
  for node in nodes:
829
    node.Release()
830

    
831

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

835
  """
836
  return GetConfig().GetVclusterSettings()
837

    
838

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

842
  @rtype: bool
843

844
  """
845
  if _cfg is None:
846
    cfg = GetConfig()
847
  else:
848
    cfg = _cfg
849

    
850
  (master, _) = cfg.GetVclusterSettings()
851

    
852
  return bool(master)
853

    
854

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

859
  """
860
  return not UseVirtualCluster()
861

    
862

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

866
  """
867
  return GetConfig().GetDiskOptions()