Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 565cb4bf

History | View | Annotate | Download (13.6 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 __getitem__(self, key):
74
    """Legacy dict-like interface.
75

76
    """
77
    if key == "name":
78
      return self.name
79
    else:
80
      raise KeyError(key)
81

    
82
  def get(self, key, default):
83
    """Legacy dict-like interface.
84

85
    """
86
    try:
87
      return self[key]
88
    except KeyError:
89
      return default
90

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

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

    
99
    self.used = False
100
    self.disk_template = None
101

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

105
    @type idx: int
106
    @param idx: NIC index
107
    @param default: Default value
108

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

    
115

    
116
class _QaNode(object):
117
  __slots__ = [
118
    "primary",
119
    "secondary",
120
    "_added",
121
    "_use_count",
122
    ]
123

    
124
  def __init__(self, primary, secondary):
125
    """Initializes instances of this class.
126

127
    """
128
    self.primary = primary
129
    self.secondary = secondary
130
    self._added = False
131
    self._use_count = 0
132

    
133
  @classmethod
134
  def FromDict(cls, data):
135
    """Creates node object from JSON dictionary.
136

137
    """
138
    return cls(primary=data["primary"], secondary=data.get("secondary"))
139

    
140
  def __getitem__(self, key):
141
    """Legacy dict-like interface.
142

143
    """
144
    if key == "primary":
145
      return self.primary
146
    elif key == "secondary":
147
      return self.secondary
148
    else:
149
      raise KeyError(key)
150

    
151
  def get(self, key, default):
152
    """Legacy dict-like interface.
153

154
    """
155
    try:
156
      return self[key]
157
    except KeyError:
158
      return default
159

    
160
  def Use(self):
161
    """Marks a node as being in use.
162

163
    """
164
    assert self._use_count >= 0
165

    
166
    self._use_count += 1
167

    
168
    return self
169

    
170
  def Release(self):
171
    """Release a node (opposite of L{Use}).
172

173
    """
174
    assert self.use_count > 0
175

    
176
    self._use_count -= 1
177

    
178
  def MarkAdded(self):
179
    """Marks node as having been added to a cluster.
180

181
    """
182
    assert not self._added
183
    self._added = True
184

    
185
  def MarkRemoved(self):
186
    """Marks node as having been removed from a cluster.
187

188
    """
189
    assert self._added
190
    self._added = False
191

    
192
  @property
193
  def added(self):
194
    """Returns whether a node is part of a cluster.
195

196
    """
197
    return self._added
198

    
199
  @property
200
  def use_count(self):
201
    """Returns number of current uses (controlled by L{Use} and L{Release}).
202

203
    """
204
    return self._use_count
205

    
206

    
207
_RESOURCE_CONVERTER = {
208
  "instances": _QaInstance.FromDict,
209
  "nodes": _QaNode.FromDict,
210
  }
211

    
212

    
213
def _ConvertResources((key, value)):
214
  """Converts cluster resources in configuration to Python objects.
215

216
  """
217
  fn = _RESOURCE_CONVERTER.get(key, None)
218
  if fn:
219
    return (key, map(fn, value))
220
  else:
221
    return (key, value)
222

    
223

    
224
class _QaConfig(object):
225
  def __init__(self, data):
226
    """Initializes instances of this class.
227

228
    """
229
    self._data = data
230

    
231
    #: Cluster-wide run-time value of the exclusive storage flag
232
    self._exclusive_storage = None
233

    
234
  @classmethod
235
  def Load(cls, filename):
236
    """Loads a configuration file and produces a configuration object.
237

238
    @type filename: string
239
    @param filename: Path to configuration file
240
    @rtype: L{_QaConfig}
241

242
    """
243
    data = serializer.LoadJson(utils.ReadFile(filename))
244

    
245
    result = cls(dict(map(_ConvertResources,
246
                          data.items()))) # pylint: disable=E1103
247
    result.Validate()
248

    
249
    return result
250

    
251
  def Validate(self):
252
    """Validates loaded configuration data.
253

254
    """
255
    if not self.get("nodes"):
256
      raise qa_error.Error("Need at least one node")
257

    
258
    if not self.get("instances"):
259
      raise qa_error.Error("Need at least one instance")
260

    
261
    if (self.get("disk") is None or
262
        self.get("disk-growth") is None or
263
        len(self.get("disk")) != len(self.get("disk-growth"))):
264
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
265
                           " and have the same number of items")
266

    
267
    check = self.GetInstanceCheckScript()
268
    if check:
269
      try:
270
        os.stat(check)
271
      except EnvironmentError, err:
272
        raise qa_error.Error("Can't find instance check script '%s': %s" %
273
                             (check, err))
