Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ e80edd3b

History | View | Annotate | Download (13.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

    
33
import qa_error
34

    
35

    
36
_INSTANCE_CHECK_KEY = "instance-check"
37
_ENABLED_HV_KEY = "enabled-hypervisors"
38

    
39
#: QA configuration (L{_QaConfig})
40
_config = None
41

    
42

    
43
class _QaInstance(object):
44
  __slots__ = [
45
    "name",
46
    "nicmac",
47
    "_used",
48
    "_disk_template",
49
    ]
50

    
51
  def __init__(self, name, nicmac):
52
    """Initializes instances of this class.
53

54
    """
55
    self.name = name
56
    self.nicmac = nicmac
57
    self._used = None
58
    self._disk_template = None
59

    
60
  @classmethod
61
  def FromDict(cls, data):
62
    """Creates instance object from JSON dictionary.
63

64
    """
65
    nicmac = []
66

    
67
    macaddr = data.get("nic.mac/0")
68
    if macaddr:
69
      nicmac.append(macaddr)
70

    
71
    return cls(name=data["name"], nicmac=nicmac)
72

    
73
  def __repr__(self):
74
    status = [
75
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
76
      "name=%s" % self.name,
77
      "nicmac=%s" % self.nicmac,
78
      "used=%s" % self._used,
79
      "disk_template=%s" % self._disk_template,
80
      ]
81

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

    
84
  def Use(self):
85
    """Marks instance as being in use.
86

87
    """
88
    assert not self._used
89
    assert self._disk_template is None
90

    
91
    self._used = True
92

    
93
  def Release(self):
94
    """Releases instance and makes it available again.
95

96
    """
97
    assert self._used, \
98
      ("Instance '%s' was never acquired or released more than once" %
99
       self.name)
100

    
101
    self._used = False
102
    self._disk_template = None
103

    
104
  def GetNicMacAddr(self, idx, default):
105
    """Returns MAC address for NIC.
106

107
    @type idx: int
108
    @param idx: NIC index
109
    @param default: Default value
110

111
    """
112
    if len(self.nicmac) > idx:
113
      return self.nicmac[idx]
114
    else:
115
      return default
116

    
117
  def SetDiskTemplate(self, template):
118
    """Set the disk template.
119

120
    """
121
    assert template in constants.DISK_TEMPLATES
122

    
123
    self._disk_template = template
124

    
125
  @property
126
  def used(self):
127
    """Returns boolean denoting whether instance is in use.
128

129
    """
130
    return self._used
131

    
132
  @property
133
  def disk_template(self):
134
    """Returns the current disk template.
135

136
    """
137
    return self._disk_template
138

    
139

    
140
class _QaNode(object):
141
  __slots__ = [
142
    "primary",
143
    "secondary",
144
    "_added",
145
    "_use_count",
146
    ]
147

    
148
  def __init__(self, primary, secondary):
149
    """Initializes instances of this class.
150

151
    """
152
    self.primary = primary
153
    self.secondary = secondary
154
    self._added = False
155
    self._use_count = 0
156

    
157
  @classmethod
158
  def FromDict(cls, data):
159
    """Creates node object from JSON dictionary.
160

161
    """
162
    return cls(primary=data["primary"], secondary=data.get("secondary"))
163

    
164
  def __repr__(self):
165
    status = [
166
      "%s.%s" % (self.__class__.__module__, self.__class__.__name__),
167
      "primary=%s" % self.primary,
168
      "secondary=%s" % self.secondary,
169
      "added=%s" % self._added,
170
      "use_count=%s" % self._use_count,
171
      ]
172

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

    
175
  def Use(self):
176
    """Marks a node as being in use.
177

178
    """
179
    assert self._use_count >= 0
180

    
181
    self._use_count += 1
182

    
183
    return self
184

    
185
  def Release(self):
186
    """Release a node (opposite of L{Use}).
187

188
    """
189
    assert self.use_count > 0
190

    
191
    self._use_count -= 1
192

    
193
  def MarkAdded(self):
194
    """Marks node as having been added to a cluster.
195

196
    """
197
    assert not self._added
198
    self._added = True
199

    
200
  def MarkRemoved(self):
201
    """Marks node as having been removed from a cluster.
202

203
    """
204
    assert self._added
205
    self._added = False
206

    
207
  @property
208
  def added(self):
209
    """Returns whether a node is part of a cluster.
210

211
    """
212
    return self._added
213

    
214
  @property
215
  def use_count(self):
216
    """Returns number of current uses (controlled by L{Use} and L{Release}).
217

218
    """
219
    return self._use_count
220

    
221

    
222
_RESOURCE_CONVERTER = {
223
  "instances": _QaInstance.FromDict,
224
  "nodes": _QaNode.FromDict,
225
  }
226

    
227

    
228
def _ConvertResources((key, value)):
229
  """Converts cluster resources in configuration to Python objects.
230

231
  """
232
  fn = _RESOURCE_CONVERTER.get(key, None)
233
  if fn:
234
    return (key, map(fn, value))
235
  else:
236
    return (key, value)
237

    
238

    
239
class _QaConfig(object):
240
  def __init__(self, data):
241
    """Initializes instances of this class.
242

243
    """
244
    self._data = data
245

    
246
    #: Cluster-wide run-time value of the exclusive storage flag
247
    self._exclusive_storage = None
248

    
249
  @classmethod
250
  def Load(cls, filename):
251
    """Loads a configuration file and produces a configuration object.
252

253
    @type filename: string
254
    @param filename: Path to configuration file
255
    @rtype: L{_QaConfig}
256

257
    """
258
    data = serializer.LoadJson(utils.ReadFile(filename))
259

    
260
    result = cls(dict(map(_ConvertResources,
261
                          data.items()))) # pylint: disable=E1103
262
    result.Validate()
263

    
264
    return result
265

    
266
  def Validate(self):
267
    """Validates loaded configuration data.
268

269
    """
270
    if not self.get("name"):
271
      raise qa_error.Error("Cluster name is required")
272

    
273
    if not self.get("nodes"):
274
      raise qa_error.Error("Need at least one node")
275

    
276
    if not self.get("instances"):
277
      raise qa_error.Error("Need at least one instance")
278

    
279
    if (self.get("disk") is None or
280
        self.get("disk-growth") is None or
281
        len(self.get("disk")) != len(self.get("disk-growth"))):
282
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
283
                           " and have the same number of items")
