Statistics
| Branch: | Tag: | Revision:

root / qa / qa_config.py @ dbdb0594

History | View | Annotate | Download (13.4 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.use_count = 0
131
    self._added = False
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 MarkAdded(self):
171
    """Marks node as having been added to a cluster.
172

173
    """
174
    assert not self._added
175
    self._added = True
176

    
177
  def MarkRemoved(self):
178
    """Marks node as having been removed from a cluster.
179

180
    """
181
    assert self._added
182
    self._added = False
183

    
184
  @property
185
  def added(self):
186
    """Returns whether a node is part of a cluster.
187

188
    """
189
    return self._added
190

    
191

    
192
_RESOURCE_CONVERTER = {
193
  "instances": _QaInstance.FromDict,
194
  "nodes": _QaNode.FromDict,
195
  }
196

    
197

    
198
def _ConvertResources((key, value)):
199
  """Converts cluster resources in configuration to Python objects.
200

201
  """
202
  fn = _RESOURCE_CONVERTER.get(key, None)
203
  if fn:
204
    return (key, map(fn, value))
205
  else:
206
    return (key, value)
207

    
208

    
209
class _QaConfig(object):
210
  def __init__(self, data):
211
    """Initializes instances of this class.
212

213
    """
214
    self._data = data
215

    
216
    #: Cluster-wide run-time value of the exclusive storage flag
217
    self._exclusive_storage = None
218

    
219
  @classmethod
220
  def Load(cls, filename):
221
    """Loads a configuration file and produces a configuration object.
222

223
    @type filename: string
224
    @param filename: Path to configuration file
225
    @rtype: L{_QaConfig}
226

227
    """
228
    data = serializer.LoadJson(utils.ReadFile(filename))
229

    
230
    result = cls(dict(map(_ConvertResources,
231
                          data.items()))) # pylint: disable=E1103
232
    result.Validate()
233

    
234
    return result
235

    
236
  def Validate(self):
237
    """Validates loaded configuration data.
238

239
    """
240
    if not self.get("nodes"):
241
      raise qa_error.Error("Need at least one node")
242

    
243
    if not self.get("instances"):
244
      raise qa_error.Error("Need at least one instance")
245

    
246
    if (self.get("disk") is None or
247
        self.get("disk-growth") is None or
248
        len(self.get("disk")) != len(self.get("disk-growth"))):
249
      raise qa_error.Error("Config options 'disk' and 'disk-growth' must exist"
250
                           " and have the same number of items")
251

    
252
    check = self.GetInstanceCheckScript()
253
    if check:
254
      try:
255
        os.stat(check)
256
      except EnvironmentError, err:
257
        raise qa_error.Error("Can't find instance check script '%s': %s" %
258
                             (check, err))
259

    
260
    enabled_hv = frozenset(self.GetEnabledHypervisors())
261
    if not enabled_hv:
262
      raise qa_error.Error("No hypervisor is enabled")
263

    
264
    difference = enabled_hv - constants.HYPER_TYPES
265
    if difference:
266
      raise qa_error.Error("Unknown hypervisor(s) enabled: %s" %
267
                           utils.CommaJoin(difference))
268

    
269
  def __getitem__(self, name):
270
    """Returns configuration value.
271

272
    @type name: string
273
    @param name: Name of configuration entry
274

275
    """
276
    return self._data[name]
277

    
278
  def get(self, name, default=None):
279
    """Returns configuration value.
280

281
    @type name: string
282
    @param name: Name of configuration entry
283
    @param default: Default value
284

285
    """
286
    return self._data.get(name, default)
287

    
288
  def GetMasterNode(self):
289
    """Returns the default master node for the cluster.
290

291
    """
292
    return self["nodes"][0]
293

    
294
  def GetInstanceCheckScript(self):
295
    """Returns path to instance check script or C{None}.
296

297
    """
298
    return self._data.get(_INSTANCE_CHECK_KEY, None)
299

    
300
  def GetEnabledHypervisors(self):
301
    """Returns list of enabled hypervisors.
302

303
    @rtype: list
304

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

    
322
  def GetDefaultHypervisor(self):
323
    """Returns the default hypervisor to be used.
324

325
    """
326
    return self.GetEnabledHypervisors()[0]
327

    
328
  def SetExclusiveStorage(self, value):
329
    """Set the expected value of the C{exclusive_storage} flag for the cluster.
330

331
    """
332
    self._exclusive_storage = bool(value)
333

    
334
  def GetExclusiveStorage(self):
335
    """Get the expected value of the C{exclusive_storage} flag for the cluster.
336

337
    """
338
    value = self._exclusive_storage
339
    assert value is not None
340
    return value
341

    
342
  def IsTemplateSupported(self, templ):
343
    """Is the given disk template supported by the current configuration?
344

345
    """
346
    return (not self.GetExclusiveStorage() or
347
            templ in constants.DTS_EXCL_STORAGE)
348

    
349

    
350
def Load(path):
351
  """Loads the passed configuration file.
352

353
  """
354
  global _config # pylint: disable=W0603
355

    
356
  _config = _QaConfig.Load(path)
357

    
358

    
359
def GetConfig():
360
  """Returns the configuration object.
361

362
  """
363
  if _config is None:
364
    raise RuntimeError("Configuration not yet loaded")
365

    
366
  return _config
367

    
368

    
369
def get(name, default=None):
370
  """Wrapper for L{_QaConfig.get}.
371

372
  """
373
  return GetConfig().get(name, default=default)
374

    
375

    
376
class Either:
377
  def __init__(self, tests):
378
    """Initializes this class.
379

380
    @type tests: list or string
381
    @param tests: List of test names
382
    @see: L{TestEnabled} for details
383

384
    """
385
    self.tests = tests
386

    
387

    
388
def _MakeSequence(value):
389
  """Make sequence of single argument.
390

391
  If the single argument is not already a list or tuple, a list with the
392
  argument as a single item is returned.
393

394
  """
395
  if isinstance(value, (list, tuple)):
396
    return value
397
  else:
398
    return [value]
399

    
400

    
401
def _TestEnabledInner(check_fn, names, fn):
402
  """Evaluate test conditions.
403

404
  @type check_fn: callable
405
  @param check_fn: Callback to check whether a test is enabled
406
  @type names: sequence or string
407
  @param names: Test name(s)
408
  @type fn: callable
409
  @param fn: Aggregation function
410
  @rtype: bool
411
  @return: Whether test is enabled
412

413
  """
414
  names = _MakeSequence(names)
415

    
416
  result = []
417

    
418
  for name in names:
419
    if isinstance(name, Either):
420
      value = _TestEnabledInner(check_fn, name.tests, compat.any)
421
    elif isinstance(name, (list, tuple)):
422
      value = _TestEnabledInner(check_fn, name, compat.all)
423
    else:
424
      value = check_fn(name)
425

    
426
    result.append(value)
427

    
428
  return fn(result)
429

    
430

    
431
def TestEnabled(tests, _cfg=None):
432
  """Returns True if the given tests are enabled.
433

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

437
  """
438
  if _cfg is None:
439
    cfg = GetConfig()
440
  else:
441
    cfg = _cfg
442

    
443
  # Get settings for all tests
444
  cfg_tests = cfg.get("tests", {})
445

    
446
  # Get default setting
447
  default = cfg_tests.get("default", True)
448

    
449
  return _TestEnabledInner(lambda name: cfg_tests.get(name, default),
450
                           tests, compat.all)
451

    
452

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

456
  """
457
  return GetConfig().GetInstanceCheckScript(*args)
458

    
459

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

463
  """
464
  return GetConfig().GetEnabledHypervisors(*args)
465

    
466

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

470
  """
471
  return GetConfig().GetDefaultHypervisor(*args)
472

    
473

    
474
def GetInstanceNicMac(inst, default=None):
475
  """Returns MAC address for instance's network interface.
476

477
  """
478
  return inst.GetNicMacAddr(0, default)
479

    
480

    
481
def GetMasterNode():
482
  """Wrapper for L{_QaConfig.GetMasterNode}.
483

484
  """
485
  return GetConfig().GetMasterNode()
486

    
487

    
488
def AcquireInstance(_cfg=None):
489
  """Returns an instance which isn't in use.
490

491
  """
492
  if _cfg is None:
493
    cfg = GetConfig()
494
  else:
495
    cfg = _cfg
496

    
497
  # Filter out unwanted instances
498
  instances = filter(lambda inst: not inst.used, cfg["instances"])
499

    
500
  if not instances:
501
    raise qa_error.OutOfInstancesError("No instances left")
502

    
503
  inst = instances[0]
504

    
505
  assert not inst.used
506
  assert inst.disk_template is None
507

    
508
  inst.used = True
509

    
510
  return inst
511

    
512

    
513
def GetInstanceTemplate(inst):
514
  """Return the disk template of an instance.
515

516
  """
517
  templ = inst.disk_template
518
  assert templ is not None
519
  return templ
520

    
521

    
522
def SetInstanceTemplate(inst, template):
523
  """Set the disk template for an instance.
524

525
  """
526
  inst.disk_template = template
527

    
528

    
529
def SetExclusiveStorage(value):
530
  """Wrapper for L{_QaConfig.SetExclusiveStorage}.
531

532
  """
533
  return GetConfig().SetExclusiveStorage(value)
534

    
535

    
536
def GetExclusiveStorage():
537
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
538

539
  """
540
  return GetConfig().GetExclusiveStorage()
541

    
542

    
543
def IsTemplateSupported(templ):
544
  """Wrapper for L{_QaConfig.GetExclusiveStorage}.
545

546
  """
547
  return GetConfig().IsTemplateSupported(templ)
548

    
549

    
550
def AcquireNode(exclude=None, _cfg=None):
551
  """Returns the least used node.
552

553
  """
554
  if _cfg is None:
555
    cfg = GetConfig()
556
  else:
557
    cfg = _cfg
558

    
559
  master = cfg.GetMasterNode()
560

    
561
  # Filter out unwanted nodes
562
  # TODO: Maybe combine filters
563
  if exclude is None:
564
    nodes = cfg["nodes"][:]
565
  elif isinstance(exclude, (list, tuple)):
566
    nodes = filter(lambda node: node not in exclude, cfg["nodes"])
567
  else:
568
    nodes = filter(lambda node: node != exclude, cfg["nodes"])
569

    
570
  nodes = filter(lambda node: node.added or node == master, nodes)
571

    
572
  if not nodes:
573
    raise qa_error.OutOfNodesError("No nodes left")
574

    
575
  # Get node with least number of uses
576
  # TODO: Switch to computing sort key instead of comparing directly
577
  def compare(a, b):
578
    result = cmp(a.use_count, b.use_count)
579
    if result == 0:
580
      result = cmp(a.primary, b.primary)
581
    return result
582

    
583
  nodes.sort(cmp=compare)
584

    
585
  return nodes[0].Use()
586

    
587

    
588
def AcquireManyNodes(num, exclude=None):
589
  """Return the least used nodes.
590

591
  @type num: int
592
  @param num: Number of nodes; can be 0.
593
  @type exclude: list of nodes or C{None}
594
  @param exclude: nodes to be excluded from the choice
595
  @rtype: list of nodes
596
  @return: C{num} different nodes
597

598
  """
599
  nodes = []
600
  if exclude is None:
601
    exclude = []
602
  elif isinstance(exclude, (list, tuple)):
603
    # Don't modify the incoming argument
604
    exclude = list(exclude)
605
  else:
606
    exclude = [exclude]
607

    
608
  try:
609
    for _ in range(0, num):
610
      n = AcquireNode(exclude=exclude)
611
      nodes.append(n)
612
      exclude.append(n)
613
  except qa_error.OutOfNodesError:
614
    ReleaseManyNodes(nodes)
615
    raise
616
  return nodes
617

    
618

    
619
def ReleaseNode(node):
620
  assert node.use_count > 0
621

    
622
  node.use_count -= 1
623

    
624

    
625
def ReleaseManyNodes(nodes):
626
  for n in nodes:
627
    ReleaseNode(n)