Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 345d395d

History | View | Annotate | Download (17.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
#: QA configuration (L{_QaConfig})
44
_config = None
45

    
46

    
47
class _QaInstance(object):
48
  __slots__ = [
49
    "name",
50
    "nicmac",
51
    "_used",
52
    "_disk_template",
53
    ]
54

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

58
    """
59
    self.name = name
60
    self.nicmac = nicmac
61
    self._used = None
62
    self._disk_template = None
63

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

68
    """
69
    nicmac = []
70

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

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

    
77
  def __repr__(self):
78
    status = [
79
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
80
      "name=%s" % self.name,
81
      "nicmac=%s" % self.nicmac,
82
      "used=%s" % self._used,
83
      "disk_template=%s" % self._disk_template,
84
      ]
85

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

    
88
  def Use(self):
89
    """Marks instance as being in use.
90

91
    """
92
    assert not self._used
93
    assert self._disk_template is None
94

    
95
    self._used = True
96

    
97
  def Release(self):
98
    """Releases instance and makes it available again.
99

100
    """
101
    assert self._used, \
102
      ("Instance '%s' was never acquired or released more than once" %
103
       self.name)
104

    
105
    self._used = False
106
    self._disk_template = None
107

    
108
  def GetNicMacAddr(self, idx, default):
109
    """Returns MAC address for NIC.
110

111
    @type idx: int
112
    @param idx: NIC index
113
    @param default: Default value
114

115
    """
116
    if len(self.nicmac) > idx:
117
      return self.nicmac[idx]
118
    else:
119
      return default
120

    
121
  def SetDiskTemplate(self, template):
122
    """Set the disk template.
123

124
    """
125
    assert template in constants.DISK_TEMPLATES
126

    
127
    self._disk_template = template
128

    
129
  @property
130
  def used(self):
131
    """Returns boolean denoting whether instance is in use.
132

133
    """
134
    return self._used
135

    
136
  @property
137
  def disk_template(self):
138
    """Returns the current disk template.
139

140
    """
141
    return self._disk_template
142

    
143

    
144
class _QaNode(object):
145
  __slots__ = [
146
    "primary",
147
    "secondary",
148
    "_added",
149
    "_use_count",
150
    ]
151

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

155
    """
156
    self.primary = primary
157
    self.secondary = secondary
158
    self._added = False
159
    self._use_count = 0
160

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

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

    
168
  def __repr__(self):
169
    status = [
170
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
171
      "primary=%s" % self.primary,
172
      "secondary=%s" % self.secondary,
173
      "added=%s" % self._added,
174
      "use_count=%s" % self._use_count,
175
      ]
176

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

    
179
  def Use(self):
180
    """Marks a node as being in use.
181

182
    """
183
    assert self._use_count >= 0
184

    
185
    self._use_count += 1
186

    
187
    return self
188

    
189
  def Release(self):
190
    """Release a node (opposite of L{Use}).
191

192
    """
193
    assert self.use_count > 0
194

    
195
    self._use_count -= 1
196

    
197
  def MarkAdded(self):
198
    """Marks node as having been added to a cluster.
199

200
    """
201
    assert not self._added
202
    self._added = True
203

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

207
    """
208
    assert self._added
209
    self._added = False
210

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

215
    """
216
    return self._added
217

    
218
  @property
219
  def use_count(self):
220
    """Returns number of current uses (controlled by L{Use} and L{Release}).
221

222
    """
223
    return self._use_count
224

    
225

    
226
_RESOURCE_CONVERTER = {
227
  "instances": _QaInstance.FromDict,
228
  "nodes": _QaNode.FromDict,
229
  }
230

    
231

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

235
  """
236
  fn = _RESOURCE_CONVERTER.get(key, None)
237
  if fn:
238
    return (key, map(fn, value))
239
  else:
240
    return (key, value)
241

    
242

    
243
class _QaConfig(object):
244
  def __init__(self, data):
245
    """Initializes instances of this class.
246

247
    """
248
    self._data = data
249

    
250
    #: Cluster-wide run-time value of the exclusive storage flag
251
    self._exclusive_storage = None
252

    
253
  @classmethod
254
  def Load(cls, filename):
255
    """Loads a configuration file and produces a configuration object.
256

257
    @type filename: string
258
    @param filename: Path to configuration file
259
    @rtype: L{_QaConfig}
260

261
    """
262
    data = serializer.LoadJson(utils.ReadFile(filename))
263

    
264
    result = cls(dict(map(_ConvertResources,
265
                          data.items()))) # pylint: disable=E1103
266
    result.Validate()
267

    
268
    return result
269

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

273
    """
274
    if not self.get("name"):
275
      raise qa_error.Error("Cluster name is required")
276

    
277
    if not self.get("nodes"):
278
      raise qa_error.Error("Need at least one node")
279

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

    
283
    disks = self.GetDiskOptions()
284
    if disks is None:
285
      raise qa_error.Error("Config option 'disks' must exist")
286
    else:
287
      for d in disks:
288
        if d.get("size") is None or d.get("growth") is None:
289
          raise qa_error.Error("Config options `size` and `growth` must exist"
290
                               " for all `disks` items")
291
    check = self.GetInstanceCheckScript()
292
    if check:
293
      try:
294
        os.stat(check)
295
      except EnvironmentError, err:
296
        raise qa_error.Error("Can't find instance check script '%s': %s" %
297
                             (check, err))
298

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

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

    
308
    (vc_master, vc_basedir) = self.GetVclusterSettings()
309
    if bool(vc_master) != bool(vc_basedir):
310
      raise qa_error.Error("All or none of the config options '%s' and '%s'"
311
                           " must be set" %
312
                           (_VCLUSTER_MASTER_KEY, _VCLUSTER_BASEDIR_KEY))
313

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

    
318
  def __getitem__(self, name):
319
    """Returns configuration value.
320

321
    @type name: string
322
    @param name: Name of configuration entry
323

324
    """
325
    return self._data[name]
326

    
327
  def get(self, name, default=None):
328
    """Returns configuration value.
329

330
    @type name: string
331
    @param name: Name of configuration entry
332
    @param default: Default value
333

334
    """
335
    return self._data.get(name, default)
336

    
337
  def GetMasterNode(self):
338
    """Returns the default master node for the cluster.
339

340
    """
341
    return self["nodes"][0]
342

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

346
    """
347
    return self._data.get(_INSTANCE_CHECK_KEY, None)
348

    
349
  def GetEnabledHypervisors(self):
350
    """Returns list of enabled hypervisors.
351

352
    @rtype: list
353

354
    """
355
    return self._GetStringListParameter(
356
      _ENABLED_HV_KEY,
357
      [constants.DEFAULT_ENABLED_HYPERVISOR])
358

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

362
    """
363
    return self.GetEnabledHypervisors()[0]
364

    
365
  def GetEnabledDiskTemplates(self):
366
    """Returns the list of enabled disk templates.
367

368
    @rtype: list
369

370
    """
371
    return self._GetStringListParameter(
372
      _ENABLED_DISK_TEMPLATES_KEY,
373
      constants.DEFAULT_ENABLED_DISK_TEMPLATES)
374

    
375
  def GetDefaultDiskTemplate(self):
376
    """Returns the default disk template to be used.
377

378
    """
379
    return self.GetEnabledDiskTemplates()[0]
380

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

384
    @rtype: list
385

386
    """
387
    try:
388
      value = self._data[key]
389
    except KeyError:
390
      return default_values
391
    else:
392
      if value is None:
393
        return []
394
      elif isinstance(value, basestring):
395
        return value.split(",")
396
      else:
397
        return value
398

    
399
  def SetExclusiveStorage(self, value):
400
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
401

402
    """
403
    self._exclusive_storage = bool(value)
404

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

408
    """
409
    value = self._exclusive_storage
410
    assert value is not None
411
    return value
412

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

416
    """
417
    enabled = templ in self.GetEnabledDiskTemplates()
418
    return enabled and (not self.GetExclusiveStorage() or
419
                        templ in constants.DTS_EXCL_STORAGE)
420

    
421
  def AreSpindlesSupported(self):
422
    """Are spindles supported by the current configuration?
423

424
    """
425
    return self.GetExclusiveStorage()
426

    
427
  def GetVclusterSettings(self):
428
    """Returns settings for virtual cluster.
429

430
    """
431
    master = self.get(_VCLUSTER_MASTER_KEY)
432
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
433

    
434
    return (master, basedir)
435

    
436
  def GetDiskOptions(self):
437
    """Return options for the disks of the instances.
438

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

442
    """
443
    try:
444
      return self._data["disks"]
445
    except KeyError:
446
      pass
447

    
448
    # Legacy interface
449
    sizes = self._data.get("disk")
450
    growths = self._data.get("disk-growth")
451
    if sizes or growths:
452
      if (sizes is None or growths is None or len(sizes) != len(growths)):
453
        raise qa_error.Error("Config options 'disk' and 'disk-growth' must"
454
                             " exist and have the same number of items")
455
      disks = []
456
      for (size, growth) in zip(sizes, growths):
457
        disks.append({"size": size, "growth": growth})
458
      return disks
459
    else:
460
      return None
461

    
462

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

466
  """
467
  global _config # pylint: disable=W0603
468

    
469
  _config = _QaConfig.Load(path)
470

    
471

    
472
def GetConfig():
473
  """Returns the configuration object.
474

475
  """
476
  if _config is None:
477
    raise RuntimeError("Configuration not yet loaded")
478

    
479
  return _config
480

    
481

    
482
def get(name, default=None):
483
  """Wrapper for L{_QaConfig.get}.
484

485
  """
486
  return GetConfig().get(name, default=default)
487

    
488

    
489
class Either:
490
  def __init__(self, tests):
491
    """Initializes this class.
492

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

497
    """
498
    self.tests = tests
499

    
500

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

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

507
  """
508
  if isinstance(value, (list, tuple)):
509
    return value
510
  else:
511
    return [value]
512

    
513

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

517
  @type check_fn: callable
518
  @param check_fn: Callback to check whether a test is enabled
519
  @type names: sequence or string
520
  @param names: Test name(s)
521
  @type fn: callable
522
  @param fn: Aggregation function
523
  @rtype: bool
524
  @return: Whether test is enabled
525

526
  """
527
  names = _MakeSequence(names)
528

    
529
  result = []
530

    
531
  for name in names:
532
    if isinstance(name, Either):
533
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
534
    elif isinstance(name, (list, tuple)):
535
      value = _TestEnabledInner(check_fn, name, compat.all)
536
    elif callable(name):
537
      value = name()
538
    else:
539
      value = check_fn(name)
540

    
541
    result.append(value)
542

    
543
  return fn(result)
544

    
545

    
546
def TestEnabled(tests, _cfg=None):
547
  """Returns True if the given tests are enabled.
548

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

552
  """
553
  if _cfg is None:
554
    cfg = GetConfig()
555
  else:
556
    cfg = _cfg
557

    
558
  # Get settings for all tests
559
  cfg_tests = cfg.get("tests", {})
560

    
561
  # Get default setting
562
  default = cfg_tests.get("default", True)
563

    
564
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
565
                           tests, compat.all)
