Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 7260cfbe

History | View | Annotate | Download (29.5 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
def FillDict(defaults_dict, custom_dict, skip_keys=[]):
52
  """Basic function to apply settings on top a default dict.
53

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

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

    
73

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

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

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

    
90

    
91
class ConfigObject(object):
92
  """A generic config object.
93

94
  It has the following properties:
95

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

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

103
  """
104
  __slots__ = []
105

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

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

    
116
  def __setstate__(self, state):
117
    for name in state:
118
      if name in self.__slots__:
119
        setattr(self, name, state[name])
120

    
121
  def ToDict(self):
122
    """Convert to a dict holding only standard python types.
123

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

130
    """
131
    result = {}
132
    for name in self.__slots__:
133
      value = getattr(self, name, None)
134
      if value is not None:
135
        result[name] = value
136
    return result
137

    
138
  __getstate__ = ToDict
139

    
140
  @classmethod
141
  def FromDict(cls, val):
142
    """Create an object from a dictionary.
143

144
    This generic routine takes a dict, instantiates a new instance of
145
    the given class, and sets attributes based on the dict content.
146

147
    As for `ToDict`, this does not work if the class has children
148
    who are ConfigObjects themselves (e.g. the nics list in an
149
    Instance), in which case the object should subclass the function
150
    and alter the objects.
151

152
    """
153
    if not isinstance(val, dict):
154
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
155
                                      " expected dict, got %s" % type(val))
156
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
157
    obj = cls(**val_str) # pylint: disable-msg=W0142
158
    return obj
159

    
160
  @staticmethod
161
  def _ContainerToDicts(container):
162
    """Convert the elements of a container to standard python types.
163

164
    This method converts a container with elements derived from
165
    ConfigData to standard python types. If the container is a dict,
166
    we don't touch the keys, only the values.
167

168
    """
169
    if isinstance(container, dict):
170
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
171
    elif isinstance(container, (list, tuple, set, frozenset)):
172
      ret = [elem.ToDict() for elem in container]
173
    else:
174
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
175
                      type(container))
176
    return ret
177

    
178
  @staticmethod
179
  def _ContainerFromDicts(source, c_type, e_type):
180
    """Convert a container from standard python types.
181

182
    This method converts a container with standard python types to
183
    ConfigData objects. If the container is a dict, we don't touch the
184
    keys, only the values.
185

186
    """
187
    if not isinstance(c_type, type):
188
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
189
                      " not a type" % type(c_type))
190
    if c_type is dict:
191
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
192
    elif c_type in (list, tuple, set, frozenset):
193
      ret = c_type([e_type.FromDict(elem) for elem in source])
194
    else:
195
      raise TypeError("Invalid container type %s passed to"
196
                      " _ContainerFromDicts" % c_type)
197
    return ret
198

    
199
  def Copy(self):
200
    """Makes a deep copy of the current object and its children.
201

202
    """
203
    dict_form = self.ToDict()
204
    clone_obj = self.__class__.FromDict(dict_form)
205
    return clone_obj
206

    
207
  def __repr__(self):
208
    """Implement __repr__ for ConfigObjects."""
209
    return repr(self.ToDict())
210

    
211
  def UpgradeConfig(self):
212
    """Fill defaults for missing configuration values.
213

214
    This method will be called at configuration load time, and its
215
    implementation will be object dependent.
216

217
    """
218
    pass
219

    
220

    
221
class TaggableObject(ConfigObject):
222
  """An generic class supporting tags.
223

224
  """
225
  __slots__ = ConfigObject.__slots__ + ["tags"]
226
  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
227

    
228
  @classmethod
229
  def ValidateTag(cls, tag):
230
    """Check if a tag is valid.
231

232
    If the tag is invalid, an errors.TagError will be raised. The
233
    function has no return value.
234

235
    """
236
    if not isinstance(tag, basestring):
237
      raise errors.TagError("Invalid tag type (not a string)")
238
    if len(tag) > constants.MAX_TAG_LEN:
239
      raise errors.TagError("Tag too long (>%d characters)" %
240
                            constants.MAX_TAG_LEN)
241
    if not tag:
242
      raise errors.TagError("Tags cannot be empty")
243
    if not cls.VALID_TAG_RE.match(tag):
244
      raise errors.TagError("Tag contains invalid characters")
245

    
246
  def GetTags(self):
247
    """Return the tags list.
248

249
    """
250
    tags = getattr(self, "tags", None)
251
    if tags is None:
252
      tags = self.tags = set()
253
    return tags
254

    
255
  def AddTag(self, tag):
256
    """Add a new tag.
257

258
    """
259
    self.ValidateTag(tag)
260
    tags = self.GetTags()
261
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
262
      raise errors.TagError("Too many tags")
263
    self.GetTags().add(tag)
264

    
265
  def RemoveTag(self, tag):
266
    """Remove a tag.
267

268
    """
269
    self.ValidateTag(tag)
270
    tags = self.GetTags()
271
    try:
272
      tags.remove(tag)
273
    except KeyError:
274
      raise errors.TagError("Tag not found")
275

    
276
  def ToDict(self):
277
    """Taggable-object-specific conversion to standard python types.
278

279
    This replaces the tags set with a list.
280

281
    """
282
    bo = super(TaggableObject, self).ToDict()
283

    
284
    tags = bo.get("tags", None)
285
    if isinstance(tags, set):
286
      bo["tags"] = list(tags)
287
    return bo
288

    
289
  @classmethod
290
  def FromDict(cls, val):
291
    """Custom function for instances.
292

293
    """
294
    obj = super(TaggableObject, cls).FromDict(val)
295
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
296
      obj.tags = set(obj.tags)
297
    return obj
298

    
299

    
300
class ConfigData(ConfigObject):
301
  """Top-level config object."""
302
  __slots__ = (["version", "cluster", "nodes", "instances", "serial_no"] +
303
               _TIMESTAMPS)
304

    
305
  def ToDict(self):
306
    """Custom function for top-level config data.
307

308
    This just replaces the list of instances, nodes and the cluster
309
    with standard python types.
310

311
    """
312
    mydict = super(ConfigData, self).ToDict()
313
    mydict["cluster"] = mydict["cluster"].ToDict()
314
    for key in "nodes", "instances":
315
      mydict[key] = self._ContainerToDicts(mydict[key])
316

    
317
    return mydict
318

    
319
  @classmethod
320
  def FromDict(cls, val):
321
    """Custom function for top-level config data
322

323
    """
324
    obj = super(ConfigData, cls).FromDict(val)
325
    obj.cluster = Cluster.FromDict(obj.cluster)
326
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
327
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
328
    return obj
329

    
330
  def UpgradeConfig(self):
331
    """Fill defaults for missing configuration values.
332

333
    """
334
    self.cluster.UpgradeConfig()
335
    for node in self.nodes.values():
336
      node.UpgradeConfig()
337
    for instance in self.instances.values():
338
      instance.UpgradeConfig()
339

    
340

    
341
class NIC(ConfigObject):
342
  """Config object representing a network card."""
343
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
344

    
345
  @classmethod
346
  def CheckParameterSyntax(cls, nicparams):
347
    """Check the given parameters for validity.
348

349
    @type nicparams:  dict
350
    @param nicparams: dictionary with parameter names/value
351
    @raise errors.ConfigurationError: when a parameter is not valid
352

353
    """
354
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
355
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
356
      raise errors.ConfigurationError(err)
357

    
358
    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
359
        not nicparams[constants.NIC_LINK]):
360
      err = "Missing bridged nic link"
361
      raise errors.ConfigurationError(err)
362

    
363
  def UpgradeConfig(self):
364
    """Fill defaults for missing configuration values.
365

366
    """
367
    if self.nicparams is None:
368
      self.nicparams = {}
369
      if self.bridge is not None:
370
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
371
        self.nicparams[constants.NIC_LINK] = self.bridge
372
    # bridge is no longer used it 2.1. The slot is left there to support
373
    # upgrading, but will be removed in 2.2
374
    if self.bridge is not None:
375
      self.bridge = None
376

    
377

    
378
class Disk(ConfigObject):
379
  """Config object representing a block device."""
380
  __slots__ = ["dev_type", "logical_id", "physical_id",
381
               "children", "iv_name", "size", "mode"]
382

    
383
  def CreateOnSecondary(self):
384
    """Test if this device needs to be created on a secondary node."""
385
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
386

    
387
  def AssembleOnSecondary(self):
388
    """Test if this device needs to be assembled on a secondary node."""
389
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
390

    
391
  def OpenOnSecondary(self):
392
    """Test if this device needs to be opened on a secondary node."""
393
    return self.dev_type in (constants.LD_LV,)
394

    
395
  def StaticDevPath(self):
396
    """Return the device path if this device type has a static one.
397

398
    Some devices (LVM for example) live always at the same /dev/ path,
399
    irrespective of their status. For such devices, we return this
400
    path, for others we return None.
401

402
    """
403
    if self.dev_type == constants.LD_LV:
404
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
405
    return None
406

    
407
  def ChildrenNeeded(self):
408
    """Compute the needed number of children for activation.
409

410
    This method will return either -1 (all children) or a positive
411
    number denoting the minimum number of children needed for
412
    activation (only mirrored devices will usually return >=0).
413

414
    Currently, only DRBD8 supports diskless activation (therefore we
415
    return 0), for all other we keep the previous semantics and return
416
    -1.
417

418
    """
419
    if self.dev_type == constants.LD_DRBD8:
420
      return 0
421
    return -1
422

    
423
  def GetNodes(self, node):
424
    """This function returns the nodes this device lives on.
425

426
    Given the node on which the parent of the device lives on (or, in
427
    case of a top-level device, the primary node of the devices'
428
    instance), this function will return a list of nodes on which this
429
    devices needs to (or can) be assembled.
430

431
    """
432
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
433
      result = [node]
434
    elif self.dev_type in constants.LDS_DRBD:
435
      result = [self.logical_id[0], self.logical_id[1]]
436
      if node not in result:
437
        raise errors.ConfigurationError("DRBD device passed unknown node")
438
    else:
439
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
440
    return result
441

    
442
  def ComputeNodeTree(self, parent_node):
443
    """Compute the node/disk tree for this disk and its children.
444

445
    This method, given the node on which the parent disk lives, will
446
    return the list of all (node, disk) pairs which describe the disk
447
    tree in the most compact way. For example, a drbd/lvm stack
448
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
449
    which represents all the top-level devices on the nodes.
450

451
    """
452
    my_nodes = self.GetNodes(parent_node)
453
    result = [(node, self) for node in my_nodes]
454
    if not self.children:
455
      # leaf device
456
      return result
457
    for node in my_nodes:
458
      for child in self.children:
459
        child_result = child.ComputeNodeTree(node)
460
        if len(child_result) == 1:
461
          # child (and all its descendants) is simple, doesn't split
462
          # over multiple hosts, so we don't need to describe it, our
463
          # own entry for this node describes it completely
464
          continue
465
        else:
466
          # check if child nodes differ from my nodes; note that
467
          # subdisk can differ from the child itself, and be instead
468
          # one of its descendants
469
          for subnode, subdisk in child_result:
470
            if subnode not in my_nodes:
471
              result.append((subnode, subdisk))
472
            # otherwise child is under our own node, so we ignore this
473
            # entry (but probably the other results in the list will
474
            # be different)
475
    return result
476

    
477
  def RecordGrow(self, amount):
478
    """Update the size of this disk after growth.
479

480
    This method recurses over the disks's children and updates their
481
    size correspondigly. The method needs to be kept in sync with the
482
    actual algorithms from bdev.
483

484
    """
485
    if self.dev_type == constants.LD_LV:
486
      self.size += amount
487
    elif self.dev_type == constants.LD_DRBD8:
488
      if self.children:
489
        self.children[0].RecordGrow(amount)
490
      self.size += amount
491
    else:
492
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
493
                                   " disk type %s" % self.dev_type)
494

    
495
  def UnsetSize(self):
496
    """Sets recursively the size to zero for the disk and its children.
497

498
    """
499
    if self.children:
500
      for child in self.children:
501
        child.UnsetSize()
502
    self.size = 0
503

    
504
  def SetPhysicalID(self, target_node, nodes_ip):
505
    """Convert the logical ID to the physical ID.
506

507
    This is used only for drbd, which needs ip/port configuration.
508

509
    The routine descends down and updates its children also, because
510
    this helps when the only the top device is passed to the remote
511
    node.
512

513
    Arguments:
514
      - target_node: the node we wish to configure for
515
      - nodes_ip: a mapping of node name to ip
516

517
    The target_node must exist in in nodes_ip, and must be one of the
518
    nodes in the logical ID for each of the DRBD devices encountered
519
    in the disk tree.
520

521
    """
522
    if self.children:
523
      for child in self.children:
524
        child.SetPhysicalID(target_node, nodes_ip)
525

    
526
    if self.logical_id is None and self.physical_id is not None:
527
      return
528
    if self.dev_type in constants.LDS_DRBD:
529
      pnode, snode, port, pminor, sminor, secret = self.logical_id
530
      if target_node not in (pnode, snode):
531
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
532
                                        target_node)
533
      pnode_ip = nodes_ip.get(pnode, None)
534
      snode_ip = nodes_ip.get(snode, None)
535
      if pnode_ip is None or snode_ip is None:
536
        raise errors.ConfigurationError("Can't find primary or secondary node"
537
                                        " for %s" % str(self))
538
      p_data = (pnode_ip, port)
539
      s_data = (snode_ip, port)
540
      if pnode == target_node:
541
        self.physical_id = p_data + s_data + (pminor, secret)
542
      else: # it must be secondary, we tested above
543
        self.physical_id = s_data + p_data + (sminor, secret)
544
    else:
545
      self.physical_id = self.logical_id
546
    return
547

    
548
  def ToDict(self):
549
    """Disk-specific conversion to standard python types.
550

551
    This replaces the children lists of objects with lists of
552
    standard python types.
553

554
    """
555
    bo = super(Disk, self).ToDict()
556

    
557
    for attr in ("children",):
558
      alist = bo.get(attr, None)
559
      if alist:
560
        bo[attr] = self._ContainerToDicts(alist)
561
    return bo
562

    
563
  @classmethod
564
  def FromDict(cls, val):
565
    """Custom function for Disks
566

567
    """
568
    obj = super(Disk, cls).FromDict(val)
569
    if obj.children:
570
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
571
    if obj.logical_id and isinstance(obj.logical_id, list):
572
      obj.logical_id = tuple(obj.logical_id)
573
    if obj.physical_id and isinstance(obj.physical_id, list):
574
      obj.physical_id = tuple(obj.physical_id)
575
    if obj.dev_type in constants.LDS_DRBD:
576
      # we need a tuple of length six here
577
      if len(obj.logical_id) < 6:
578
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
579
    return obj
580

    
581
  def __str__(self):
582
    """Custom str() formatter for disks.
583

584
    """
585
    if self.dev_type == constants.LD_LV:
586
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
587
    elif self.dev_type in constants.LDS_DRBD:
588
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
589
      val = "<DRBD8("
590
      if self.physical_id is None:
591
        phy = "unconfigured"
592
      else:
593
        phy = ("configured as %s:%s %s:%s" %
594
               (self.physical_id[0], self.physical_id[1],
595
                self.physical_id[2], self.physical_id[3]))
596

    
597
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
598
              (node_a, minor_a, node_b, minor_b, port, phy))
599
      if self.children and self.children.count(None) == 0:
600
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
601
      else:
602
        val += "no local storage"
603
    else:
604
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
605
             (self.dev_type, self.logical_id, self.physical_id, self.children))
606
    if self.iv_name is None:
607
      val += ", not visible"
608
    else:
609
      val += ", visible as /dev/%s" % self.iv_name
610
    if isinstance(self.size, int):
611
      val += ", size=%dm)>" % self.size
612
    else:
613
      val += ", size='%s')>" % (self.size,)
614
    return val
615

    
616
  def Verify(self):
617
    """Checks that this disk is correctly configured.
618

619
    """
620
    all_errors = []
621
    if self.mode not in constants.DISK_ACCESS_SET:
622
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
623
    return all_errors
624

    
625
  def UpgradeConfig(self):
626
    """Fill defaults for missing configuration values.
627

628
    """
629
    if self.children:
630
      for child in self.children:
631
        child.UpgradeConfig()
632
    # add here config upgrade for this disk
633

    
634

    
635
class Instance(TaggableObject):
636
  """Config object representing an instance."""
637
  __slots__ = TaggableObject.__slots__ + [
638
    "name",
639
    "primary_node",
640
    "os",
641
    "hypervisor",
642
    "hvparams",
643
    "beparams",
644
    "admin_up",
645
    "nics",
646
    "disks",
647
    "disk_template",
648
    "network_port",
649
    "serial_no",
650
    ] + _TIMESTAMPS + _UUID
651

    
652
  def _ComputeSecondaryNodes(self):
653
    """Compute the list of secondary nodes.
654

655
    This is a simple wrapper over _ComputeAllNodes.
656

657
    """
658
    all_nodes = set(self._ComputeAllNodes())
659
    all_nodes.discard(self.primary_node)
660
    return tuple(all_nodes)
661

    
662
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
663
                             "List of secondary nodes")
664

    
665
  def _ComputeAllNodes(self):
666
    """Compute the list of all nodes.
667

668
    Since the data is already there (in the drbd disks), keeping it as
669
    a separate normal attribute is redundant and if not properly
670
    synchronised can cause problems. Thus it's better to compute it
671
    dynamically.
672

673
    """
674
    def _Helper(nodes, device):
675
      """Recursively computes nodes given a top device."""
676
      if device.dev_type in constants.LDS_DRBD:
677
        nodea, nodeb = device.logical_id[:2]
678
        nodes.add(nodea)
679
        nodes.add(nodeb)
680
      if device.children:
681
        for child in device.children:
682
          _Helper(nodes, child)
683

    
684
    all_nodes = set()
685
    all_nodes.add(self.primary_node)
686
    for device in self.disks:
687
      _Helper(all_nodes, device)
688
    return tuple(all_nodes)
689

    
690
  all_nodes = property(_ComputeAllNodes, None, None,
691
                       "List of all nodes of the instance")
692

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

696
    This function figures out what logical volumes should belong on
697
    which nodes, recursing through a device tree.
698

699
    @param lvmap: optional dictionary to receive the
700
        'node' : ['lv', ...] data.
701

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

705
    """
706
    if node == None:
707
      node = self.primary_node
708

    
709
    if lvmap is None:
710
      lvmap = { node : [] }
711
      ret = lvmap
712
    else:
713
      if not node in lvmap:
714
        lvmap[node] = []
715
      ret = None
716

    
717
    if not devs:
718
      devs = self.disks
719

    
720
    for dev in devs:
721
      if dev.dev_type == constants.LD_LV:
722
        lvmap[node].append(dev.logical_id[1])
723

    
724
      elif dev.dev_type in constants.LDS_DRBD:
725
        if dev.children:
726
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
727
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
728

    
729
      elif dev.children:
730
        self.MapLVsByNode(lvmap, dev.children, node)
731

    
732
    return ret
733

    
734
  def FindDisk(self, idx):
735
    """Find a disk given having a specified index.
736

737
    This is just a wrapper that does validation of the index.
738

739
    @type idx: int
740
    @param idx: the disk index
741
    @rtype: L{Disk}
742
    @return: the corresponding disk
743
    @raise errors.OpPrereqError: when the given index is not valid
744

745
    """
746
    try:
747
      idx = int(idx)
748
      return self.disks[idx]
749
    except ValueError, err:
750
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
751
                                 errors.ECODE_INVAL)
