Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ 99f5fa44

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

    
98
class _QaNode(object):
99
  __slots__ = [
100
    "primary",
101
    "secondary",
102
    "_added",
103
    "_use_count",
104
    ]
105

    
106
  def __init__(self, primary, secondary):
107
    """Initializes instances of this class.
108

109
    """
110
    self.primary = primary
111
    self.secondary = secondary
112
    self._added = False
113
    self._use_count = 0
114

    
115
  @classmethod
116
  def FromDict(cls, data):
117
    """Creates node object from JSON dictionary.
118

119
    """
120
    return cls(primary=data["primary"], secondary=data.get("secondary"))
121

    
122
  def Use(self):
123
    """Marks a node as being in use.
124

125
    """
126
    assert self._use_count >= 0
127

    
128
    self._use_count += 1
129

    
130
    return self
131

    
132
  def Release(self):
133
    """Release a node (opposite of L{Use}).
134

135
    """
136
    assert self.use_count > 0
137

    
138
    self._use_count -= 1
139

    
140
  def MarkAdded(self):
141
    """Marks node as having been added to a cluster.
142

143
    """
144
    assert not self._added
145
    self._added = True
146

    
147
  def MarkRemoved(self):
148
    """Marks node as having been removed from a cluster.
149

150
    """
151
    assert self._added
152
    self._added = False
153

    
154
  @property
155
  def added(self):
156
    """Returns whether a node is part of a cluster.
157

158
    """
159
    return self._added
160

    
161
  @property
162
  def use_count(self):
163
    """Returns number of current uses (controlled by L{Use} and L{Release}).
164

165
    """
166
    return self._use_count
167

    
168

    
169
_RESOURCE_CONVERTER = {
170
  "instances": _QaInstance.FromDict,
171
  "nodes": _QaNode.FromDict,
172
  }
173

    
174

    
175
def _ConvertResources((key, value)):
176
  """Converts cluster resources in configuration to Python objects.
177

178
  """
179
  fn = _RESOURCE_CONVERTER.get(key, None)
180
  if fn:
181
    return (key, map(fn, value))
182
  else:
183
    return (key, value)
184

    
185

    
186
class _QaConfig(object):
187
  def __init__(self, data):
188
    """Initializes instances of this class.
189

190
    """
191
    self._data = data
192

    
193
    #: Cluster-wide run-time value of the exclusive storage flag
194
    self._exclusive_storage = None
195

    
196
  @classmethod
197
  def Load(cls, filename):
198
    """Loads a configuration file and produces a configuration object.
199

200
    @type filename: string
201
    @param filename: Path to configuration file
202
    @rtype: L{_QaConfig}
203

204
    """
205
    data = serializer.LoadJson(utils.ReadFile(filename))
206

    
207
    result = cls(dict(map(_ConvertResources,
208
                          data.items()))) # pylint: disable=E1103
209
    result.Validate()
210

    
211
    return result
212

    
213
  def Validate(self):
214
    """Validates loaded configuration data.
215

216
    """
217
    if not self.get("nodes"):
218
      raise qa_error.Error("Need at least one node")
219

    
220
    if not self.get("instances"):
221
      raise qa_error.Error("Need at least one instance")
222

    
223
    if (self.get("disk") is None or
224
        self.get("disk-growth") is None or
225
        len(self.get("disk")) != len(self.get("disk-growth"))):
226
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
227
                           " and have the same number of items")
228

    
229
    check = self.GetInstanceCheckScript()
230
    if check:
231
      try:
232
        os.stat(check)
233
      except EnvironmentError, err:
234
        raise qa_error.Error("Can't find instance check script '%s': %s" %
235
                             (check, err))
236

    
237
    enabled_hv = frozenset(self.GetEnabledHypervisors())
238
    if not enabled_hv:
239
      raise qa_error.Error("No hypervisor is enabled")
240

    
241
    difference = enabled_hv - constants.HYPER_TYPES
242
    if difference:
243
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
244
                           utils.CommaJoin(difference))
245

    
246
  def __getitem__(self, name):
247
    """Returns configuration value.
248

249
    @type name: string
250
    @param name: Name of configuration entry
251

252
    """
253
    return self._data[name]
254

    
255
  def get(self, name, default=None):
256
    """Returns configuration value.
257

258
    @type name: string
259
    @param name: Name of configuration entry
260
    @param default: Default value
261

262
    """