566

    
567

    
568
def GetInstanceCheckScript(*args):
569
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
570

571
  """
572
  return GetConfig().GetInstanceCheckScript(*args)
573

    
574

    
575
def GetEnabledHypervisors(*args):
576
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
577

578
  """
579
  return GetConfig().GetEnabledHypervisors(*args)
580

    
581

    
582
def GetDefaultHypervisor(*args):
583
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
584

585
  """
586
  return GetConfig().GetDefaultHypervisor(*args)
587

    
588

    
589
def GetEnabledDiskTemplates(*args):
590
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
591

592
  """
593
  return GetConfig().GetEnabledDiskTemplates(*args)
594

    
595

    
596
def GetDefaultDiskTemplate(*args):
597
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
598

599
  """
600
  return GetConfig().GetDefaultDiskTemplate(*args)
601

    
602

    
603
def GetMasterNode():
604
  """Wrapper for L{_QaConfig.GetMasterNode}.
605

606
  """
607
  return GetConfig().GetMasterNode()
608

    
609

    
610
def AcquireInstance(_cfg=None):
611
  """Returns an instance which isn't in use.
612

613
  """
614
  if _cfg is None:
615
    cfg = GetConfig()
616
  else:
617
    cfg = _cfg
618

    
619
  # Filter out unwanted instances
620
  instances = filter(lambda inst: not inst.used, cfg["instances"])
621

    
622
  if not instances:
623
    raise qa_error.OutOfInstancesError("No instances left")
624

    
625
  instance = instances[0]
626
  instance.Use()
627

    
628
  return instance
629

    
630

    
631
def SetExclusiveStorage(value):
632
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
633

634
  """