752
    except IndexError:
753
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
754
                                 " 0 to %d" % (idx, len(self.disks)),
755
                                 errors.ECODE_INVAL)
756

    
757
  def ToDict(self):
758
    """Instance-specific conversion to standard python types.
759

760
    This replaces the children lists of objects with lists of standard
761
    python types.
762

763
    """
764
    bo = super(Instance, self).ToDict()
765

    
766
    for attr in "nics", "disks":
767
      alist = bo.get(attr, None)
768
      if alist:
769
        nlist = self._ContainerToDicts(alist)
770
      else:
771
        nlist = []
772
      bo[attr] = nlist
773
    return bo
774

    
775
  @classmethod
776
  def FromDict(cls, val):
777
    """Custom function for instances.
778

779
    """
780
    obj = super(Instance, cls).FromDict(val)
781
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
782
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
783
    return obj
784

    
785
  def UpgradeConfig(self):
786
    """Fill defaults for missing configuration values.
787

788
    """
789
    for nic in self.nics:
790
      nic.UpgradeConfig()
791
    for disk in self.disks:
792
      disk.UpgradeConfig()
793
    if self.hvparams:
794
      for key in constants.HVC_GLOBALS:
795
        try:
796
          del self.hvparams[key]
797
        except KeyError:
798
          pass
