Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 51cb1581

History | View | Annotate | Download (35.1 kB)

1
#
2
#
3

    
4
# Copyright (C) 2006, 2007 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
"""Transportable objects for Ganeti.
23

24
This module provides small, mostly data-only objects which are safe to
25
pass to and from external parties.
26

27
"""
28

    
29
# pylint: disable-msg=E0203,W0201
30

    
31
# E0203: Access to member %r before its definition, since we use
32
# objects.py which doesn't explicitely initialise its members
33

    
34
# W0201: Attribute '%s' defined outside __init__
35

    
36
import ConfigParser
37
import re
38
import copy
39
from cStringIO import StringIO
40

    
41
from ganeti import errors
42
from ganeti import constants
43

    
44

    
45
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
46
           "OS", "Node", "Cluster", "FillDict"]
47

    
48
_TIMESTAMPS = ["ctime", "mtime"]
49
_UUID = ["uuid"]
50

    
51

    
52
def FillDict(defaults_dict, custom_dict, skip_keys=None):
53
  """Basic function to apply settings on top a default dict.
54

55
  @type defaults_dict: dict
56
  @param defaults_dict: dictionary holding the default values
57
  @type custom_dict: dict
58
  @param custom_dict: dictionary holding customized value
59
  @type skip_keys: list
60
  @param skip_keys: which keys not to fill
61
  @rtype: dict
62
  @return: dict with the 'full' values
63

64
  """
65
  ret_dict = copy.deepcopy(defaults_dict)
66
  ret_dict.update(custom_dict)
67
  if skip_keys:
68
    for k in skip_keys:
69
      try:
70
        del ret_dict[k]
71
      except KeyError:
72
        pass
73
  return ret_dict
74

    
75

    
76
def UpgradeGroupedParams(target, defaults):
77
  """Update all groups for the target parameter.
78

79
  @type target: dict of dicts
80
  @param target: {group: {parameter: value}}
81
  @type defaults: dict
82
  @param defaults: default parameter values
83

84
  """
85
  if target is None:
86
    target = {constants.PP_DEFAULT: defaults}
87
  else:
88
    for group in target:
89
      target[group] = FillDict(defaults, target[group])
90
  return target
91

    
92

    
93
class ConfigObject(object):
94
  """A generic config object.
95

96
  It has the following properties:
97

98
    - provides somewhat safe recursive unpickling and pickling for its classes
99
    - unset attributes which are defined in slots are always returned
100
      as None instead of raising an error
101

102
  Classes derived from this must always declare __slots__ (we use many
103
  config objects and the memory reduction is useful)
104

105
  """
106
  __slots__ = []
107

    
108
  def __init__(self, **kwargs):
109
    for k, v in kwargs.iteritems():
110
      setattr(self, k, v)
111

    
112
  def __getattr__(self, name):
113
    if name not in self._all_slots():
114
      raise AttributeError("Invalid object attribute %s.%s" %
115
                           (type(self).__name__, name))
116
    return None
117

    
118
  def __setstate__(self, state):
119
    slots = self._all_slots()
120
    for name in state:
121
      if name in slots:
122
        setattr(self, name, state[name])
123

    
124
  @classmethod
125
  def _all_slots(cls):
126
    """Compute the list of all declared slots for a class.
127

128
    """
129
    slots = []
130
    for parent in cls.__mro__:
131
      slots.extend(getattr(parent, "__slots__", []))
132
    return slots
133

    
134
  def ToDict(self):
135
    """Convert to a dict holding only standard python types.
136

137
    The generic routine just dumps all of this object's attributes in
138
    a dict. It does not work if the class has children who are
139
    ConfigObjects themselves (e.g. the nics list in an Instance), in
140
    which case the object should subclass the function in order to
141
    make sure all objects returned are only standard python types.
142

143
    """
144
    result = {}
145
    for name in self._all_slots():
146
      value = getattr(self, name, None)
147
      if value is not None:
148
        result[name] = value
149
    return result
150

    
151
  __getstate__ = ToDict
152

    
153
  @classmethod
154
  def FromDict(cls, val):
155
    """Create an object from a dictionary.
156

157
    This generic routine takes a dict, instantiates a new instance of
158
    the given class, and sets attributes based on the dict content.
159

160
    As for `ToDict`, this does not work if the class has children
161
    who are ConfigObjects themselves (e.g. the nics list in an
162
    Instance), in which case the object should subclass the function
163
    and alter the objects.
164

165
    """
166
    if not isinstance(val, dict):
167
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
168
                                      " expected dict, got %s" % type(val))
169
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
170
    obj = cls(**val_str) # pylint: disable-msg=W0142
171
    return obj
172

    
173
  @staticmethod
