Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 6f88e076

History | View | Annotate | Download (12 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
_RESOURCE_CONVERTER = {
117
  "instances": _QaInstance.FromDict,
118
  }
119

    
120

    
121
def _ConvertResources((key, value)):
122
  """Converts cluster resources in configuration to Python objects.
123

124
  """
125
  fn = _RESOURCE_CONVERTER.get(key, None)
126
  if fn:
127
    return (key, map(fn, value))
128
  else:
129
    return (key, value)
130

    
131

    
132
class _QaConfig(object):
133
  def __init__(self, data):
134
    """Initializes instances of this class.
135

136
    """
137
    self._data = data
138

    
139
    #: Cluster-wide run-time value of the exclusive storage flag
140
    self._exclusive_storage = None
141

    
142
  @classmethod
143
  def Load(cls, filename):
144
    """Loads a configuration file and produces a configuration object.
145

146
    @type filename: string
147
    @param filename: Path to configuration file
148
    @rtype: L{_QaConfig}
149

150
    """
151
    data = serializer.LoadJson(utils.ReadFile(filename))
152

    
153
    result = cls(dict(map(_ConvertResources,
154
                          data.items()))) # pylint: disable=E1103
155
    result.Validate()
156

    
157
    return result
158

    
159
  def Validate(self):
160
    """Validates loaded configuration data.
161

162
    """
163
    if not self.get("nodes"):
164
      raise qa_error.Error("Need at least one node")
165

    
166
    if not self.get("instances"):
167
      raise qa_error.Error("Need at least one instance")
168

    
169
    if (self.get("disk") is None or
170
        self.get("disk-growth") is None or
171
        len(self.get("disk")) != len(self.get("disk-growth"))):
172
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
173
                           " and have the same number of items")
174

    
175
    check = self.GetInstanceCheckScript()
176
    if check:
177
      try:
178
        os.stat(check)
179
      except EnvironmentError, err:
180
        raise qa_error.Error("Can't find instance check script '%s': %s" %
181
                             (check, err))
182

    
183
    enabled_hv = frozenset(self.GetEnabledHypervisors())
184
    if not enabled_hv:
185
      raise qa_error.Error("No hypervisor is enabled")
186

    
187
    difference = enabled_hv - constants.HYPER_TYPES
188
    if difference:
189
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
190
                           utils.CommaJoin(difference))
191

    
192
  def __getitem__(self, name):
193
    """Returns configuration value.
194

195
    @type name: string
196
    @param name: Name of configuration entry
197

198
    """
199
    return self._data[name]
200

    
201
  def get(self, name, default=None):
202
    """Returns configuration value.
203

204
    @type name: string
205
    @param name: Name of configuration entry
206
    @param default: Default value
207

208
    """
209
    return self._data.get(name, default)
210

    
211
  def GetMasterNode(self):
212
    """Returns the default master node for the cluster.
213

214
    """
215
    return self["nodes"][0]
216

    
217
  def GetInstanceCheckScript(self):
218
    """Returns path to instance check script or C{None}.
219

220
    """
221
    return self._data.get(_INSTANCE_CHECK_KEY, None)
222

    
223
  def GetEnabledHypervisors(self):
224
    """Returns list of enabled hypervisors.
225

226
    @rtype: list
227

228
    """
229
    try:
230
      value = self._data[_ENABLED_HV_KEY]
231
    except KeyError:
232
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
233
    else:
234
      if value is None:
235
        return []
236
      elif isinstance(value, basestring):
237
        # The configuration key ("enabled-hypervisors") implies there can be
238
        # multiple values. Multiple hypervisors are comma-separated on the
239
        # command line option to "gnt-cluster init", so we need to handle them
240
        # equally here.
241
        return value.split(",")
242
      else:
243
        return value
244

    
245
  def GetDefaultHypervisor(self):
246
    """Returns the default hypervisor to be used.
247

248
    """
