Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 02a5fe0e

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

    
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 Release(self):
74
    """Releases instance and makes it available again.
75

76
    """
77
    assert self.used, \
78
      ("Instance '%s' was never acquired or released more than once" %
79
       self.name)
80

    
81
    self.used = False
82
    self._disk_template = None
83

    
84
  def GetNicMacAddr(self, idx, default):
85
    """Returns MAC address for NIC.
86

87
    @type idx: int
88
    @param idx: NIC index
89
    @param default: Default value
90

91
    """
92
    if len(self.nicmac) > idx:
93
      return self.nicmac[idx]
94
    else:
95
      return default
96

    
97
  def SetDiskTemplate(self, template):
98
    """Set the disk template.
99

100
    """
101
    assert template in constants.DISK_TEMPLATES
102

    
103
    self._disk_template = template
104

    
105
  @property
106
  def disk_template(self):
107
    """Returns the current disk template.
108

109
    """
110
    return self._disk_template
111

    
112

    
113
class _QaNode(object):
114
  __slots__ = [
115
    "primary",
116
    "secondary",
117
    "_added",
118
    "_use_count",
119
    ]
120

    
121
  def __init__(self, primary, secondary):
122
    """Initializes instances of this class.
123

124
    """
125
    self.primary = primary
126
    self.secondary = secondary
127
    self._added = False
128
    self._use_count = 0
129

    
130
  @classmethod
131
  def FromDict(cls, data):
132
    """Creates node object from JSON dictionary.
133

134
    """
135
    return cls(primary=data["primary"], secondary=data.get("secondary"))
136

    
137
  def Use(self):
138
    """Marks a node as being in use.
139

140
    """
141
    assert self._use_count >= 0
142

    
143
    self._use_count += 1
144

    
145
    return self
146

    
147
  def Release(self):
148
    """Release a node (opposite of L{Use}).
149

150
    """
151
    assert self.use_count > 0
152

    
153
    self._use_count -= 1
154

    
155
  def MarkAdded(self):
156
    """Marks node as having been added to a cluster.
157

158
    """
159
    assert not self._added
160
    self._added = True
161

    
162
  def MarkRemoved(self):
163
    """Marks node as having been removed from a cluster.
164

165
    """
166
    assert self._added
167
    self._added = False
168

    
169
  @property
170
  def added(self):
171
    """Returns whether a node is part of a cluster.
172

173
    """
174
    return self._added
175

    
176
  @property
177
  def use_count(self):
178
    """Returns number of current uses (controlled by L{Use} and L{Release}).
179

180
    """
181
    return self._use_count
182

    
183

    
184
_RESOURCE_CONVERTER = {
185
  "instances": _QaInstance.FromDict,
186
  "nodes": _QaNode.FromDict,
187
  }
188

    
189

    
190
def _ConvertResources((key, value)):
191
  """Converts cluster resources in configuration to Python objects.
192

193
  """
194
  fn = _RESOURCE_CONVERTER.get(key, None)
195
  if fn:
196
    return (key, map(fn, value))
197
  else:
198
    return (key, value)
199

    
200

    
201
class _QaConfig(object):
202
  def __init__(self, data):
203
    """Initializes instances of this class.
204

205
    """
206
    self._data = data
207

    
208
    #: Cluster-wide run-time value of the exclusive storage flag
209
    self._exclusive_storage = None
210

    
211
  @classmethod
212
  def Load(cls, filename):
213
    """Loads a configuration file and produces a configuration object.
214

215
    @type filename: string
216
    @param filename: Path to configuration file
217
    @rtype: L{_QaConfig}
218

219
    """
220
    data = serializer.LoadJson(utils.ReadFile(filename))
221

    
222
    result = cls(dict(map(_ConvertResources,
223
                          data.items()))) # pylint: disable=E1103
224
    result.Validate()
225

    
226
    return result
227

    
228
  def Validate(self):
229
    """Validates loaded configuration data.
230

231
    """
232
    if not self.get("nodes"):
233
      raise qa_error.Error("Need at least one node")
234

    
235
    if not self.get("instances"):
236
      raise qa_error.Error("Need at least one instance")
237

    
238
    if (self.get("disk") is None or
239
        self.get("disk-growth") is None or
240
        len(self.get("disk")) != len(self.get("disk-growth"))):
241
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
242
                           " and have the same number of items")
243

    
244
    check = self.GetInstanceCheckScript()
245
    if check:
246
      try:
247
        os.stat(check)
248
      except EnvironmentError, err:
249
        raise qa_error.Error("Can't find instance check script '%s': %s" %
250
                             (check, err))
251

    
252
    enabled_hv = frozenset(self.GetEnabledHypervisors())
253
    if not enabled_hv:
254
      raise qa_error.Error("No hypervisor is enabled")
255

    
256
    difference = enabled_hv - constants.HYPER_TYPES
257
    if difference:
258
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
259
                           utils.CommaJoin(difference))
260

    
261
  def __getitem__(self, name):
262
    """Returns configuration value.
263

264
    @type name: string
265
    @param name: Name of configuration entry
266

267
    """
268
    return self._data[name]
269

    
270
  def get(self, name, default=None):
271
    """Returns configuration value.
272

273
    @type name: string
274
    @param name: Name of configuration entry
275
    @param default: Default value
276

277
    """
278
    return self._data.get(name, default)
279

    
280
  def GetMasterNode(self):
281
    """Returns the default master node for the cluster.
282

283
    """
284
    return self["nodes"][0]
285

    
286
  def GetInstanceCheckScript(self):
287
    """Returns path to instance check script or C{None}.
288

289
    """
290
    return self._data.get(_INSTANCE_CHECK_KEY, None)
291

    
292
  def GetEnabledHypervisors(self):
293
    """Returns list of enabled hypervisors.
294

295
    @rtype: list
296

297
    """
298
    try:
299
      value = self._data[_ENABLED_HV_KEY]
300
    except KeyError:
301
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
302
    else:
303
      if value is None:
304
        return []
305
      elif isinstance(value, basestring):
306
        # The configuration key ("enabled-hypervisors") implies there can be
307
        # multiple values. Multiple hypervisors are comma-separated on the
308
        # command line option to "gnt-cluster init", so we need to handle them
309
        # equally here.
310
        return value.split(",")
311
      else:
312
        return value
313

    
314
  def GetDefaultHypervisor(self):
315
    """Returns the default hypervisor to be used.
316

317
    """
318
    return self.GetEnabledHypervisors()[0]
319

    
320
  def SetExclusiveStorage(self, value):
321
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
322

323
    """
324
    self._exclusive_storage = bool(value)
325

    
326
  def GetExclusiveStorage(self):
327
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
328

329
    """
330
    value = self._exclusive_storage
331
    assert value is not None
332
    return value
333

    
334
  def IsTemplateSupported(self, templ):
335
    """Is the given disk template supported by the current configuration?
336

337
    """
338
    return (not self.GetExclusiveStorage() or
339
            templ in constants.DTS_EXCL_STORAGE)
340

    
341

    
342
def Load(path):
343
  """Loads the passed configuration file.
344

345
  """
346
  global _config # pylint: disable=W0603
347

    
348
  _config = _QaConfig.Load(path)
349

    
350

    
351
def GetConfig():
352
  """Returns the configuration object.
353

354
  """
355
  if _config is None:
356
    raise RuntimeError("Configuration not yet loaded")
357

    
358
  return _config
359

    
360

    
361
def get(name, default=None):
362
  """Wrapper for L{_QaConfig.get}.
363

364
  """
365
  return GetConfig().get(name, default=default)
366

    
367

    
368
class Either:
369
  def __init__(self, tests):
370
    """Initializes this class.
371

372
    @type tests: list or string
373
    @param tests: List of test names
374
    @see: L{TestEnabled} for details
375

376
    """
377
    self.tests = tests
378

    
379

    
380
def _MakeSequence(value):
381
  """Make sequence of single argument.
382

383
  If the single argument is not already a list or tuple, a list with the
384
  argument as a single item is returned.
385

386
  """
387
  if isinstance(value, (list, tuple)):
388
    return value
389
  else:
390
    return [value]
391

    
392

    
393
def _TestEnabledInner(check_fn, names, fn):
394
  """Evaluate test conditions.
395

396
  @type check_fn: callable
397
  @param check_fn: Callback to check whether a test is enabled
398
  @type names: sequence or string
399
  @param names: Test name(s)
400
  @type fn: callable
401
  @param fn: Aggregation function
402
  @rtype: bool
403
  @return: Whether test is enabled
404

405
  """
406
  names = _MakeSequence(names)
407

    
408
  result = []
409

    
410
  for name in names:
411
    if isinstance(name, Either):
412
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
413
    elif isinstance(name, (list, tuple)):
414
      value = _TestEnabledInner(check_fn, name, compat.all)
415
    else:
416
      value = check_fn(name)
417

    
418
    result.append(value)
419

    
420
  return fn(result)