174
  def _ContainerToDicts(container):
175
    """Convert the elements of a container to standard python types.
176

177
    This method converts a container with elements derived from
178
    ConfigData to standard python types. If the container is a dict,
179
    we don't touch the keys, only the values.
180

181
    """
182
    if isinstance(container, dict):
183
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
184
    elif isinstance(container, (list, tuple, set, frozenset)):
185
      ret = [elem.ToDict() for elem in container]
186
    else:
187
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
188
                      type(container))
189
    return ret
190

    
191
  @staticmethod
192
  def _ContainerFromDicts(source, c_type, e_type):
193
    """Convert a container from standard python types.
194

195
    This method converts a container with standard python types to
196
    ConfigData objects. If the container is a dict, we don't touch the
197
    keys, only the values.
198

199
    """
200
    if not isinstance(c_type, type):
201
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
202
                      " not a type" % type(c_type))
203
    if c_type is dict:
204
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
205
    elif c_type in (list, tuple, set, frozenset):
206
      ret = c_type([e_type.FromDict(elem) for elem in source])
207
    else:
208
      raise TypeError("Invalid container type %s passed to"
209
                      " _ContainerFromDicts" % c_type)
210
    return ret
211

    
212
  def Copy(self):
213
    """Makes a deep copy of the current object and its children.
214

215
    """
216
    dict_form = self.ToDict()
217
    clone_obj = self.__class__.FromDict(dict_form)
218
    return clone_obj
219

    
220
  def __repr__(self):
221
    """Implement __repr__ for ConfigObjects."""
222
    return repr(self.ToDict())
223

    
224
  def UpgradeConfig(self):
225
    """Fill defaults for missing configuration values.
226

227
    This method will be called at configuration load time, and its
228
    implementation will be object dependent.
229

230
    """
231
    pass
232

    
233

    
234
class TaggableObject(ConfigObject):
235
  """An generic class supporting tags.
236

237
  """
238
  __slots__ = ["tags"]
239
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
240

    
241
  @classmethod
242
  def ValidateTag(cls, tag):
243
    """Check if a tag is valid.
244

245
    If the tag is invalid, an errors.TagError will be raised. The
246
    function has no return value.
247

248
    """
249
    if not isinstance(tag, basestring):
250
      raise errors.TagError("Invalid tag type (not a string)")
251
    if len(tag) > constants.MAX_TAG_LEN:
252
      raise errors.TagError("Tag too long (>%d characters)" %
253
                            constants.MAX_TAG_LEN)
254
    if not tag:
255
      raise errors.TagError("Tags cannot be empty")
256
    if not cls.VALID_TAG_RE.match(tag):
257
      raise errors.TagError("Tag contains invalid characters")
258

    
259
  def GetTags(self):
260
    """Return the tags list.
261

262
    """
263
    tags = getattr(self, "tags", None)
264
    if tags is None:
265
      tags = self.tags = set()
266
    return tags
267

    
268
  def AddTag(self, tag):
269
    """Add a new tag.
270

271
    """
272
    self.ValidateTag(tag)
273
    tags = self.GetTags()
274
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
275
      raise errors.TagError("Too many tags")
276
    self.GetTags().add(tag)
277

    
278
  def RemoveTag(self, tag):
279
    """Remove a tag.
280

281
    """
282
    self.ValidateTag(tag)
283
    tags = self.GetTags()
284
    try:
285
      tags.remove(tag)
286
    except KeyError:
287
      raise errors.TagError("Tag not found")
288

    
289
  def ToDict(self):
290
    """Taggable-object-specific conversion to standard python types.
291

292
    This replaces the tags set with a list.
293

294
    """
295
    bo = super(TaggableObject, self).ToDict()
296

    
297
    tags = bo.get("tags", None)
298
    if isinstance(tags, set):
299
      bo["tags"] = list(tags)
300
    return bo
301

    
302
  @classmethod
303
  def FromDict(cls, val):
304
    """Custom function for instances.
305

306
    """
307
    obj = super(TaggableObject, cls).FromDict(val)
308
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
309
      obj.tags = set(obj.tags)
310
    return obj
311

    
312

    
313
class ConfigData(ConfigObject):
314
  """Top-level config object."""
315
  __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
316
               _TIMESTAMPS)
317

    
318
  def ToDict(self):
319
    """Custom function for top-level config data.
320

321
    This just replaces the list of instances, nodes and the cluster
322
    with standard python types.
323

324
    """
325
    mydict = super(ConfigData, self).ToDict()
326
    mydict["cluster"] = mydict["cluster"].ToDict()
327
    for key in "nodes", "instances":
328
      mydict[key] = self._ContainerToDicts(mydict[key])