274

    
275
    enabled_hv = frozenset(self.GetEnabledHypervisors())
276
    if not enabled_hv:
277
      raise qa_error.Error("No hypervisor is enabled")
278

    
279
    difference = enabled_hv - constants.HYPER_TYPES
280
    if difference:
281
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
282
                           utils.CommaJoin(difference))
283

    
284
  def __getitem__(self, name):
285
    """Returns configuration value.
286

287
    @type name: string
288
    @param name: Name of configuration entry
289

290
    """
291
    return self._data[name]
292

    
293
  def get(self, name, default=None):
294
    """Returns configuration value.
295

296
    @type name: string
297
    @param name: Name of configuration entry
298
    @param default: Default value
299

300
    """
301
    return self._data.get(name, default)
302

    
303
  def GetMasterNode(self):
304
    """Returns the default master node for the cluster.
305

306
    """
307
    return self["nodes"][0]
308

    
309
  def GetInstanceCheckScript(self):
310
    """Returns path to instance check script or C{None}.
311

312
    """
313
    return self._data.get(_INSTANCE_CHECK_KEY, None)
314

    
315
  def GetEnabledHypervisors(self):
316
    """Returns list of enabled hypervisors.
317

318
    @rtype: list
319

320
    """
321
    try:
322
      value = self._data[_ENABLED_HV_KEY]
323
    except KeyError:
324
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
325
    else:
326
      if value is None:
327
        return []
328
      elif isinstance(value, basestring):
329
        # The configuration key ("enabled-hypervisors") implies there can be
330
        # multiple values. Multiple hypervisors are comma-separated on the
331
        # command line option to "gnt-cluster init", so we need to handle them
332
        # equally here.
333
        return value.split(",")
334
      else:
335
        return value
336

    
337
  def GetDefaultHypervisor(self):
338
    """Returns the default hypervisor to be used.
339

340
    """
341
    return self.GetEnabledHypervisors()[0]
342

    
343
  def SetExclusiveStorage(self, value):
344
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
345

346
    """
347
    self._exclusive_storage = bool(value)
348

    
349
  def GetExclusiveStorage(self):
350
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
351

352
    """
353
    value = self._exclusive_storage
354
    assert value is not None
355
    return value
356

    
357
  def IsTemplateSupported(self, templ):
358
    """Is the given disk template supported by the current configuration?
359

360
    """
361
    return (not self.GetExclusiveStorage() or
362
            templ in constants.DTS_EXCL_STORAGE)
363

    
364

    
365
def Load(path):
366
  """Loads the passed configuration file.
367

368
  """
369
  global _config # pylint: disable=W0603
370

    
371
  _config = _QaConfig.Load(path)
372

    
373

    
374
def GetConfig():
375
  """Returns the configuration object.
376

377
  """
378
  if _config is None:
379
    raise RuntimeError("Configuration not yet loaded")
380

    
381
  return _config
382

    
383

    
384
def get(name, default=None):
385
  """Wrapper for L{_QaConfig.get}.
386

387
  """
388
  return GetConfig().get(name, default=default)
389

    
390

    
391
class Either:
392
  def __init__(self, tests):
393
    """Initializes this class.
394

395
    @type tests: list or string
396
    @param tests: List of test names
397
    @see: L{TestEnabled} for details
398

399
    """
400
    self.tests = tests
401

    
402

    
403
def _MakeSequence(value):
404
  """Make sequence of single argument.
405

406
  If the single argument is not already a list or tuple, a list with the
407
  argument as a single item is returned.
408

409
  """
410
  if isinstance(value, (list, tuple)):
411
    return value
412
  else:
413
    return [value]
414

    
415

    
416
def _TestEnabledInner(check_fn, names, fn):
417
  """Evaluate test conditions.
418

419
  @type check_fn: callable
420
  @param check_fn: Callback to check whether a test is enabled
421
  @type names: sequence or string
422
  @param names: Test name(s)
423
  @type fn: callable
424
  @param fn: Aggregation function
425
  @rtype: bool
426
  @return: Whether test is enabled
427

428
  """
429
  names = _MakeSequence(names)
430

    
431
  result = []
432

    
433
  for name in names:
434
    if isinstance(name, Either):
435
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
436
    elif isinstance(name, (list, tuple)):
437
      value = _TestEnabledInner(check_fn, name, compat.all)
438
    else:
439
      value = check_fn(name)
440

    
441
    result.append(value)
442

    
443
  return fn(result)
444

    
445

    
446
def TestEnabled(tests, _cfg=None):
447
  """Returns True if the given tests are enabled.
448

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

452
  """
453
  if _cfg is None:
454
    cfg = GetConfig()
455
  else:
456
    cfg = _cfg
457

    
458
  # Get settings for all tests