799

    
800

    
801
class OS(ConfigObject):
802
  """Config object representing an operating system."""
803
  __slots__ = [
804
    "name",
805
    "path",
806
    "api_versions",
807
    "create_script",
808
    "export_script",
809
    "import_script",
810
    "rename_script",
811
    "supported_variants",
812
    ]
813

    
814

    
815
class Node(TaggableObject):
816
  """Config object representing a node."""
817
  __slots__ = TaggableObject.__slots__ + [
818
    "name",
819
    "primary_ip",
820
    "secondary_ip",
821
    "serial_no",
822
    "master_candidate",
823
    "offline",
824
    "drained",
825
    ] + _TIMESTAMPS + _UUID
826

    
827

    
828
class Cluster(TaggableObject):
829
  """Config object representing the cluster."""
830
  __slots__ = TaggableObject.__slots__ + [
831
    "serial_no",
832
    "rsahostkeypub",
833
    "highest_used_port",
834
    "tcpudp_port_pool",
835
    "mac_prefix",
836
    "volume_group_name",
837
    "default_bridge",
838
    "default_hypervisor",
839
    "master_node",
840
    "master_ip",
841
    "master_netdev",
842
    "cluster_name",
843
    "file_storage_dir",
844
    "enabled_hypervisors",
845
    "hvparams",
846
    "beparams",
847
    "nicparams",
848
    "candidate_pool_size",
849
    "modify_etc_hosts",
850
    "modify_ssh_setup",
851
    ] + _TIMESTAMPS + _UUID
