Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 02cff8aa

History | View | Annotate | Download (15.8 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
    enabled = templ in self.GetEnabledDiskTemplates()
416
    return enabled and (not self.GetExclusiveStorage() or
417
                        templ in constants.DTS_EXCL_STORAGE)
418

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

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

    
426
    return (master, basedir)
427

    
428

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

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

    
435
  _config = _QaConfig.Load(path)
436

    
437

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

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

    
445
  return _config
446

    
447

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

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

    
454

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

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

463
    """
464
    self.tests = tests
465

    
466

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

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

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

    
479

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

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

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

    
495
  result = []
496

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

    
507
    result.append(value)
508

    
509
  return fn(result)
510

    
511

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

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

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

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

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

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

    
533

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

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

    
540

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

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

    
547

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

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

    
554

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

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

    
561

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

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

    
568

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

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

    
575

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

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

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

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

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

    
594
  return instance
595

    
596

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

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

    
603

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

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

    
610

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

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

    
617

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

621
  @type node: L{_QaNode}
622

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

    
626

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

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

    
636
  master = cfg.GetMasterNode()
637

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

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

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

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

    
655

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

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

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

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

    
686

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

    
691

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

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

    
698

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

702
  @rtype: bool
703

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

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

    
712
  return bool(master)
713

    
714

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

719
  """
720
  return not UseVirtualCluster()