Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 6c881c52

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

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

    
40
from ganeti import errors
41
from ganeti import constants
42

    
43

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

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

    
50
def FillDict(defaults_dict, custom_dict, skip_keys=[]):
51
  """Basic function to apply settings on top a default dict.
52

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

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

    
72

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

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

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

    
89

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

93
  It has the following properties:
94

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

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

102
  """
103
  __slots__ = []
104

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

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

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

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

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

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

    
137
  __getstate__ = ToDict
138

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

216
    """
217
    pass
218

    
219

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

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

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

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

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

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

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

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

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

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

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

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

278
    This replaces the tags set with a list.
279

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

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

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

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

    
298

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

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

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

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

    
316
    return mydict
317

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

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

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

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

    
339

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

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

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

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

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

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

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

    
376

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
633

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

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

654
    This is a simple wrapper over _ComputeAllNodes.
655

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    
731
    return ret
732

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

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

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

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

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

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

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

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

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

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

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

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

    
799

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

    
813

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

    
826

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

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

855
    """
856
    if self.hvparams is None:
857
      self.hvparams = constants.HVC_DEFAULTS
858
    else:
859
      for hypervisor in self.hvparams:
860
        self.hvparams[hypervisor] = FillDict(
861
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
862

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

    
872
    if self.modify_etc_hosts is None:
873
      self.modify_etc_hosts = True
874

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

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

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

    
890
  def ToDict(self):
891
    """Custom function for cluster.
892

893
    """
894
    mydict = super(Cluster, self).ToDict()
895
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
896
    return mydict
897

    
898
  @classmethod
899
  def FromDict(cls, val):
900
    """Custom function for cluster.
901

902
    """
903
    obj = super(Cluster, cls).FromDict(val)
904
    if not isinstance(obj.tcpudp_port_pool, set):
905
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
906
    return obj
907

    
908
  def FillHV(self, instance, skip_globals=False):
909
    """Fill an instance's hvparams dict.
910

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

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

    
928
  def FillBE(self, instance):
929
    """Fill an instance's beparams dict.
930

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

937
    """
938
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
939
                    instance.beparams)
940

    
941

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

    
954

    
955
class ConfdRequest(ConfigObject):
956
  """Object holding a confd request.
957

958
  @ivar protocol: confd protocol version
959
  @ivar type: confd query type
960
  @ivar query: query request
961
  @ivar rsalt: requested reply salt
962

963
  """
964
  __slots__ = [
965
    "protocol",
966
    "type",
967
    "query",
968
    "rsalt",
969
    ]
970

    
971

    
972
class ConfdReply(ConfigObject):
973
  """Object holding a confd reply.
974

975
  @ivar protocol: confd protocol version
976
  @ivar status: reply status code (ok, error)
977
  @ivar answer: confd query reply
978
  @ivar serial: configuration serial number
979

980
  """
981
  __slots__ = [
982
    "protocol",
983
    "status",
984
    "answer",
985
    "serial",
986
    ]
987

    
988

    
989
class SerializableConfigParser(ConfigParser.SafeConfigParser):
990
  """Simple wrapper over ConfigParse that allows serialization.
991

992
  This class is basically ConfigParser.SafeConfigParser with two
993
  additional methods that allow it to serialize/unserialize to/from a
994
  buffer.
995

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

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