329

    
330
    return mydict
331

    
332
  @classmethod
333
  def FromDict(cls, val):
334
    """Custom function for top-level config data
335

336
    """
337
    obj = super(ConfigData, cls).FromDict(val)
338
    obj.cluster = Cluster.FromDict(obj.cluster)
339
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
340
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
341
    return obj
342

    
343
  def HasAnyDiskOfType(self, dev_type):
344
    """Check if in there is at disk of the given type in the configuration.
345

346
    @type dev_type: L{constants.LDS_BLOCK}
347
    @param dev_type: the type to look for
348
    @rtype: boolean
349
    @return: boolean indicating if a disk of the given type was found or not
350

351
    """
352
    for instance in self.instances.values():
353
      for disk in instance.disks:
354
        if disk.IsBasedOnDiskType(dev_type):
355
          return True
356
    return False
357

    
358
  def UpgradeConfig(self):
359
    """Fill defaults for missing configuration values.
360

361
    """
362
    self.cluster.UpgradeConfig()
363
    for node in self.nodes.values():
364
      node.UpgradeConfig()
365
    for instance in self.instances.values():
366
      instance.UpgradeConfig()
367

    
368

    
369
class NIC(ConfigObject):
370
  """Config object representing a network card."""
371
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
372

    
373
  @classmethod
374
  def CheckParameterSyntax(cls, nicparams):
375
    """Check the given parameters for validity.
376

377
    @type nicparams:  dict
378
    @param nicparams: dictionary with parameter names/value
379
    @raise errors.ConfigurationError: when a parameter is not valid
380

381
    """
382
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
383
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
384
      raise errors.ConfigurationError(err)
385

    
386
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
387
        not nicparams[constants.NIC_LINK]):
388
      err = "Missing bridged nic link"
389
      raise errors.ConfigurationError(err)
390

    
391
  def UpgradeConfig(self):
392
    """Fill defaults for missing configuration values.
393

394
    """
395
    if self.nicparams is None:
396
      self.nicparams = {}
397
      if self.bridge is not None:
398
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
399
        self.nicparams[constants.NIC_LINK] = self.bridge
400
    # bridge is no longer used it 2.1. The slot is left there to support
401
    # upgrading, but can be removed once upgrades to the current version
402
    # straight from 2.0 are deprecated.
403
    if self.bridge is not None:
404
      self.bridge = None
405

    
406

    
407
class Disk(ConfigObject):
408
  """Config object representing a block device."""
409
  __slots__ = ["dev_type", "logical_id", "physical_id",
410
               "children", "iv_name", "size", "mode"]
411

    
412
  def CreateOnSecondary(self):
413
    """Test if this device needs to be created on a secondary node."""
414
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
415

    
416
  def AssembleOnSecondary(self):
417
    """Test if this device needs to be assembled on a secondary node."""
418
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
419

    
420
  def OpenOnSecondary(self):
421
    """Test if this device needs to be opened on a secondary node."""
422
    return self.dev_type in (constants.LD_LV,)
423

    
424
  def StaticDevPath(self):
425
    """Return the device path if this device type has a static one.
426

427
    Some devices (LVM for example) live always at the same /dev/ path,
428
    irrespective of their status. For such devices, we return this
429
    path, for others we return None.
430

431
    @warning: The path returned is not a normalized pathname; callers
432
        should check that it is a valid path.
433

434
    """
435
    if self.dev_type == constants.LD_LV:
436
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
437
    return None
438

    
439
  def ChildrenNeeded(self):
440
    """Compute the needed number of children for activation.
441

442
    This method will return either -1 (all children) or a positive
443
    number denoting the minimum number of children needed for
444
    activation (only mirrored devices will usually return >=0).
445

446
    Currently, only DRBD8 supports diskless activation (therefore we
447
    return 0), for all other we keep the previous semantics and return
448
    -1.
449

450
    """
451
    if self.dev_type == constants.LD_DRBD8:
452
      return 0
453
    return -1
454

    
455
  def IsBasedOnDiskType(self, dev_type):
456
    """Check if the disk or its children are based on the given type.
457

458
    @type dev_type: L{constants.LDS_BLOCK}
459
    @param dev_type: the type to look for
460
    @rtype: boolean
461
    @return: boolean indicating if a device of the given type was found or not
462

463
    """
464
    if self.children:
465
      for child in self.children:
466
        if child.IsBasedOnDiskType(dev_type):
467
          return True
468
    return self.dev_type == dev_type
469

    
470
  def GetNodes(self, node):
471
    """This function returns the nodes this device lives on.
472

473
    Given the node on which the parent of the device lives on (or, in
474
    case of a top-level device, the primary node of the devices'
475
    instance), this function will return a list of nodes on which this
476
    devices needs to (or can) be assembled.
477

478
    """