249
    return self.GetEnabledHypervisors()[0]
250

    
251
  def SetExclusiveStorage(self, value):
252
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
253

254
    """
255
    self._exclusive_storage = bool(value)
256

    
257
  def GetExclusiveStorage(self):
258
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
259

260
    """
261
    value = self._exclusive_storage
262
    assert value is not None
263
    return value
264

    
265
  def IsTemplateSupported(self, templ):
266
    """Is the given disk template supported by the current configuration?
267

268
    """
269
    return (not self.GetExclusiveStorage() or
270
            templ in constants.DTS_EXCL_STORAGE)
271

    
272

    
273
def Load(path):
274
  """Loads the passed configuration file.
275

276
  """
277
  global _config # pylint: disable=W0603
278

    
279
  _config = _QaConfig.Load(path)
280

    
281

    
282
def GetConfig():
283
  """Returns the configuration object.
284

285
  """
286
  if _config is None:
287
    raise RuntimeError("Configuration not yet loaded")
288

    
289
  return _config
290

    
291

    
292
def get(name, default=None):
293
  """Wrapper for L{_QaConfig.get}.
294

295
  """
296
  return GetConfig().get(name, default=default)
297

    
298

    
299
class Either:
300
  def __init__(self, tests):
301
    """Initializes this class.
302

303
    @type tests: list or string
304
    @param tests: List of test names
305
    @see: L{TestEnabled} for details
306

307
    """
308
    self.tests = tests
309

    
310

    
311
def _MakeSequence(value):
312
  """Make sequence of single argument.
313

314
  If the single argument is not already a list or tuple, a list with the
315
  argument as a single item is returned.
316

317
  """
318
  if isinstance(value, (list, tuple)):
319
    return value
320
  else:
321
    return [value]
322

    
323

    
324
def _TestEnabledInner(check_fn, names, fn):
325
  """Evaluate test conditions.
326

327
  @type check_fn: callable
328
  @param check_fn: Callback to check whether a test is enabled
329
  @type names: sequence or string
330
  @param names: Test name(s)
331
  @type fn: callable
332
  @param fn: Aggregation function
333
  @rtype: bool
334
  @return: Whether test is enabled
335

336
  """
337
  names = _MakeSequence(names)
338

    
339
  result = []
340

    
341
  for name in names:
342
    if isinstance(name, Either):
343
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
344
    elif isinstance(name, (list, tuple)):
345
      value = _TestEnabledInner(check_fn, name, compat.all)
346
    else:
347
      value = check_fn(name)
348

    
349
    result.append(value)
350

    
351
  return fn(result)
352

    
353

    
354
def TestEnabled(tests, _cfg=None):
355
  """Returns True if the given tests are enabled.
356

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

360
  """
361
  if _cfg is None:
362
    cfg = GetConfig()
363
  else:
364
    cfg = _cfg
365

    
366
  # Get settings for all tests
367
  cfg_tests = cfg.get("tests", {})
368

    
369
  # Get default setting
370
  default = cfg_tests.get("default", True)
371

    
372
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
373
                           tests, compat.all)
374

    
375

    
376
def GetInstanceCheckScript(*args):
377
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
378

379
  """
380
  return GetConfig().GetInstanceCheckScript(*args)
381

    
382

    
383
def GetEnabledHypervisors(*args):
384
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
385

386
  """
387
  return GetConfig().GetEnabledHypervisors(*args)
388

    
389

    
390
def GetDefaultHypervisor(*args):
391
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
392

393
  """
394
  return GetConfig().GetDefaultHypervisor(*args)
395

    
396

    
397
def GetInstanceNicMac(inst, default=None):
398
  """Returns MAC address for instance's network interface.
399

400
  """
401
  return inst.GetNicMacAddr(0, default)
402

    
403

    
404
def GetMasterNode():
405
  """Wrapper for L{_QaConfig.GetMasterNode}.
