Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 2dae8d64

History | View | Annotate | Download (15.7 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
    if (self.get("disk") is None or
284
        self.get("disk-growth") is None or
285
        len(self.get("disk")) != len(self.get("disk-growth"))):
286
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
287
                           " and have the same number of items")
288

    
289
    check = self.GetInstanceCheckScript()
290
    if check:
291
      try:
292
        os.stat(check)
293
      except EnvironmentError, err:
294
        raise qa_error.Error("Can't find instance check script '%s': %s" %
295
                             (check, err))
296

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

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

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

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

    
316
  def __getitem__(self, name):
317
    """Returns configuration value.
318

319
    @type name: string
320
    @param name: Name of configuration entry
321

322
    """
323
    return self._data[name]
324

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

328
    @type name: string
329
    @param name: Name of configuration entry
330
    @param default: Default value
331

332
    """
333
    return self._data.get(name, default)
334

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

338
    """
339
    return self["nodes"][0]
340

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

344
    """
345
    return self._data.get(_INSTANCE_CHECK_KEY, None)
346

    
347
  def GetEnabledHypervisors(self):
348
    """Returns list of enabled hypervisors.
349

350
    @rtype: list
351

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

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

360
    """
361
    return self.GetEnabledHypervisors()[0]
362

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

366
    @rtype: list
367

368
    """
369
    return self._GetStringListParameter(
370
      _ENABLED_DISK_TEMPLATES_KEY,
371
      list(constants.DEFAULT_ENABLED_DISK_TEMPLATES))
372

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

376
    """
377
    return self.GetEnabledDiskTemplates()[0]
378

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

382
    @rtype: list
383

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

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

400
    """
401
    self._exclusive_storage = bool(value)
402

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

406
    """
407
    value = self._exclusive_storage
408
    assert value is not None
409
    return value
410

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

414
    """
415
    return (not self.GetExclusiveStorage() or
416
            templ in constants.DTS_EXCL_STORAGE)
417

    
418
  def GetVclusterSettings(self):
419
    """Returns settings for virtual cluster.
420

421
    """
422
    master = self.get(_VCLUSTER_MASTER_KEY)
423
    basedir = self.get(_VCLUSTER_BASEDIR_KEY)
424

    
425
    return (master, basedir)
426

    
427

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

431
  """
432
  global _config # pylint: disable=W0603
433

    
434
  _config = _QaConfig.Load(path)
435

    
436

    
437
def GetConfig():
438
  """Returns the configuration object.
439

440
  """
441
  if _config is None:
442
    raise RuntimeError("Configuration not yet loaded")
443

    
444
  return _config
445

    
446

    
447
def get(name, default=None):
448
  """Wrapper for L{_QaConfig.get}.
449

450
  """
451
  return GetConfig().get(name, default=default)
452

    
453

    
454
class Either:
455
  def __init__(self, tests):
456
    """Initializes this class.
457

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

462
    """
463
    self.tests = tests
464

    
465

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

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

472
  """
473
  if isinstance(value, (list, tuple)):
474
    return value
475
  else:
476
    return [value]
477

    
478

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

482
  @type check_fn: callable
483
  @param check_fn: Callback to check whether a test is enabled
484
  @type names: sequence or string
485
  @param names: Test name(s)
486
  @type fn: callable
487
  @param fn: Aggregation function
488
  @rtype: bool
489
  @return: Whether test is enabled
490

491
  """
492
  names = _MakeSequence(names)
493

    
494
  result = []
495

    
496
  for name in names:
497
    if isinstance(name, Either):
498
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
499
    elif isinstance(name, (list, tuple)):
500
      value = _TestEnabledInner(check_fn, name, compat.all)
501
    elif callable(name):
502
      value = name()
503
    else:
504
      value = check_fn(name)
505

    
506
    result.append(value)
507

    
508
  return fn(result)
509

    
510

    
511
def TestEnabled(tests, _cfg=None):
512
  """Returns True if the given tests are enabled.
513

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

517
  """
518
  if _cfg is None:
519
    cfg = GetConfig()
520
  else:
521
    cfg = _cfg
522

    
523
  # Get settings for all tests
524
  cfg_tests = cfg.get("tests", {})
525

    
526
  # Get default setting
527
  default = cfg_tests.get("default", True)
528

    
529
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
530
                           tests, compat.all)
531

    
532

    
533
def GetInstanceCheckScript(*args):
534
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
535

536
  """