852

    
853
  def UpgradeConfig(self):
854
    """Fill defaults for missing configuration values.
855

856
    """
857
    # pylint: disable-msg=E0203
858
    # because these are "defined" via slots, not manually
859
    if self.hvparams is None:
860
      self.hvparams = constants.HVC_DEFAULTS
861
    else:
862
      for hypervisor in self.hvparams:
863
        self.hvparams[hypervisor] = FillDict(
864
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
865

    
866
    self.beparams = UpgradeGroupedParams(self.beparams,
867
                                         constants.BEC_DEFAULTS)
868
    migrate_default_bridge = not self.nicparams
869
    self.nicparams = UpgradeGroupedParams(self.nicparams,
870
                                          constants.NICC_DEFAULTS)
871
    if migrate_default_bridge:
872
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
873
        self.default_bridge
874

    
875
    if self.modify_etc_hosts is None:
876
      self.modify_etc_hosts = True
877

    
878
    if self.modify_ssh_setup is None:
879
      self.modify_ssh_setup = True
880

    
881
    # default_bridge is no longer used it 2.1. The slot is left there to
882
    # support auto-upgrading, but will be removed in 2.2
883
    if self.default_bridge is not None:
884
      self.default_bridge = None
885

    
886
    # default_hypervisor is just the first enabled one in 2.1
887
    if self.default_hypervisor is not None:
888
      self.enabled_hypervisors = ([self.default_hypervisor] +
889
        [hvname for hvname in self.enabled_hypervisors
890
         if hvname != self.default_hypervisor])
891
      self.default_hypervisor = None
892

    
893
  def ToDict(self):
894
    """Custom function for cluster.
895

896
    """
897
    mydict = super(Cluster, self).ToDict()
898
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
899
    return mydict
900

    
901
  @classmethod
902
  def FromDict(cls, val):
903
    """Custom function for cluster.
904

905
    """
906
    obj = super(Cluster, cls).FromDict(val)
907
    if not isinstance(obj.tcpudp_port_pool, set):
908
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
909
    return obj
910

    
911
  def FillHV(self, instance, skip_globals=False):
912
    """Fill an instance's hvparams dict.
913

914
    @type instance: L{objects.Instance}
915
    @param instance: the instance parameter to fill
916
    @type skip_globals: boolean
917
    @param skip_globals: if True, the global hypervisor parameters will
918
        not be filled
919
    @rtype: dict
920
    @return: a copy of the instance's hvparams with missing keys filled from
921
        the cluster defaults
922

923
    """
924
    if skip_globals:
925
      skip_keys = constants.HVC_GLOBALS
926
    else:
927
      skip_keys = []
928
    return FillDict(self.hvparams.get(instance.hypervisor, {}),
929
                    instance.hvparams, skip_keys=skip_keys)
930

    
931
  def FillBE(self, instance):
932
    """Fill an instance's beparams dict.
933

934
    @type instance: L{objects.Instance}
935
    @param instance: the instance parameter to fill
936
    @rtype: dict
937
    @return: a copy of the instance's beparams with missing keys filled from
938
        the cluster defaults
939

940
    """
941
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
942
                    instance.beparams)
