Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 6a654276

History | View | Annotate | Download (11.9 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 GetNicMacAddr(self, idx, default):
92
    """Returns MAC address for NIC.
93

94
    @type idx: int
95
    @param idx: NIC index
96
    @param default: Default value
97

98
    """
99
    if len(self.nicmac) > idx:
100
      return self.nicmac[idx]
101
    else:
102
      return default
103

    
104

    
105
_RESOURCE_CONVERTER = {
106
  "instances": _QaInstance.FromDict,
107
  }
108

    
109

    
110
def _ConvertResources((key, value)):
111
  """Converts cluster resources in configuration to Python objects.
112

113
  """
114
  fn = _RESOURCE_CONVERTER.get(key, None)
115
  if fn:
116
    return (key, map(fn, value))
117
  else:
118
    return (key, value)
119

    
120

    
121
class _QaConfig(object):
122
  def __init__(self, data):
123
    """Initializes instances of this class.
124

125
    """
126
    self._data = data
127

    
128
    #: Cluster-wide run-time value of the exclusive storage flag
129
    self._exclusive_storage = None
130

    
131
  @classmethod
132
  def Load(cls, filename):
133
    """Loads a configuration file and produces a configuration object.
134

135
    @type filename: string
136
    @param filename: Path to configuration file
137
    @rtype: L{_QaConfig}
138

139
    """
140
    data = serializer.LoadJson(utils.ReadFile(filename))
141

    
142
    result = cls(dict(map(_ConvertResources,
143
                          data.items()))) # pylint: disable=E1103
144
    result.Validate()
145

    
146
    return result
147

    
148
  def Validate(self):
149
    """Validates loaded configuration data.
150

151
    """
152
    if not self.get("nodes"):
153
      raise qa_error.Error("Need at least one node")
154

    
155
    if not self.get("instances"):
156
      raise qa_error.Error("Need at least one instance")
157

    
158
    if (self.get("disk") is None or
159
        self.get("disk-growth") is None or
160
        len(self.get("disk")) != len(self.get("disk-growth"))):
161
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
162
                           " and have the same number of items")
163

    
164
    check = self.GetInstanceCheckScript()
165
    if check:
166
      try:
167
        os.stat(check)
168
      except EnvironmentError, err:
169
        raise qa_error.Error("Can't find instance check script '%s': %s" %
170
                             (check, err))
171

    
172
    enabled_hv = frozenset(self.GetEnabledHypervisors())
173
    if not enabled_hv:
174
      raise qa_error.Error("No hypervisor is enabled")
175

    
176
    difference = enabled_hv - constants.HYPER_TYPES
177
    if difference:
178
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
179
                           utils.CommaJoin(difference))
180

    
181
  def __getitem__(self, name):
182
    """Returns configuration value.
183

184
    @type name: string
185
    @param name: Name of configuration entry
186

187
    """
188
    return self._data[name]
189

    
190
  def get(self, name, default=None):
191
    """Returns configuration value.
192

193
    @type name: string
194
    @param name: Name of configuration entry
195
    @param default: Default value
196

197
    """
198
    return self._data.get(name, default)
199

    
200
  def GetMasterNode(self):
201
    """Returns the default master node for the cluster.
202

203
    """
204
    return self["nodes"][0]
205

    
206
  def GetInstanceCheckScript(self):
207
    """Returns path to instance check script or C{None}.
208

209
    """
210
    return self._data.get(_INSTANCE_CHECK_KEY, None)
211

    
212
  def GetEnabledHypervisors(self):
213
    """Returns list of enabled hypervisors.
214

215
    @rtype: list
216

217
    """
218
    try:
219
      value = self._data[_ENABLED_HV_KEY]
220
    except KeyError:
221
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
222
    else:
223
      if value is None:
224
        return []
225
      elif isinstance(value, basestring):
226
        # The configuration key ("enabled-hypervisors") implies there can be
227
        # multiple values. Multiple hypervisors are comma-separated on the
228
        # command line option to "gnt-cluster init", so we need to handle them