479
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
480
      result = [node]
481
    elif self.dev_type in constants.LDS_DRBD:
482
      result = [self.logical_id[0], self.logical_id[1]]
483
      if node not in result:
484
        raise errors.ConfigurationError("DRBD device passed unknown node")
485
    else:
486
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
487
    return result
488

    
489
  def ComputeNodeTree(self, parent_node):
490
    """Compute the node/disk tree for this disk and its children.
491

492
    This method, given the node on which the parent disk lives, will
493
    return the list of all (node, disk) pairs which describe the disk
494
    tree in the most compact way. For example, a drbd/lvm stack
495
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
496
    which represents all the top-level devices on the nodes.
497

498
    """
499
    my_nodes = self.GetNodes(parent_node)
500
    result = [(node, self) for node in my_nodes]
501
    if not self.children:
502
      # leaf device
503
      return result
504
    for node in my_nodes:
505
      for child in self.children:
506
        child_result = child.ComputeNodeTree(node)
507
        if len(child_result) == 1:
508
          # child (and all its descendants) is simple, doesn't split
509
          # over multiple hosts, so we don't need to describe it, our
510
          # own entry for this node describes it completely
511
          continue
512
        else:
513
          # check if child nodes differ from my nodes; note that
514
          # subdisk can differ from the child itself, and be instead
515
          # one of its descendants
516
          for subnode, subdisk in child_result:
517
            if subnode not in my_nodes:
518
              result.append((subnode, subdisk))
519
            # otherwise child is under our own node, so we ignore this
520
            # entry (but probably the other results in the list will
521
            # be different)
522
    return result
523

    
524
  def RecordGrow(self, amount):
525
    """Update the size of this disk after growth.
526

527
    This method recurses over the disks's children and updates their
528
    size correspondigly. The method needs to be kept in sync with the
529
    actual algorithms from bdev.
530

531
    """
532
    if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
533
      self.size += amount
534
    elif self.dev_type == constants.LD_DRBD8:
535
      if self.children:
536
        self.children[0].RecordGrow(amount)
537
      self.size += amount
538
    else:
539
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
540
                                   " disk type %s" % self.dev_type)
541

    
542
  def UnsetSize(self):
543
    """Sets recursively the size to zero for the disk and its children.
544

545
    """
546
    if self.children:
547
      for child in self.children:
548
        child.UnsetSize()
549
    self.size = 0
550

    
551
  def SetPhysicalID(self, target_node, nodes_ip):
552
    """Convert the logical ID to the physical ID.
553

554
    This is used only for drbd, which needs ip/port configuration.
555

556
    The routine descends down and updates its children also, because
557
    this helps when the only the top device is passed to the remote
558
    node.
559

560
    Arguments:
561
      - target_node: the node we wish to configure for
562
      - nodes_ip: a mapping of node name to ip
563

564
    The target_node must exist in in nodes_ip, and must be one of the
565
    nodes in the logical ID for each of the DRBD devices encountered
566
    in the disk tree.
567

568
    """
569
    if self.children:
570
      for child in self.children:
571
        child.SetPhysicalID(target_node, nodes_ip)
572

    
573
    if self.logical_id is None and self.physical_id is not None:
574
      return
575
    if self.dev_type in constants.LDS_DRBD:
576
      pnode, snode, port, pminor, sminor, secret = self.logical_id
577
      if target_node not in (pnode, snode):
578
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
579
                                        target_node)
580
      pnode_ip = nodes_ip.get(pnode, None)
581
      snode_ip = nodes_ip.get(snode, None)
582
      if pnode_ip is None or snode_ip is None:
583
        raise errors.ConfigurationError("Can't find primary or secondary node"
584
                                        " for %s" % str(self))
585
      p_data = (pnode_ip, port)
586
      s_data = (snode_ip, port)
587
      if pnode == target_node:
588
        self.physical_id = p_data + s_data + (pminor, secret)
589
      else: # it must be secondary, we tested above
590
        self.physical_id = s_data + p_data + (sminor, secret)
591
    else:
592
      self.physical_id = self.logical_id
593
    return
594

    
595
  def ToDict(self):
596
    """Disk-specific conversion to standard python types.
597

598
    This replaces the children lists of objects with lists of
599
    standard python types.
600

601
    """
602
    bo = super(Disk, self).ToDict()
603

    
604
    for attr in ("children",):
605
      alist = bo.get(attr, None)
606
      if alist:
607
        bo[attr] = self._ContainerToDicts(alist)
608
    return bo
609

    
610
  @classmethod