943

    
944

    
945
class BlockDevStatus(ConfigObject):
946
  """Config object representing the status of a block device."""
947
  __slots__ = [
948
    "dev_path",
949
    "major",
950
    "minor",
951
    "sync_percent",
952
    "estimated_time",
953
    "is_degraded",
954
    "ldisk_status",
955
    ]
956

    
957

    
958
class ConfdRequest(ConfigObject):
959
  """Object holding a confd request.
960

961
  @ivar protocol: confd protocol version
962
  @ivar type: confd query type
963
  @ivar query: query request
964
  @ivar rsalt: requested reply salt
965

966
  """
967
  __slots__ = [
968
    "protocol",
969
    "type",
970
    "query",
971
    "rsalt",
972
    ]
973

    
974

    
975
class ConfdReply(ConfigObject):
976
  """Object holding a confd reply.
977

978
  @ivar protocol: confd protocol version
979
  @ivar status: reply status code (ok, error)
980
  @ivar answer: confd query reply
981
  @ivar serial: configuration serial number
982

983
  """
984
  __slots__ = [
985
    "protocol",
986
    "status",
987
    "answer",
988
    "serial",
989
    ]
990

    
991

    
992
class SerializableConfigParser(ConfigParser.SafeConfigParser):
993
  """Simple wrapper over ConfigParse that allows serialization.
994

995
  This class is basically ConfigParser.SafeConfigParser with two
996
  additional methods that allow it to serialize/unserialize to/from a
997
  buffer.
998

999
  """
1000
  def Dumps(self):
1001
    """Dump this instance and return the string representation."""
1002
    buf = StringIO()
1003
    self.write(buf)
1004
    return buf.getvalue()
1005

    
1006
  @staticmethod
1007
  def Loads(data):
1008
    """Load data from a string."""
1009
    buf = StringIO(data)
1010
    cfp = SerializableConfigParser()
1011
    cfp.readfp(buf)
1012
    return cfp