537
  return GetConfig().GetInstanceCheckScript(*args)
538

    
539

    
540
def GetEnabledHypervisors(*args):
541
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
542

543
  """
544
  return GetConfig().GetEnabledHypervisors(*args)
545

    
546

    
547
def GetDefaultHypervisor(*args):
548
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
549

550
  """
551
  return GetConfig().GetDefaultHypervisor(*args)
552

    
553

    
554
def GetEnabledDiskTemplates(*args):
555
  """Wrapper for L{_QaConfig.GetEnabledDiskTemplates}.
556

557
  """
558
  return GetConfig().GetEnabledDiskTemplates(*args)
559

    
560

    
561
def GetDefaultDiskTemplate(*args):
562
  """Wrapper for L{_QaConfig.GetDefaultDiskTemplate}.
563

564
  """
565
  return GetConfig().GetDefaultDiskTemplate(*args)
566

    
567

    
568
def GetMasterNode():
569
  """Wrapper for L{_QaConfig.GetMasterNode}.
570

571
  """
572
  return GetConfig().GetMasterNode()
573

    
574

    
575
def AcquireInstance(_cfg=None):
576
  """Returns an instance which isn't in use.
577

578
  """
579
  if _cfg is None:
580
    cfg = GetConfig()
581
  else:
582
    cfg = _cfg
583

    
584
  # Filter out unwanted instances
585
  instances = filter(lambda inst: not inst.used, cfg["instances"])
586

    
587
  if not instances:
588
    raise qa_error.OutOfInstancesError("No instances left")
589

    
590
  instance = instances[0]
591
  instance.Use()
592

    
593
  return instance
594

    
595

    
596
def SetExclusiveStorage(value):
597
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
598

599
  """
600
  return GetConfig().SetExclusiveStorage(value)
601

    
602

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

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

    
609

    
610
def IsTemplateSupported(templ):
611
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
612

613
  """
614
  return GetConfig().IsTemplateSupported(templ)
615

    
616

    
617
def _NodeSortKey(node):
618
  """Returns sort key for a node.
619

620
  @type node: L{_QaNode}
621

622
  """
623
  return (node.use_count, utils.NiceSortKey(node.primary))
624

    
625

    
626
def AcquireNode(exclude=None, _cfg=None):
627
  """Returns the least used node.
628

629
  """
630
  if _cfg is None:
631
    cfg = GetConfig()
632
  else:
633
    cfg = _cfg
634

    
635
  master = cfg.GetMasterNode()
636

    
637
  # Filter out unwanted nodes
638
  # TODO: Maybe combine filters
639
  if exclude is None:
640
    nodes = cfg["nodes"][:]
641
  elif isinstance(exclude, (list, tuple)):
642
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
643
  else:
644
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
645

    
646
  nodes = filter(lambda node: node.added or node == master, nodes)
647

    
648
  if not nodes:
649
    raise qa_error.OutOfNodesError("No nodes left")
650

    
651
  # Return node with least number of uses
652
  return sorted(nodes, key=_NodeSortKey)[0].Use()
653

    
654

    
655
def AcquireManyNodes(num, exclude=None):
656
  """Return the least used nodes.
657

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

665
  """
666
  nodes = []
667
  if exclude is None:
668
    exclude = []
669
  elif isinstance(exclude, (list, tuple)):
670
    # Don't modify the incoming argument
671
    exclude = list(exclude)
672
  else:
673
    exclude = [exclude]
674

    
675
  try:
676
    for _ in range(0, num):
677
      n = AcquireNode(exclude=exclude)
678
      nodes.append(n)
679
      exclude.append(n)
680
  except qa_error.OutOfNodesError:
681
    ReleaseManyNodes(nodes)
682
    raise
683
  return nodes
684

    
685

    
686
def ReleaseManyNodes(nodes):
687
  for node in nodes:
688
    node.Release()
689

    
690

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

694
  """
695
  return GetConfig().GetVclusterSettings()
696

    
697

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

701
  @rtype: bool
702

703
  """
704
  if _cfg is None:
705
    cfg = GetConfig()
706
  else:
707
    cfg = _cfg
708

    
709
  (master, _) = cfg.GetVclusterSettings()
710

    
711
  return bool(master)
712

    
713

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

718
  """
719
  return not UseVirtualCluster()