611
  def FromDict(cls, val):
612
    """Custom function for Disks
613

614
    """
615
    obj = super(Disk, cls).FromDict(val)
616
    if obj.children:
617
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
618
    if obj.logical_id and isinstance(obj.logical_id, list):
619
      obj.logical_id = tuple(obj.logical_id)
620
    if obj.physical_id and isinstance(obj.physical_id, list):
621
      obj.physical_id = tuple(obj.physical_id)
622
    if obj.dev_type in constants.LDS_DRBD:
623
      # we need a tuple of length six here
624
      if len(obj.logical_id) < 6:
625
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
626
    return obj
627

    
628
  def __str__(self):
629
    """Custom str() formatter for disks.
630

631
    """
632
    if self.dev_type == constants.LD_LV:
633
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
634
    elif self.dev_type in constants.LDS_DRBD:
635
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
636
      val = "<DRBD8("
637
      if self.physical_id is None:
638
        phy = "unconfigured"
639
      else:
640
        phy = ("configured as %s:%s %s:%s" %
641
               (self.physical_id[0], self.physical_id[1],
642
                self.physical_id[2], self.physical_id[3]))
643

    
644
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
645
              (node_a, minor_a, node_b, minor_b, port, phy))
646
      if self.children and self.children.count(None) == 0:
647
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
648
      else:
649
        val += "no local storage"
650
    else:
651
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
652
             (self.dev_type, self.logical_id, self.physical_id, self.children))
653
    if self.iv_name is None:
654
      val += ", not visible"
655
    else:
656
      val += ", visible as /dev/%s" % self.iv_name
657
    if isinstance(self.size, int):
658
      val += ", size=%dm)>" % self.size
659
    else:
660
      val += ", size='%s')>" % (self.size,)
661
    return val
662

    
663
  def Verify(self):
664
    """Checks that this disk is correctly configured.
665

666
    """
667
    all_errors = []
668
    if self.mode not in constants.DISK_ACCESS_SET:
669
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
670
    return all_errors
671

    
672
  def UpgradeConfig(self):
673
    """Fill defaults for missing configuration values.
674

675
    """
676
    if self.children:
677
      for child in self.children:
678
        child.UpgradeConfig()
679
    # add here config upgrade for this disk
680

    
681

    
682
class Instance(TaggableObject):
683
  """Config object representing an instance."""
684
  __slots__ = [
685
    "name",
686
    "primary_node",
687
    "os",
688
    "hypervisor",
689
    "hvparams",
690
    "beparams",
691
    "osparams",
692
    "admin_up",
693
    "nics",
694
    "disks",
695
    "disk_template",
696
    "network_port",
697
    "serial_no",
698
    ] + _TIMESTAMPS + _UUID
699

    
700
  def _ComputeSecondaryNodes(self):
701
    """Compute the list of secondary nodes.
702

703
    This is a simple wrapper over _ComputeAllNodes.
704

705
    """
706
    all_nodes = set(self._ComputeAllNodes())
707
    all_nodes.discard(self.primary_node)
708
    return tuple(all_nodes)
709

    
710
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
711
                             "List of secondary nodes")
712

    
713
  def _ComputeAllNodes(self):
714
    """Compute the list of all nodes.
715

716
    Since the data is already there (in the drbd disks), keeping it as
717
    a separate normal attribute is redundant and if not properly
718
    synchronised can cause problems. Thus it's better to compute it
719
    dynamically.
720

721
    """
722
    def _Helper(nodes, device):
723
      """Recursively computes nodes given a top device."""
724
      if device.dev_type in constants.LDS_DRBD:
725
        nodea, nodeb = device.logical_id[:2]
726
        nodes.add(nodea)
727
        nodes.add(nodeb)
728
      if device.children:
729
        for child in device.children:
730
          _Helper(nodes, child)
731

    
732
    all_nodes = set()
733
    all_nodes.add(self.primary_node)
734
    for device in self.disks:
735
      _Helper(all_nodes, device)
736
    return tuple(all_nodes)
737

    
738
  all_nodes = property(_ComputeAllNodes, None, None,
739
                       "List of all nodes of the instance")
740

    
741
  def MapLVsByNode(self, lvmap=None, devs=None, node=None):
742
    """Provide a mapping of nodes to LVs this instance owns.
743

744
    This function figures out what logical volumes should belong on
745
    which nodes, recursing through a device tree.
746

747
    @param lvmap: optional dictionary to receive the
748
        'node' : ['lv', ...] data.
749

750
    @return: None if lvmap arg is given, otherwise, a dictionary
751
        of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
752

753
    """
754
    if node == None:
755
      node = self.primary_node
756

    
757
    if lvmap is None:
758
      lvmap = { node : [] }
759
      ret = lvmap
760
    else:
761
      if not node in lvmap:
762
        lvmap[node] = []
763
      ret = None
764

    
765
    if not devs:
766
      devs = self.disks
767

    
768
    for dev in devs:
769
      if dev.dev_type == constants.LD_LV:
770
        lvmap[node].append(dev.logical_id[1])
771

    
772
      elif dev.dev_type in constants.LDS_DRBD:
773
        if dev.children:
774
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
775
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
776

    
777
      elif dev.children:
778
        self.MapLVsByNode(lvmap, dev.children, node)
779

    
780
    return ret
781

    
782
  def FindDisk(self, idx):
783
    """Find a disk given having a specified index.
784

785
    This is just a wrapper that does validation of the index.
786

787
    @type idx: int
788
    @param idx: the disk index
789
    @rtype: L{Disk}
790
    @return: the corresponding disk
791
    @raise errors.OpPrereqError: when the given index is not valid
792

793
    """
794
    try:
795
      idx = int(idx)
796
      return self.disks[idx]
797
    except (TypeError, ValueError), err:
798
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
799
                                 errors.ECODE_INVAL)
800
    except IndexError:
801
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
802
                                 " 0 to %d" % (idx, len(self.disks)),
803
                                 errors.ECODE_INVAL)
804

    
805
  def ToDict(self):
806
    """Instance-specific conversion to standard python types.
807

808
    This replaces the children lists of objects with lists of standard
809
    python types.
810

811
    """
812
    bo = super(Instance, self).ToDict()
813

    
814
    for attr in "nics", "disks":
815
      alist = bo.get(attr, None)
816
      if alist:
817
        nlist = self._ContainerToDicts(alist)
818
      else:
819
        nlist = []
820
      bo[attr] = nlist
821
    return bo
822

    
823
  @classmethod
824
  def FromDict(cls, val):
825
    """Custom function for instances.
826

827
    """
828
    obj = super(Instance, cls).FromDict(val)
829
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
830
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
831
    return obj
832

    
833
  def UpgradeConfig(self):
834
    """Fill defaults for missing configuration values.
835

836
    """
837
    for nic in self.nics:
838
      nic.UpgradeConfig()
839
    for disk in self.disks:
840
      disk.UpgradeConfig()
841
    if self.hvparams:
842
      for key in constants.HVC_GLOBALS:
843
        try:
844
          del self.hvparams[key]
845
        except KeyError:
846
          pass
847
    if self.osparams is None:
848
      self.osparams = {}
849

    
850

    
851
class OS(ConfigObject):
852
  """Config object representing an operating system.
853

854
  @type supported_parameters: list
855
  @ivar supported_parameters: a list of tuples, name and description,
856
      containing the supported parameters by this OS
857

858
  """
859
  __slots__ = [
860
    "name",
861
    "path",
862
    "api_versions",
863
    "create_script",
864
    "export_script",
865
    "import_script",
866
    "rename_script",
867
    "verify_script",
868
    "supported_variants",
869
    "supported_parameters",
870
    ]
871

    
872

    
873
class Node(TaggableObject):
874
  """Config object representing a node."""
875
  __slots__ = [
876
    "name",
877
    "primary_ip",
878
    "secondary_ip",
879
    "serial_no",
880
    "master_candidate",
881
    "offline",
882
    "drained",
883
    ] + _TIMESTAMPS + _UUID
884

    
885

    
886
class Cluster(TaggableObject):
887
  """Config object representing the cluster."""
888
  __slots__ = [
889
    "serial_no",
890
    "rsahostkeypub",
891
    "highest_used_port",
892
    "tcpudp_port_pool",
893
    "mac_prefix",
894
    "volume_group_name",
895
    "drbd_usermode_helper",
896
    "default_bridge",
897
    "default_hypervisor",
898
    "master_node",
899
    "master_ip",
900
    "master_netdev",
901
    "cluster_name",
902
    "file_storage_dir",
903
    "enabled_hypervisors",
904
    "hvparams",
905
    "os_hvp",
906
    "beparams",
907
    "osparams",
908
    "nicparams",
909
    "candidate_pool_size",
910
    "modify_etc_hosts",
911
    "modify_ssh_setup",
912
    "maintain_node_health",
913
    "uid_pool",
914
    ] + _TIMESTAMPS + _UUID
915

    
916
  def UpgradeConfig(self):
917
    """Fill defaults for missing configuration values.
918

919
    """
920
    # pylint: disable-msg=E0203
921
    # because these are "defined" via slots, not manually
922
    if self.hvparams is None:
923
      self.hvparams = constants.HVC_DEFAULTS
924
    else:
925
      for hypervisor in self.hvparams:
926
        self.hvparams[hypervisor] = FillDict(
927
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
928

    
929
    if self.os_hvp is None:
930
      self.os_hvp = {}
931

    
932
    # osparams added before 2.2
933
    if self.osparams is None:
934
      self.osparams = {}
935

    
936
    self.beparams = UpgradeGroupedParams(self.beparams,
937
                                         constants.BEC_DEFAULTS)
938
    migrate_default_bridge = not self.nicparams
939
    self.nicparams = UpgradeGroupedParams(self.nicparams,
940
                                          constants.NICC_DEFAULTS)
941
    if migrate_default_bridge:
942
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
943
        self.default_bridge
944

    
945
    if self.modify_etc_hosts is None:
946
      self.modify_etc_hosts = True
947

    
948
    if self.modify_ssh_setup is None:
949
      self.modify_ssh_setup = True
950

    
951
    # default_bridge is no longer used it 2.1. The slot is left there to
952
    # support auto-upgrading. It can be removed once we decide to deprecate
953
    # upgrading straight from 2.0.
954
    if self.default_bridge is not None:
955
      self.default_bridge = None
956

    
957
    # default_hypervisor is just the first enabled one in 2.1. This slot and
958
    # code can be removed once upgrading straight from 2.0 is deprecated.
959
    if self.default_hypervisor is not None:
960
      self.enabled_hypervisors = ([self.default_hypervisor] +
961
        [hvname for hvname in self.enabled_hypervisors
962
         if hvname != self.default_hypervisor])
963
      self.default_hypervisor = None
964

    
965
    # maintain_node_health added after 2.1.1
966
    if self.maintain_node_health is None:
967
      self.maintain_node_health = False
968

    
969
    if self.uid_pool is None:
970
      self.uid_pool = []
971

    
972
  def ToDict(self):
973
    """Custom function for cluster.
974

975
    """
976
    mydict = super(Cluster, self).ToDict()
977
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
978
    return mydict
979

    
980
  @classmethod
981
  def FromDict(cls, val):
982
    """Custom function for cluster.
983

984
    """
985
    obj = super(Cluster, cls).FromDict(val)
986
    if not isinstance(obj.tcpudp_port_pool, set):
987
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
988
    return obj
989

    
990
  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
991
    """Get the default hypervisor parameters for the cluster.
992

993
    @param hypervisor: the hypervisor name
994
    @param os_name: if specified, we'll also update the defaults for this OS
995
    @param skip_keys: if passed, list of keys not to use
996
    @return: the defaults dict
997

998
    """
999
    if skip_keys is None:
1000
      skip_keys = []
1001

    
1002
    fill_stack = [self.hvparams.get(hypervisor, {})]
1003
    if os_name is not None:
1004
      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1005
      fill_stack.append(os_hvp)
1006

    
1007
    ret_dict = {}
1008
    for o_dict in fill_stack:
1009
      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1010

    
1011
    return ret_dict
1012

    
1013
  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1014
    """Fill a given hvparams dict with cluster defaults.
1015

1016
    @type hv_name: string
1017
    @param hv_name: the hypervisor to use
1018
    @type os_name: string
1019
    @param os_name: the OS to use for overriding the hypervisor defaults
1020
    @type skip_globals: boolean
1021
    @param skip_globals: if True, the global hypervisor parameters will
1022
        not be filled
1023
    @rtype: dict
1024
    @return: a copy of the given hvparams with missing keys filled from
1025
        the cluster defaults
1026

1027
    """
1028
    if skip_globals:
1029
      skip_keys = constants.HVC_GLOBALS
1030
    else:
1031
      skip_keys = []
1032

    
1033
    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1034
    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1035

    
1036
  def FillHV(self, instance, skip_globals=False):
1037
    """Fill an instance's hvparams dict with cluster defaults.
1038

1039
    @type instance: L{objects.Instance}
1040
    @param instance: the instance parameter to fill
1041
    @type skip_globals: boolean
1042
    @param skip_globals: if True, the global hypervisor parameters will
1043
        not be filled
1044
    @rtype: dict
1045
    @return: a copy of the instance's hvparams with missing keys filled from
1046
        the cluster defaults
1047

1048
    """
1049
    return self.SimpleFillHV(instance.hypervisor, instance.os,
1050
                             instance.hvparams, skip_globals)
1051

    
1052
  def SimpleFillBE(self, beparams):
1053
    """Fill a given beparams dict with cluster defaults.
1054

1055
    @type beparams: dict
1056
    @param beparams: the dict to fill
1057
    @rtype: dict
1058
    @return: a copy of the passed in beparams with missing keys filled
1059
        from the cluster defaults
1060

1061
    """
1062
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1063

    
1064
  def FillBE(self, instance):
1065
    """Fill an instance's beparams dict with cluster defaults.
1066

1067
    @type instance: L{objects.Instance}
1068
    @param instance: the instance parameter to fill
1069
    @rtype: dict
1070
    @return: a copy of the instance's beparams with missing keys filled from
1071
        the cluster defaults
1072

1073
    """
1074
    return self.SimpleFillBE(instance.beparams)
1075

    
1076
  def SimpleFillNIC(self, nicparams):
1077
    """Fill a given nicparams dict with cluster defaults.
1078

1079
    @type nicparams: dict
1080
    @param nicparams: the dict to fill
1081
    @rtype: dict
1082
    @return: a copy of the passed in nicparams with missing keys filled
1083
        from the cluster defaults
1084

1085
    """
1086
    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1087

    
1088
  def SimpleFillOS(self, os_name, os_params):
1089
    """Fill an instance's osparams dict with cluster defaults.
1090

1091
    @type os_name: string
1092
    @param os_name: the OS name to use
1093
    @type os_params: dict
1094
    @param os_params: the dict to fill with default values
1095
    @rtype: dict
1096
    @return: a copy of the instance's osparams with missing keys filled from
1097
        the cluster defaults
1098

1099
    """
1100
    name_only = os_name.split("+", 1)[0]
1101
    # base OS
1102
    result = self.osparams.get(name_only, {})
1103
    # OS with variant
1104
    result = FillDict(result, self.osparams.get(os_name, {}))
1105
    # specified params
1106
    return FillDict(result, os_params)
1107

    
1108

    
1109
class BlockDevStatus(ConfigObject):
1110
  """Config object representing the status of a block device."""
1111
  __slots__ = [
1112
    "dev_path",
1113
    "major",
1114
    "minor",
1115
    "sync_percent",
1116
    "estimated_time",
1117
    "is_degraded",
1118
    "ldisk_status",
1119
    ]
1120

    
1121

    
1122
class ImportExportStatus(ConfigObject):
1123
  """Config object representing the status of an import or export."""
1124
  __slots__ = [
1125
    "recent_output",
1126
    "listen_port",
1127
    "connected",
1128
    "progress_mbytes",
1129
    "progress_throughput",
1130
    "progress_eta",
1131
    "progress_percent",
1132
    "exit_status",
1133
    "error_message",
1134
    ] + _TIMESTAMPS
1135

    
1136

    
1137
class ImportExportOptions(ConfigObject):
1138
  """Options for import/export daemon
1139

1140
  @ivar key_name: X509 key name (None for cluster certificate)
1141
  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1142
  @ivar compress: Compression method (one of L{constants.IEC_ALL})
1143
  @ivar magic: Used to ensure the connection goes to the right disk
1144

1145
  """
1146
  __slots__ = [
1147
    "key_name",
1148
    "ca_pem",
1149
    "compress",
1150
    "magic",
1151
    ]
1152

    
1153

    
1154
class ConfdRequest(ConfigObject):
1155
  """Object holding a confd request.
1156

1157
  @ivar protocol: confd protocol version
1158
  @ivar type: confd query type
1159
  @ivar query: query request
1160
  @ivar rsalt: requested reply salt
1161

1162
  """
1163
  __slots__ = [
1164
    "protocol",
1165
    "type",
1166
    "query",
1167
    "rsalt",
1168
    ]
1169

    
1170

    
1171
class ConfdReply(ConfigObject):
1172
  """Object holding a confd reply.
1173

1174
  @ivar protocol: confd protocol version
1175
  @ivar status: reply status code (ok, error)
1176
  @ivar answer: confd query reply
1177
  @ivar serial: configuration serial number
1178

1179
  """
1180
  __slots__ = [
1181
    "protocol",
1182
    "status",
1183
    "answer",
1184
    "serial",
1185
    ]
1186

    
1187

    
1188
class SerializableConfigParser(ConfigParser.SafeConfigParser):
1189
  """Simple wrapper over ConfigParse that allows serialization.
1190

1191
  This class is basically ConfigParser.SafeConfigParser with two
1192
  additional methods that allow it to serialize/unserialize to/from a
1193
  buffer.
1194

1195
  """
1196
  def Dumps(self):
1197
    """Dump this instance and return the string representation."""
1198
    buf = StringIO()
1199
    self.write(buf)
1200
    return buf.getvalue()
1201

    
1202
  @classmethod
1203
  def Loads(cls, data):
1204
    """Load data from a string."""
1205
    buf = StringIO(data)
1206
    cfp = cls()
1207
    cfp.readfp(buf)
1208
    return cfp