229
        # equally here.
230
        return value.split(",")
231
      else:
232
        return value
233

    
234
  def GetDefaultHypervisor(self):
235
    """Returns the default hypervisor to be used.
236

237
    """
238
    return self.GetEnabledHypervisors()[0]
239

    
240
  def SetExclusiveStorage(self, value):
241
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
242

243
    """
244
    self._exclusive_storage = bool(value)
245

    
246
  def GetExclusiveStorage(self):
247
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
248

249
    """
250
    value = self._exclusive_storage
251
    assert value is not None
252
    return value
253

    
254
  def IsTemplateSupported(self, templ):
255
    """Is the given disk template supported by the current configuration?
256

257
    """
258
    return (not self.GetExclusiveStorage() or
259
            templ in constants.DTS_EXCL_STORAGE)
260

    
261

    
262
def Load(path):
263
  """Loads the passed configuration file.
264

265
  """
266
  global _config # pylint: disable=W0603
267

    
268
  _config = _QaConfig.Load(path)
269

    
270

    
271
def GetConfig():
272
  """Returns the configuration object.
273

274
  """
275
  if _config is None:
276
    raise RuntimeError("Configuration not yet loaded")
277

    
278
  return _config
279

    
280

    
281
def get(name, default=None):
282
  """Wrapper for L{_QaConfig.get}.
283

284
  """
285
  return GetConfig().get(name, default=default)
286

    
287

    
288
class Either:
289
  def __init__(self, tests):
290
    """Initializes this class.
291

292
    @type tests: list or string
293
    @param tests: List of test names
294
    @see: L{TestEnabled} for details
295

296
    """
297
    self.tests = tests
298

    
299

    
300
def _MakeSequence(value):
301
  """Make sequence of single argument.
302

303
  If the single argument is not already a list or tuple, a list with the
304
  argument as a single item is returned.
305

306
  """
307
  if isinstance(value, (list, tuple)):
308
    return value
309
  else:
310
    return [value]
311

    
312

    
313
def _TestEnabledInner(check_fn, names, fn):
314
  """Evaluate test conditions.
315

316
  @type check_fn: callable
317
  @param check_fn: Callback to check whether a test is enabled
318
  @type names: sequence or string
319
  @param names: Test name(s)
320
  @type fn: callable
321
  @param fn: Aggregation function
322
  @rtype: bool
323
  @return: Whether test is enabled
324

325
  """
326
  names = _MakeSequence(names)
327

    
328
  result = []
329

    
330
  for name in names:
331
    if isinstance(name, Either):
332
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
333
    elif isinstance(name, (list, tuple)):
334
      value = _TestEnabledInner(check_fn, name, compat.all)
335
    else:
336
      value = check_fn(name)
337

    
338
    result.append(value)
339

    
340
  return fn(result)
341

    
342

    
343
def TestEnabled(tests, _cfg=None):
344
  """Returns True if the given tests are enabled.
345

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

349
  """
350
  if _cfg is None:
351
    cfg = GetConfig()
352
  else:
353
    cfg = _cfg
354

    
355
  # Get settings for all tests
356
  cfg_tests = cfg.get("tests", {})
357

    
358
  # Get default setting
359
  default = cfg_tests.get("default", True)
360

    
361
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
362
                           tests, compat.all)
363

    
364

    
365
def GetInstanceCheckScript(*args):
366
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
367

368
  """
369
  return GetConfig().GetInstanceCheckScript(*args)
370

    
371

    
372
def GetEnabledHypervisors(*args):
373
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
374

375
  """
376
  return GetConfig().GetEnabledHypervisors(*args)
377

    
378

    
379
def GetDefaultHypervisor(*args):
380
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
381

382
  """
383
  return GetConfig().GetDefaultHypervisor(*args)
384

    
385

    
386
def GetInstanceNicMac(inst, default=None):
387
  """Returns MAC address for instance's network interface.
388

389
  """
390
  return inst.GetNicMacAddr(0, default)
391

    
392

    
393
def GetMasterNode():
394
  """Wrapper for L{_QaConfig.GetMasterNode}.