635
  return GetConfig().SetExclusiveStorage(value)
636

    
637

    
638
def GetExclusiveStorage():
639
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
640

641
  """
642
  return GetConfig().GetExclusiveStorage()
643

    
644

    
645
def IsTemplateSupported(templ):
646
  """Wrapper for L{_QaConfig.IsTemplateSupported}.
647

648
  """
649
  return GetConfig().IsTemplateSupported(templ)
650

    
651

    
652
def AreSpindlesSupported():
653
  """Wrapper for L{_QaConfig.AreSpindlesSupported}.
654

655
  """
656
  return GetConfig().AreSpindlesSupported()
657

    
658

    
659
def _NodeSortKey(node):
660
  """Returns sort key for a node.
661

662
  @type node: L{_QaNode}
663

664
  """
665
  return (node.use_count, utils.NiceSortKey(node.primary))
666

    
667

    
668
def AcquireNode(exclude=None, _cfg=None):
669
  """Returns the least used node.
670

671
  """
672
  if _cfg is None:
673
    cfg = GetConfig()
674
  else:
675
    cfg = _cfg
676

    
677
  master = cfg.GetMasterNode()
678

    
679
  # Filter out unwanted nodes
680
  # TODO: Maybe combine filters
681
  if exclude is None:
682
    nodes = cfg["nodes"][:]
683
  elif isinstance(exclude, (list, tuple)):
684
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
685
  else:
686
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
687

    
688
  nodes = filter(lambda node: node.added or node == master, nodes)
689

    
690
  if not nodes:
691
    raise qa_error.OutOfNodesError("No nodes left")
692

    
693
  # Return node with least number of uses
694
  return sorted(nodes, key=_NodeSortKey)[0].Use()
695

    
696

    
697
def AcquireManyNodes(num, exclude=None):
698
  """Return the least used nodes.
699

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

707
  """
708
  nodes = []
709
  if exclude is None:
710
    exclude = []
711
  elif isinstance(exclude, (list, tuple)):
712
    # Don't modify the incoming argument
713
    exclude = list(exclude)
714
  else:
715
    exclude = [exclude]
716

    
717
  try:
718
    for _ in range(0, num):
719
      n = AcquireNode(exclude=exclude)
720
      nodes.append(n)
721
      exclude.append(n)
722
  except qa_error.OutOfNodesError:
723
    ReleaseManyNodes(nodes)
724
    raise
725
  return nodes
726

    
727

    
728
def ReleaseManyNodes(nodes):
729
  for node in nodes:
730
    node.Release()
731

    
732

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

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

    
739

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

743
  @rtype: bool
744

745
  """
746
  if _cfg is None:
747
    cfg = GetConfig()
748
  else:
749
    cfg = _cfg
750

    
751
  (master, _) = cfg.GetVclusterSettings()
752

    
753
  return bool(master)
754

    
755

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

760
  """
761
  return not UseVirtualCluster()
762

    
763

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

767
  """
768
  return GetConfig().GetDiskOptions()