459
  cfg_tests = cfg.get("tests", {})
460

    
461
  # Get default setting
462
  default = cfg_tests.get("default", True)
463

    
464
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
465
                           tests, compat.all)
466

    
467

    
468
def GetInstanceCheckScript(*args):
469
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
470

471
  """
472
  return GetConfig().GetInstanceCheckScript(*args)
473

    
474

    
475
def GetEnabledHypervisors(*args):
476
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
477

478
  """
479
  return GetConfig().GetEnabledHypervisors(*args)
480

    
481

    
482
def GetDefaultHypervisor(*args):
483
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
484

485
  """
486
  return GetConfig().GetDefaultHypervisor(*args)
487

    
488

    
489
def GetInstanceNicMac(inst, default=None):
490
  """Returns MAC address for instance's network interface.
491

492
  """
493
  return inst.GetNicMacAddr(0, default)
494

    
495

    
496
def GetMasterNode():
497
  """Wrapper for L{_QaConfig.GetMasterNode}.
498

499
  """
500
  return GetConfig().GetMasterNode()
501

    
502

    
503
def AcquireInstance(_cfg=None):
504
  """Returns an instance which isn't in use.
505

506
  """
507
  if _cfg is None:
508
    cfg = GetConfig()
509
  else:
510
    cfg = _cfg
511

    
512
  # Filter out unwanted instances
513
  instances = filter(lambda inst: not inst.used, cfg["instances"])
514

    
515
  if not instances:
516
    raise qa_error.OutOfInstancesError("No instances left")
517

    
518
  inst = instances[0]
519

    
520
  assert not inst.used
521
  assert inst.disk_template is None
522

    
523
  inst.used = True
524

    
525
  return inst
526

    
527

    
528
def GetInstanceTemplate(inst):
529
  """Return the disk template of an instance.
530

531
  """
532
  templ = inst.disk_template
533
  assert templ is not None
534
  return templ
535

    
536

    
537
def SetInstanceTemplate(inst, template):
538
  """Set the disk template for an instance.
539

540
  """
541
  inst.disk_template = template
542

    
543

    
544
def SetExclusiveStorage(value):
545
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
546

547
  """
548
  return GetConfig().SetExclusiveStorage(value)
549

    
550

    
551
def GetExclusiveStorage():
552
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
553

554
  """
555
  return GetConfig().GetExclusiveStorage()
556

    
557

    
558
def IsTemplateSupported(templ):
559
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
560

561
  """
562
  return GetConfig().IsTemplateSupported(templ)
563

    
564

    
565
def AcquireNode(exclude=None, _cfg=None):
566
  """Returns the least used node.
567

568
  """
569
  if _cfg is None:
570
    cfg = GetConfig()
571
  else:
572
    cfg = _cfg
573

    
574
  master = cfg.GetMasterNode()
575

    
576
  # Filter out unwanted nodes
577
  # TODO: Maybe combine filters
578
  if exclude is None:
579
    nodes = cfg["nodes"][:]
580
  elif isinstance(exclude, (list, tuple)):
581
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
582
  else:
583
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
584

    
585
  nodes = filter(lambda node: node.added or node == master, nodes)
586

    
587
  if not nodes:
588
    raise qa_error.OutOfNodesError("No nodes left")
589

    
590
  # Get node with least number of uses
591
  # TODO: Switch to computing sort key instead of comparing directly
592
  def compare(a, b):
593
    result = cmp(a.use_count, b.use_count)
594
    if result == 0:
595
      result = cmp(a.primary, b.primary)
596
    return result
597

    
598
  nodes.sort(cmp=compare)
599

    
600
  return nodes[0].Use()
601

    
602

    
603
def AcquireManyNodes(num, exclude=None):
604
  """Return the least used nodes.
605

606
  @type num: int
607
  @param num: Number of nodes; can be 0.
608
  @type exclude: list of nodes or C{None}
609
  @param exclude: nodes to be excluded from the choice
610
  @rtype: list of nodes
611
  @return: C{num} different nodes
612

613
  """
614
  nodes = []
615
  if exclude is None:
616
    exclude = []
617
  elif isinstance(exclude, (list, tuple)):
618
    # Don't modify the incoming argument
619
    exclude = list(exclude)
620
  else:
621
    exclude = [exclude]
622

    
623
  try:
624
    for _ in range(0, num):
625
      n = AcquireNode(exclude=exclude)
626
      nodes.append(n)
627
      exclude.append(n)
628
  except qa_error.OutOfNodesError:
629
    ReleaseManyNodes(nodes)
630
    raise
631
  return nodes
632

    
633

    
634
def ReleaseManyNodes(nodes):
635
  for node in nodes:
636
    node.Release()