395

396
  """
397
  return GetConfig().GetMasterNode()
398

    
399

    
400
def AcquireInstance(_cfg=None):
401
  """Returns an instance which isn't in use.
402

403
  """
404
  if _cfg is None:
405
    cfg = GetConfig()
406
  else:
407
    cfg = _cfg
408

    
409
  # Filter out unwanted instances
410
  instances = filter(lambda inst: not inst.used, cfg["instances"])
411

    
412
  if not instances:
413
    raise qa_error.OutOfInstancesError("No instances left")
414

    
415
  inst = instances[0]
416

    
417
  assert not inst.used
418
  assert inst.disk_template is None
419

    
420
  inst.used = True
421

    
422
  return inst
423

    
424

    
425
def ReleaseInstance(inst):
426
  inst.used = False
427
  inst.disk_template = None
428

    
429

    
430
def GetInstanceTemplate(inst):
431
  """Return the disk template of an instance.
432

433
  """
434
  templ = inst.disk_template
435
  assert templ is not None
436
  return templ
437

    
438

    
439
def SetInstanceTemplate(inst, template):
440
  """Set the disk template for an instance.
441

442
  """
443
  inst.disk_template = template
444

    
445

    
446
def SetExclusiveStorage(value):
447
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
448

449
  """
450
  return GetConfig().SetExclusiveStorage(value)
451

    
452

    
453
def GetExclusiveStorage():
454
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
455

456
  """
457
  return GetConfig().GetExclusiveStorage()
458

    
459

    
460
def IsTemplateSupported(templ):
461
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
462

463
  """
464
  return GetConfig().IsTemplateSupported(templ)
465

    
466

    
467
def AcquireNode(exclude=None):
468
  """Returns the least used node.
469

470
  """
471
  master = GetMasterNode()
472
  cfg = GetConfig()
473

    
474
  # Filter out unwanted nodes
475
  # TODO: Maybe combine filters
476
  if exclude is None:
477
    nodes = cfg["nodes"][:]
478
  elif isinstance(exclude, (list, tuple)):
479
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
480
  else:
481
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
482

    
483
  tmp_flt = lambda node: node.get("_added", False) or node == master
484
  nodes = filter(tmp_flt, nodes)
485
  del tmp_flt
486

    
487
  if len(nodes) == 0:
488
    raise qa_error.OutOfNodesError("No nodes left")
489

    
490
  # Get node with least number of uses
491
  def compare(a, b):
492
    result = cmp(a.get("_count", 0), b.get("_count", 0))
493
    if result == 0:
494
      result = cmp(a["primary"], b["primary"])
495
    return result
496

    
497
  nodes.sort(cmp=compare)
498

    
499
  node = nodes[0]
500
  node["_count"] = node.get("_count", 0) + 1
501
  return node
502

    
503

    
504
def AcquireManyNodes(num, exclude=None):
505
  """Return the least used nodes.
506

507
  @type num: int
508
  @param num: Number of nodes; can be 0.
509
  @type exclude: list of nodes or C{None}
510
  @param exclude: nodes to be excluded from the choice
511
  @rtype: list of nodes
512
  @return: C{num} different nodes
513

514
  """
515
  nodes = []
516
  if exclude is None:
517
    exclude = []
518
  elif isinstance(exclude, (list, tuple)):
519
    # Don't modify the incoming argument
520
    exclude = list(exclude)
521
  else:
522
    exclude = [exclude]
523

    
524
  try:
525
    for _ in range(0, num):
526
      n = AcquireNode(exclude=exclude)
527
      nodes.append(n)
528
      exclude.append(n)
529
  except qa_error.OutOfNodesError:
530
    ReleaseManyNodes(nodes)
531
    raise
532
  return nodes
533

    
534

    
535
def ReleaseNode(node):
536
  node["_count"] = node.get("_count", 0) - 1
537

    
538

    
539
def ReleaseManyNodes(nodes):
540
  for n in nodes:
541
    ReleaseNode(n)