284

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

    
293
    enabled_hv = frozenset(self.GetEnabledHypervisors())
294
    if not enabled_hv:
295
      raise qa_error.Error("No hypervisor is enabled")
296

    
297
    difference = enabled_hv - constants.HYPER_TYPES
298
    if difference:
299
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
300
                           utils.CommaJoin(difference))
301

    
302
  def __getitem__(self, name):
303
    """Returns configuration value.
304

305
    @type name: string
306
    @param name: Name of configuration entry
307

308
    """
309
    return self._data[name]
310

    
311
  def get(self, name, default=None):
312
    """Returns configuration value.
313

314
    @type name: string
315
    @param name: Name of configuration entry
316
    @param default: Default value
317

318
    """
319
    return self._data.get(name, default)
320

    
321
  def GetMasterNode(self):
322
    """Returns the default master node for the cluster.
323

324
    """
325
    return self["nodes"][0]
326

    
327
  def GetInstanceCheckScript(self):
328
    """Returns path to instance check script or C{None}.
329

330
    """
331
    return self._data.get(_INSTANCE_CHECK_KEY, None)
332

    
333
  def GetEnabledHypervisors(self):
334
    """Returns list of enabled hypervisors.
335

336
    @rtype: list
337

338
    """
339
    try:
340
      value = self._data[_ENABLED_HV_KEY]
341
    except KeyError:
342
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
343
    else:
344
      if value is None:
345
        return []
346
      elif isinstance(value, basestring):
347
        # The configuration key ("enabled-hypervisors") implies there can be
348
        # multiple values. Multiple hypervisors are comma-separated on the
349
        # command line option to "gnt-cluster init", so we need to handle them
350
        # equally here.
351
        return value.split(",")
352
      else:
353
        return value
354

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

358
    """
359
    return self.GetEnabledHypervisors()[0]
360

    
361
  def SetExclusiveStorage(self, value):
362
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
363

364
    """
365
    self._exclusive_storage = bool(value)
366

    
367
  def GetExclusiveStorage(self):
368
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
369

370
    """
371
    value = self._exclusive_storage
372
    assert value is not None
373
    return value
374

    
375
  def IsTemplateSupported(self, templ):
376
    """Is the given disk template supported by the current configuration?
377

378
    """
379
    return (not self.GetExclusiveStorage() or
380
            templ in constants.DTS_EXCL_STORAGE)
381

    
382

    
383
def Load(path):
384
  """Loads the passed configuration file.
385

386
  """
387
  global _config # pylint: disable=W0603
388

    
389
  _config = _QaConfig.Load(path)
390

    
391

    
392
def GetConfig():
393
  """Returns the configuration object.
394

395
  """
396
  if _config is None:
397
    raise RuntimeError("Configuration not yet loaded")
398

    
399
  return _config
400

    
401

    
402
def get(name, default=None):
403
  """Wrapper for L{_QaConfig.get}.
404

405
  """
406
  return GetConfig().get(name, default=default)
407

    
408

    
409
class Either:
410
  def __init__(self, tests):
411
    """Initializes this class.
412

413
    @type tests: list or string
414
    @param tests: List of test names
415
    @see: L{TestEnabled} for details
416

417
    """
418
    self.tests = tests
419

    
420

    
421
def _MakeSequence(value):
422
  """Make sequence of single argument.
423

424
  If the single argument is not already a list or tuple, a list with the
425
  argument as a single item is returned.
426

427
  """
428
  if isinstance(value, (list, tuple)):
429
    return value
430
  else:
431
    return [value]
432

    
433

    
434
def _TestEnabledInner(check_fn, names, fn):
435
  """Evaluate test conditions.