263
    return self._data.get(name, default)
264

    
265
  def GetMasterNode(self):
266
    """Returns the default master node for the cluster.
267

268
    """
269
    return self["nodes"][0]
270

    
271
  def GetInstanceCheckScript(self):
272
    """Returns path to instance check script or C{None}.
273

274
    """
275
    return self._data.get(_INSTANCE_CHECK_KEY, None)
276

    
277
  def GetEnabledHypervisors(self):
278
    """Returns list of enabled hypervisors.
279

280
    @rtype: list
281

282
    """
283
    try:
284
      value = self._data[_ENABLED_HV_KEY]
285
    except KeyError:
286
      return [constants.DEFAULT_ENABLED_HYPERVISOR]
287
    else:
288
      if value is None:
289
        return []
290
      elif isinstance(value, basestring):
291
        # The configuration key ("enabled-hypervisors") implies there can be
292
        # multiple values. Multiple hypervisors are comma-separated on the
293
        # command line option to "gnt-cluster init", so we need to handle them
294
        # equally here.
295
        return value.split(",")
296
      else:
297
        return value
298

    
299
  def GetDefaultHypervisor(self):
300
    """Returns the default hypervisor to be used.
301

302
    """
303
    return self.GetEnabledHypervisors()[0]
304

    
305
  def SetExclusiveStorage(self, value):
306
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
307

308
    """
309
    self._exclusive_storage = bool(value)
310

    
311
  def GetExclusiveStorage(self):
312
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
313

314
    """
315
    value = self._exclusive_storage
316
    assert value is not None
317
    return value
318

    
319
  def IsTemplateSupported(self, templ):
320
    """Is the given disk template supported by the current configuration?
321

322
    """
323
    return (not self.GetExclusiveStorage() or
324
            templ in constants.DTS_EXCL_STORAGE)
325

    
326

    
327
def Load(path):
328
  """Loads the passed configuration file.
329

330
  """
331
  global _config # pylint: disable=W0603
332

    
333
  _config = _QaConfig.Load(path)
334

    
335

    
336
def GetConfig():
337
  """Returns the configuration object.
338

339
  """
340
  if _config is None:
341
    raise RuntimeError("Configuration not yet loaded")
342

    
343
  return _config
344

    
345

    
346
def get(name, default=None):
347
  """Wrapper for L{_QaConfig.get}.
348

349
  """
350
  return GetConfig().get(name, default=default)
351

    
352

    
353
class Either:
354
  def __init__(self, tests):
355
    """Initializes this class.
356

357
    @type tests: list or string
358
    @param tests: List of test names
359
    @see: L{TestEnabled} for details
360

361
    """
362
    self.tests = tests
363

    
364

    
365
def _MakeSequence(value):
366
  """Make sequence of single argument.
367

368
  If the single argument is not already a list or tuple, a list with the
369
  argument as a single item is returned.
370

371
  """
372
  if isinstance(value, (list, tuple)):
373
    return value
374
  else:
375
    return [value]
376

    
377

    
378
def _TestEnabledInner(check_fn, names, fn):
379
  """Evaluate test conditions.
380

381
  @type check_fn: callable
382
  @param check_fn: Callback to check whether a test is enabled
383
  @type names: sequence or string
384
  @param names: Test name(s)
385
  @type fn: callable
386
  @param fn: Aggregation function
387
  @rtype: bool
388
  @return: Whether test is enabled
389

390
  """
391
  names = _MakeSequence(names)
392

    
393
  result = []
394

    
395
  for name in names:
396
    if isinstance(name, Either):
397
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
398
    elif isinstance(name, (list, tuple)):
399
      value = _TestEnabledInner(check_fn, name, compat.all)
400
    else:
401
      value = check_fn(name)
402

    
403
    result.append(value)
404

    
405
  return fn(result)
406

    
407

    
408
def TestEnabled(tests, _cfg=None):
409
  """Returns True if the given tests are enabled.
410

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

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

    
420
  # Get settings for all tests
421
  cfg_tests = cfg.get("tests", {})
422

    
423
  # Get default setting
424
  default = cfg_tests.get("default", True)
425

    
426
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
427
                           tests, compat.all)
428

    
429

    
430
def GetInstanceCheckScript(*args):
431
  """Wrapper for L{_QaConfig.GetInstanceCheckScript}.