421

    
422

    
423
def TestEnabled(tests, _cfg=None):
424
  """Returns True if the given tests are enabled.
425

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

429
  """
430
  if _cfg is None:
431
    cfg = GetConfig()
432
  else:
433
    cfg = _cfg
434

    
435
  # Get settings for all tests
436
  cfg_tests = cfg.get("tests", {})
437

    
438
  # Get default setting
439
  default = cfg_tests.get("default", True)
440

    
441
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
442
                           tests, compat.all)
443

    
444

    
445
def GetInstanceCheckScript(*args):
446
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
447

448
  """
449
  return GetConfig().GetInstanceCheckScript(*args)
450

    
451

    
452
def GetEnabledHypervisors(*args):
453
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
454

455
  """
456
  return GetConfig().GetEnabledHypervisors(*args)
457

    
458

    
459
def GetDefaultHypervisor(*args):
460
  """Wrapper for L{_QaConfig.GetDefaultHypervisor}.
461

462
  """
463
  return GetConfig().GetDefaultHypervisor(*args)
464

    
465

    
466
def GetMasterNode():
467
  """Wrapper for L{_QaConfig.GetMasterNode}.
468

469
  """
470
  return GetConfig().GetMasterNode()
471

    
472

    
473
def AcquireInstance(_cfg=None):
474
  """Returns an instance which isn't in use.
475

476
  """
477
  if _cfg is None:
478
    cfg = GetConfig()
479
  else:
480
    cfg = _cfg
481

    
482
  # Filter out unwanted instances
483
  instances = filter(lambda inst: not inst.used, cfg["instances"])
484

    
485
  if not instances:
486
    raise qa_error.OutOfInstancesError("No instances left")
487

    
488
  inst = instances[0]
489

    
490
  assert not inst.used
491
  assert inst.disk_template is None
492

    
493
  inst.used = True
494

    
495
  return inst
496

    
497

    
498
def SetExclusiveStorage(value):
499
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
500

501
  """
502
  return GetConfig().SetExclusiveStorage(value)
503

    
504

    
505
def GetExclusiveStorage():
506
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
507

508
  """
509
  return GetConfig().GetExclusiveStorage()
510

    
511

    
512
def IsTemplateSupported(templ):
513
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
514

515
  """
516
  return GetConfig().IsTemplateSupported(templ)
517

    
518

    
519
def AcquireNode(exclude=None, _cfg=None):
520
  """Returns the least used node.
521

522
  """
523
  if _cfg is None:
524
    cfg = GetConfig()
525
  else:
526
    cfg = _cfg
527

    
528
  master = cfg.GetMasterNode()
529

    
530
  # Filter out unwanted nodes
531
  # TODO: Maybe combine filters
532
  if exclude is None:
533
    nodes = cfg["nodes"][:]
534
  elif isinstance(exclude, (list, tuple)):
535
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
536
  else:
537
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
538

    
539
  nodes = filter(lambda node: node.added or node == master, nodes)
540

    
541
  if not nodes:
542
    raise qa_error.OutOfNodesError("No nodes left")
543

    
544
  # Get node with least number of uses
545
  # TODO: Switch to computing sort key instead of comparing directly
546
  def compare(a, b):
547
    result = cmp(a.use_count, b.use_count)
548
    if result == 0:
549
      result = cmp(a.primary, b.primary)
550
    return result
551

    
552
  nodes.sort(cmp=compare)
553

    
554
  return nodes[0].Use()
555

    
556

    
557
def AcquireManyNodes(num, exclude=None):
558
  """Return the least used nodes.
559

560
  @type num: int
561
  @param num: Number of nodes; can be 0.
562
  @type exclude: list of nodes or C{None}
563
  @param exclude: nodes to be excluded from the choice
564
  @rtype: list of nodes
565
  @return: C{num} different nodes
566

567
  """
568
  nodes = []
569
  if exclude is None:
570
    exclude = []
571
  elif isinstance(exclude, (list, tuple)):
572
    # Don't modify the incoming argument
573
    exclude = list(exclude)
574
  else:
575
    exclude = [exclude]
576

    
577
  try:
578
    for _ in range(0, num):
579
      n = AcquireNode(exclude=exclude)
580
      nodes.append(n)
581
      exclude.append(n)
582
  except qa_error.OutOfNodesError:
583
    ReleaseManyNodes(nodes)
584
    raise
585
  return nodes
586

    
587

    
588
def ReleaseManyNodes(nodes):
589
  for node in nodes:
590
    node.Release()