436

437
  @type check_fn: callable
438
  @param check_fn: Callback to check whether a test is enabled
439
  @type names: sequence or string
440
  @param names: Test name(s)
441
  @type fn: callable
442
  @param fn: Aggregation function
443
  @rtype: bool
444
  @return: Whether test is enabled
445

446
  """
447
  names = _MakeSequence(names)
448

    
449
  result = []
450

    
451
  for name in names:
452
    if isinstance(name, Either):
453
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
454
    elif isinstance(name, (list, tuple)):
455
      value = _TestEnabledInner(check_fn, name, compat.all)
456
    else:
457
      value = check_fn(name)
458

    
459
    result.append(value)
460

    
461
  return fn(result)
462

    
463

    
464
def TestEnabled(tests, _cfg=None):
465
  """Returns True if the given tests are enabled.
466

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

470
  """
471
  if _cfg is None:
472
    cfg = GetConfig()
473
  else:
474
    cfg = _cfg
475

    
476
  # Get settings for all tests
477
  cfg_tests = cfg.get("tests", {})
478

    
479
  # Get default setting
480
  default = cfg_tests.get("default", True)
481

    
482
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
483
                           tests, compat.all)
484

    
485

    
486
def GetInstanceCheckScript(*args):
487
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
488

489
  """
490
  return GetConfig().GetInstanceCheckScript(*args)
491

    
492

    
493
def GetEnabledHypervisors(*args):
494
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
495

496
  """
497
  return GetConfig().GetEnabledHypervisors(*args)
498

    
499

    
500
def GetDefaultHypervisor(*args):
501
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
502

503
  """
504
  return GetConfig().GetDefaultHypervisor(*args)
505

    
506

    
507
def GetMasterNode():
508
  """Wrapper for L{_QaConfig.GetMasterNode}.
509

510
  """
511
  return GetConfig().GetMasterNode()
512

    
513

    
514
def AcquireInstance(_cfg=None):
515
  """Returns an instance which isn't in use.
516

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

    
523
  # Filter out unwanted instances
524
  instances = filter(lambda inst: not inst.used, cfg["instances"])
525

    
526
  if not instances:
527
    raise qa_error.OutOfInstancesError("No instances left")
528

    
529
  instance = instances[0]
530
  instance.Use()
531

    
532
  return instance
533

    
534

    
535
def SetExclusiveStorage(value):
536
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
537

538
  """
539
  return GetConfig().SetExclusiveStorage(value)
540

    
541

    
542
def GetExclusiveStorage():
543
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
544

545
  """
546
  return GetConfig().GetExclusiveStorage()
547

    
548

    
549
def IsTemplateSupported(templ):
550
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
551

552
  """
553
  return GetConfig().IsTemplateSupported(templ)
554

    
555

    
556
def AcquireNode(exclude=None, _cfg=None):
557
  """Returns the least used node.
558

559
  """
560
  if _cfg is None:
561
    cfg = GetConfig()
562
  else:
563
    cfg = _cfg
564

    
565
  master = cfg.GetMasterNode()
566

    
567
  # Filter out unwanted nodes
568
  # TODO: Maybe combine filters
569
  if exclude is None:
570
    nodes = cfg["nodes"][:]
571
  elif isinstance(exclude, (list, tuple)):
572
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
573
  else:
574
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
575

    
576
  nodes = filter(lambda node: node.added or node == master, nodes)
577

    
578
  if not nodes:
579
    raise qa_error.OutOfNodesError("No nodes left")
580

    
581
  # Get node with least number of uses
582
  # TODO: Switch to computing sort key instead of comparing directly
583
  def compare(a, b):
584
    result = cmp(a.use_count, b.use_count)
585
    if result == 0:
586
      result = cmp(a.primary, b.primary)
587
    return result
588

    
589
  nodes.sort(cmp=compare)
590

    
591
  return nodes[0].Use()
592

    
593

    
594
def AcquireManyNodes(num, exclude=None):
595
  """Return the least used nodes.
596

597
  @type num: int
598
  @param num: Number of nodes; can be 0.
599
  @type exclude: list of nodes or C{None}
600
  @param exclude: nodes to be excluded from the choice
601
  @rtype: list of nodes
602
  @return: C{num} different nodes
603

604
  """
605
  nodes = []
606
  if exclude is None:
607
    exclude = []
608
  elif isinstance(exclude, (list, tuple)):
609
    # Don't modify the incoming argument
610
    exclude = list(exclude)
611
  else:
612
    exclude = [exclude]
613

    
614
  try:
615
    for _ in range(0, num):
616
      n = AcquireNode(exclude=exclude)
617
      nodes.append(n)
618
      exclude.append(n)
619
  except qa_error.OutOfNodesError:
620
    ReleaseManyNodes(nodes)
621
    raise
622
  return nodes
623

    
624

    
625
def ReleaseManyNodes(nodes):
626
  for node in nodes:
627
    node.Release()