432

433
  """
434
  return GetConfig().GetInstanceCheckScript(*args)
435

    
436

    
437
def GetEnabledHypervisors(*args):
438
  """Wrapper for L{_QaConfig.GetEnabledHypervisors}.
439

440
  """
441
  return GetConfig().GetEnabledHypervisors(*args)
442

    
443

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

447
  """
448
  return GetConfig().GetDefaultHypervisor(*args)
449

    
450

    
451
def GetInstanceNicMac(inst, default=None):
452
  """Returns MAC address for instance's network interface.
453

454
  """
455
  return inst.GetNicMacAddr(0, default)
456

    
457

    
458
def GetMasterNode():
459
  """Wrapper for L{_QaConfig.GetMasterNode}.
460

461
  """
462
  return GetConfig().GetMasterNode()
463

    
464

    
465
def AcquireInstance(_cfg=None):
466
  """Returns an instance which isn't in use.
467

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

    
474
  # Filter out unwanted instances
475
  instances = filter(lambda inst: not inst.used, cfg["instances"])
476

    
477
  if not instances:
478
    raise qa_error.OutOfInstancesError("No instances left")
479

    
480
  inst = instances[0]
481

    
482
  assert not inst.used
483
  assert inst.disk_template is None
484

    
485
  inst.used = True
486

    
487
  return inst
488

    
489

    
490
def GetInstanceTemplate(inst):
491
  """Return the disk template of an instance.
492

493
  """
494
  templ = inst.disk_template
495
  assert templ is not None
496
  return templ
497

    
498

    
499
def SetInstanceTemplate(inst, template):
500
  """Set the disk template for an instance.
501

502
  """
503
  inst.disk_template = template
504

    
505

    
506
def SetExclusiveStorage(value):
507
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
508

509
  """
510
  return GetConfig().SetExclusiveStorage(value)
511

    
512

    
513
def GetExclusiveStorage():
514
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
515

516
  """
517
  return GetConfig().GetExclusiveStorage()
518

    
519

    
520
def IsTemplateSupported(templ):
521
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
522

523
  """
524
  return GetConfig().IsTemplateSupported(templ)
525

    
526

    
527
def AcquireNode(exclude=None, _cfg=None):
528
  """Returns the least used node.
529

530
  """
531
  if _cfg is None:
532
    cfg = GetConfig()
533
  else:
534
    cfg = _cfg
535

    
536
  master = cfg.GetMasterNode()
537

    
538
  # Filter out unwanted nodes
539
  # TODO: Maybe combine filters
540
  if exclude is None:
541
    nodes = cfg["nodes"][:]
542
  elif isinstance(exclude, (list, tuple)):
543
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
544
  else:
545
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
546

    
547
  nodes = filter(lambda node: node.added or node == master, nodes)
548

    
549
  if not nodes:
550
    raise qa_error.OutOfNodesError("No nodes left")
551

    
552
  # Get node with least number of uses
553
  # TODO: Switch to computing sort key instead of comparing directly
554
  def compare(a, b):
555
    result = cmp(a.use_count, b.use_count)
556
    if result == 0:
557
      result = cmp(a.primary, b.primary)
558
    return result
559

    
560
  nodes.sort(cmp=compare)
561

    
562
  return nodes[0].Use()
563

    
564

    
565
def AcquireManyNodes(num, exclude=None):
566
  """Return the least used nodes.
567

568
  @type num: int
569
  @param num: Number of nodes; can be 0.
570
  @type exclude: list of nodes or C{None}
571
  @param exclude: nodes to be excluded from the choice
572
  @rtype: list of nodes
573
  @return: C{num} different nodes
574

575
  """
576
  nodes = []
577
  if exclude is None:
578
    exclude = []
579
  elif isinstance(exclude, (list, tuple)):
580
    # Don't modify the incoming argument
581
    exclude = list(exclude)
582
  else:
583
    exclude = [exclude]
584

    
585
  try:
586
    for _ in range(0, num):
587
      n = AcquireNode(exclude=exclude)
588
      nodes.append(n)
589
      exclude.append(n)
590
  except qa_error.OutOfNodesError:
591
    ReleaseManyNodes(nodes)
592
    raise
593
  return nodes
594

    
595

    
596
def ReleaseManyNodes(nodes):
597
  for node in nodes:
598
    node.Release()