406

407
  """
408
  return GetConfig().GetMasterNode()
409

    
410

    
411
def AcquireInstance(_cfg=None):
412
  """Returns an instance which isn't in use.
413

414
  """
415
  if _cfg is None:
416
    cfg = GetConfig()
417
  else:
418
    cfg = _cfg
419

    
420
  # Filter out unwanted instances
421
  instances = filter(lambda inst: not inst.used, cfg["instances"])
422

    
423
  if not instances:
424
    raise qa_error.OutOfInstancesError("No instances left")
425

    
426
  inst = instances[0]
427

    
428
  assert not inst.used
429
  assert inst.disk_template is None
430

    
431
  inst.used = True
432

    
433
  return inst
434

    
435

    
436
def GetInstanceTemplate(inst):
437
  """Return the disk template of an instance.
438

439
  """
440
  templ = inst.disk_template
441
  assert templ is not None
442
  return templ
443

    
444

    
445
def SetInstanceTemplate(inst, template):
446
  """Set the disk template for an instance.
447

448
  """
449
  inst.disk_template = template
450

    
451

    
452
def SetExclusiveStorage(value):
453
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
454

455
  """
456
  return GetConfig().SetExclusiveStorage(value)
457

    
458

    
459
def GetExclusiveStorage():
460
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
461

462
  """
463
  return GetConfig().GetExclusiveStorage()
464

    
465

    
466
def IsTemplateSupported(templ):
467
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
468

469
  """
470
  return GetConfig().IsTemplateSupported(templ)
471

    
472

    
473
def AcquireNode(exclude=None):
474
  """Returns the least used node.
475

476
  """
477
  master = GetMasterNode()
478
  cfg = GetConfig()
479

    
480
  # Filter out unwanted nodes
481
  # TODO: Maybe combine filters
482
  if exclude is None:
483
    nodes = cfg["nodes"][:]
484
  elif isinstance(exclude, (list, tuple)):
485
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
486
  else:
487
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
488

    
489
  tmp_flt = lambda node: node.get("_added", False) or node == master
490
  nodes = filter(tmp_flt, nodes)
491
  del tmp_flt
492

    
493
  if len(nodes) == 0:
494
    raise qa_error.OutOfNodesError("No nodes left")
495

    
496
  # Get node with least number of uses
497
  def compare(a, b):
498
    result = cmp(a.get("_count", 0), b.get("_count", 0))
499
    if result == 0:
500
      result = cmp(a["primary"], b["primary"])
501
    return result
502

    
503
  nodes.sort(cmp=compare)
504

    
505
  node = nodes[0]
506
  node["_count"] = node.get("_count", 0) + 1
507
  return node
508

    
509

    
510
def AcquireManyNodes(num, exclude=None):
511
  """Return the least used nodes.
512

513
  @type num: int
514
  @param num: Number of nodes; can be 0.
515
  @type exclude: list of nodes or C{None}
516
  @param exclude: nodes to be excluded from the choice
517
  @rtype: list of nodes
518
  @return: C{num} different nodes
519

520
  """
521
  nodes = []
522
  if exclude is None:
523
    exclude = []
524
  elif isinstance(exclude, (list, tuple)):
525
    # Don't modify the incoming argument
526
    exclude = list(exclude)
527
  else:
528
    exclude = [exclude]
529

    
530
  try:
531
    for _ in range(0, num):
532
      n = AcquireNode(exclude=exclude)
533
      nodes.append(n)
534
      exclude.append(n)
535
  except qa_error.OutOfNodesError:
536
    ReleaseManyNodes(nodes)
537
    raise
538
  return nodes
539

    
540

    
541
def ReleaseNode(node):
542
  node["_count"] = node.get("_count", 0) - 1
543

    
544

    
545
def ReleaseManyNodes(nodes):
546
  for n in nodes:
547
    ReleaseNode(n)