Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ e978484a

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

    
30
import ConfigParser
31
import re
32
import copy
33
from cStringIO import StringIO
34

    
35
from ganeti import errors
36
from ganeti import constants
37

    
38

    
39
__all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
40
           "OS", "Node", "Cluster", "FillDict"]
41

    
42

    
43
def FillDict(defaults_dict, custom_dict):
44
  """Basic function to apply settings on top a default dict.
45

46
  @type defaults_dict: dict
47
  @param defaults_dict: dictionary holding the default values
48
  @type custom_dict: dict
49
  @param custom_dict: dictionary holding customized value
50
  @rtype: dict
51
  @return: dict with the 'full' values
52

53
  """
54
  ret_dict = copy.deepcopy(defaults_dict)
55
  ret_dict.update(custom_dict)
56
  return ret_dict
57

    
58

    
59
def UpgradeGroupedParams(target, defaults):
60
  """Update all groups for the target parameter.
61

62
  @type target: dict of dicts
63
  @param target: {group: {parameter: value}}
64
  @type defaults: dict
65
  @param defaults: default parameter values
66

67
  """
68
  if target is None:
69
    target = {constants.PP_DEFAULT: defaults}
70
  else:
71
    for group in target:
72
      target[group] = FillDict(defaults, target[group])
73
  return target
74

    
75

    
76
class ConfigObject(object):
77
  """A generic config object.
78

79
  It has the following properties:
80

81
    - provides somewhat safe recursive unpickling and pickling for its classes
82
    - unset attributes which are defined in slots are always returned
83
      as None instead of raising an error
84

85
  Classes derived from this must always declare __slots__ (we use many
86
  config objects and the memory reduction is useful)
87

88
  """
89
  __slots__ = []
90

    
91
  def __init__(self, **kwargs):
92
    for k, v in kwargs.iteritems():
93
      setattr(self, k, v)
94
    self.UpgradeConfig()
95

    
96
  def __getattr__(self, name):
97
    if name not in self.__slots__:
98
      raise AttributeError("Invalid object attribute %s.%s" %
99
                           (type(self).__name__, name))
100
    return None
101

    
102
  def __setstate__(self, state):
103
    for name in state:
104
      if name in self.__slots__:
105
        setattr(self, name, state[name])
106

    
107
  def ToDict(self):
108
    """Convert to a dict holding only standard python types.
109

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

116
    """
117
    result = {}
118
    for name in self.__slots__:
119
      value = getattr(self, name, None)
120
      if value is not None:
121
        result[name] = value
122
    return result
123

    
124
  __getstate__ = ToDict
125

    
126
  @classmethod
127
  def FromDict(cls, val):
128
    """Create an object from a dictionary.
129

130
    This generic routine takes a dict, instantiates a new instance of
131
    the given class, and sets attributes based on the dict content.
132

133
    As for `ToDict`, this does not work if the class has children
134
    who are ConfigObjects themselves (e.g. the nics list in an
135
    Instance), in which case the object should subclass the function
136
    and alter the objects.
137

138
    """
139
    if not isinstance(val, dict):
140
      raise errors.ConfigurationError("Invalid object passed to FromDict:"
141
                                      " expected dict, got %s" % type(val))
142
    val_str = dict([(str(k), v) for k, v in val.iteritems()])
143
    obj = cls(**val_str)
144
    return obj
145

    
146
  @staticmethod
147
  def _ContainerToDicts(container):
148
    """Convert the elements of a container to standard python types.
149

150
    This method converts a container with elements derived from
151
    ConfigData to standard python types. If the container is a dict,
152
    we don't touch the keys, only the values.
153

154
    """
155
    if isinstance(container, dict):
156
      ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
157
    elif isinstance(container, (list, tuple, set, frozenset)):
158
      ret = [elem.ToDict() for elem in container]
159
    else:
160
      raise TypeError("Invalid type %s passed to _ContainerToDicts" %
161
                      type(container))
162
    return ret
163

    
164
  @staticmethod
165
  def _ContainerFromDicts(source, c_type, e_type):
166
    """Convert a container from standard python types.
167

168
    This method converts a container with standard python types to
169
    ConfigData objects. If the container is a dict, we don't touch the
170
    keys, only the values.
171

172
    """
173
    if not isinstance(c_type, type):
174
      raise TypeError("Container type %s passed to _ContainerFromDicts is"
175
                      " not a type" % type(c_type))
176
    if c_type is dict:
177
      ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
178
    elif c_type in (list, tuple, set, frozenset):
179
      ret = c_type([e_type.FromDict(elem) for elem in source])
180
    else:
181
      raise TypeError("Invalid container type %s passed to"
182
                      " _ContainerFromDicts" % c_type)
183
    return ret
184

    
185
  def Copy(self):
186
    """Makes a deep copy of the current object and its children.
187

188
    """
189
    dict_form = self.ToDict()
190
    clone_obj = self.__class__.FromDict(dict_form)
191
    return clone_obj
192

    
193
  def __repr__(self):
194
    """Implement __repr__ for ConfigObjects."""
195
    return repr(self.ToDict())
196

    
197
  def UpgradeConfig(self):
198
    """Fill defaults for missing configuration values.
199

200
    This method will be called at object init time, and its implementation will
201
    be object dependent.
202

203
    """
204
    pass
205

    
206

    
207
class TaggableObject(ConfigObject):
208
  """An generic class supporting tags.
209

210
  """
211
  __slots__ = ConfigObject.__slots__ + ["tags"]
212

    
213
  @staticmethod
214
  def ValidateTag(tag):
215
    """Check if a tag is valid.
216

217
    If the tag is invalid, an errors.TagError will be raised. The
218
    function has no return value.
219

220
    """
221
    if not isinstance(tag, basestring):
222
      raise errors.TagError("Invalid tag type (not a string)")
223
    if len(tag) > constants.MAX_TAG_LEN:
224
      raise errors.TagError("Tag too long (>%d characters)" %
225
                            constants.MAX_TAG_LEN)
226
    if not tag:
227
      raise errors.TagError("Tags cannot be empty")
228
    if not re.match("^[\w.+*/:-]+$", tag):
229
      raise errors.TagError("Tag contains invalid characters")
230

    
231
  def GetTags(self):
232
    """Return the tags list.
233

234
    """
235
    tags = getattr(self, "tags", None)
236
    if tags is None:
237
      tags = self.tags = set()
238
    return tags
239

    
240
  def AddTag(self, tag):
241
    """Add a new tag.
242

243
    """
244
    self.ValidateTag(tag)
245
    tags = self.GetTags()
246
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
247
      raise errors.TagError("Too many tags")
248
    self.GetTags().add(tag)
249

    
250
  def RemoveTag(self, tag):
251
    """Remove a tag.
252

253
    """
254
    self.ValidateTag(tag)
255
    tags = self.GetTags()
256
    try:
257
      tags.remove(tag)
258
    except KeyError:
259
      raise errors.TagError("Tag not found")
260

    
261
  def ToDict(self):
262
    """Taggable-object-specific conversion to standard python types.
263

264
    This replaces the tags set with a list.
265

266
    """
267
    bo = super(TaggableObject, self).ToDict()
268

    
269
    tags = bo.get("tags", None)
270
    if isinstance(tags, set):
271
      bo["tags"] = list(tags)
272
    return bo
273

    
274
  @classmethod
275
  def FromDict(cls, val):
276
    """Custom function for instances.
277

278
    """
279
    obj = super(TaggableObject, cls).FromDict(val)
280
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
281
      obj.tags = set(obj.tags)
282
    return obj
283

    
284

    
285
class ConfigData(ConfigObject):
286
  """Top-level config object."""
287
  __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
288

    
289
  def ToDict(self):
290
    """Custom function for top-level config data.
291

292
    This just replaces the list of instances, nodes and the cluster
293
    with standard python types.
294

295
    """
296
    mydict = super(ConfigData, self).ToDict()
297
    mydict["cluster"] = mydict["cluster"].ToDict()
298
    for key in "nodes", "instances":
299
      mydict[key] = self._ContainerToDicts(mydict[key])
300

    
301
    return mydict
302

    
303
  @classmethod
304
  def FromDict(cls, val):
305
    """Custom function for top-level config data
306

307
    """
308
    obj = super(ConfigData, cls).FromDict(val)
309
    obj.cluster = Cluster.FromDict(obj.cluster)
310
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
311
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
312
    return obj
313

    
314

    
315
class NIC(ConfigObject):
316
  """Config object representing a network card."""
317
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
318

    
319
  @classmethod
320
  def CheckParameterSyntax(cls, nicparams):
321
    """Check the given parameters for validity.
322

323
    @type nicparams:  dict
324
    @param nicparams: dictionary with parameter names/value
325
    @raise errors.ConfigurationError: when a parameter is not valid
326

327
    """
328
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
329
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
330
      raise errors.ConfigurationError(err)
331

    
332
    if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
333
        not nicparams[constants.NIC_LINK]):
334
      err = "Missing bridged nic link"
335
      raise errors.ConfigurationError(err)
336

    
337
  def UpgradeConfig(self):
338
    """Fill defaults for missing configuration values.
339

340
    """
341
    if self.nicparams is None:
342
      self.nicparams = {}
343
      if self.bridge is not None:
344
        self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
345
        self.nicparams[constants.NIC_LINK] = self.bridge
346
    # bridge is no longer used it 2.1. The slot is left there to support
347
    # upgrading, but will be removed in 2.2
348
    if self.bridge is not None:
349
      self.bridge = None
350

    
351

    
352
class Disk(ConfigObject):
353
  """Config object representing a block device."""
354
  __slots__ = ["dev_type", "logical_id", "physical_id",
355
               "children", "iv_name", "size", "mode"]
356

    
357
  def CreateOnSecondary(self):
358
    """Test if this device needs to be created on a secondary node."""
359
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
360

    
361
  def AssembleOnSecondary(self):
362
    """Test if this device needs to be assembled on a secondary node."""
363
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
364

    
365
  def OpenOnSecondary(self):
366
    """Test if this device needs to be opened on a secondary node."""
367
    return self.dev_type in (constants.LD_LV,)
368

    
369
  def StaticDevPath(self):
370
    """Return the device path if this device type has a static one.
371

372
    Some devices (LVM for example) live always at the same /dev/ path,
373
    irrespective of their status. For such devices, we return this
374
    path, for others we return None.
375

376
    """
377
    if self.dev_type == constants.LD_LV:
378
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
379
    return None
380

    
381
  def ChildrenNeeded(self):
382
    """Compute the needed number of children for activation.
383

384
    This method will return either -1 (all children) or a positive
385
    number denoting the minimum number of children needed for
386
    activation (only mirrored devices will usually return >=0).
387

388
    Currently, only DRBD8 supports diskless activation (therefore we
389
    return 0), for all other we keep the previous semantics and return
390
    -1.
391

392
    """
393
    if self.dev_type == constants.LD_DRBD8:
394
      return 0
395
    return -1
396

    
397
  def GetNodes(self, node):
398
    """This function returns the nodes this device lives on.
399

400
    Given the node on which the parent of the device lives on (or, in
401
    case of a top-level device, the primary node of the devices'
402
    instance), this function will return a list of nodes on which this
403
    devices needs to (or can) be assembled.
404

405
    """
406
    if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
407
      result = [node]
408
    elif self.dev_type in constants.LDS_DRBD:
409
      result = [self.logical_id[0], self.logical_id[1]]
410
      if node not in result:
411
        raise errors.ConfigurationError("DRBD device passed unknown node")
412
    else:
413
      raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
414
    return result
415

    
416
  def ComputeNodeTree(self, parent_node):
417
    """Compute the node/disk tree for this disk and its children.
418

419
    This method, given the node on which the parent disk lives, will
420
    return the list of all (node, disk) pairs which describe the disk
421
    tree in the most compact way. For example, a drbd/lvm stack
422
    will be returned as (primary_node, drbd) and (secondary_node, drbd)
423
    which represents all the top-level devices on the nodes.
424

425
    """
426
    my_nodes = self.GetNodes(parent_node)
427
    result = [(node, self) for node in my_nodes]
428
    if not self.children:
429
      # leaf device
430
      return result
431
    for node in my_nodes:
432
      for child in self.children:
433
        child_result = child.ComputeNodeTree(node)
434
        if len(child_result) == 1:
435
          # child (and all its descendants) is simple, doesn't split
436
          # over multiple hosts, so we don't need to describe it, our
437
          # own entry for this node describes it completely
438
          continue
439
        else:
440
          # check if child nodes differ from my nodes; note that
441
          # subdisk can differ from the child itself, and be instead
442
          # one of its descendants
443
          for subnode, subdisk in child_result:
444
            if subnode not in my_nodes:
445
              result.append((subnode, subdisk))
446
            # otherwise child is under our own node, so we ignore this
447
            # entry (but probably the other results in the list will
448
            # be different)
449
    return result
450

    
451
  def RecordGrow(self, amount):
452
    """Update the size of this disk after growth.
453

454
    This method recurses over the disks's children and updates their
455
    size correspondigly. The method needs to be kept in sync with the
456
    actual algorithms from bdev.
457

458
    """
459
    if self.dev_type == constants.LD_LV:
460
      self.size += amount
461
    elif self.dev_type == constants.LD_DRBD8:
462
      if self.children:
463
        self.children[0].RecordGrow(amount)
464
      self.size += amount
465
    else:
466
      raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
467
                                   " disk type %s" % self.dev_type)
468

    
469
  def UnsetSize(self):
470
    """Sets recursively the size to zero for the disk and its children.
471

472
    """
473
    if self.children:
474
      for child in self.children:
475
        child.UnsetSize()
476
    self.size = 0
477

    
478
  def SetPhysicalID(self, target_node, nodes_ip):
479
    """Convert the logical ID to the physical ID.
480

481
    This is used only for drbd, which needs ip/port configuration.
482

483
    The routine descends down and updates its children also, because
484
    this helps when the only the top device is passed to the remote
485
    node.
486

487
    Arguments:
488
      - target_node: the node we wish to configure for
489
      - nodes_ip: a mapping of node name to ip
490

491
    The target_node must exist in in nodes_ip, and must be one of the
492
    nodes in the logical ID for each of the DRBD devices encountered
493
    in the disk tree.
494

495
    """
496
    if self.children:
497
      for child in self.children:
498
        child.SetPhysicalID(target_node, nodes_ip)
499

    
500
    if self.logical_id is None and self.physical_id is not None:
501
      return
502
    if self.dev_type in constants.LDS_DRBD:
503
      pnode, snode, port, pminor, sminor, secret = self.logical_id
504
      if target_node not in (pnode, snode):
505
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
506
                                        target_node)
507
      pnode_ip = nodes_ip.get(pnode, None)
508
      snode_ip = nodes_ip.get(snode, None)
509
      if pnode_ip is None or snode_ip is None:
510
        raise errors.ConfigurationError("Can't find primary or secondary node"
511
                                        " for %s" % str(self))
512
      p_data = (pnode_ip, port)
513
      s_data = (snode_ip, port)
514
      if pnode == target_node:
515
        self.physical_id = p_data + s_data + (pminor, secret)
516
      else: # it must be secondary, we tested above
517
        self.physical_id = s_data + p_data + (sminor, secret)
518
    else:
519
      self.physical_id = self.logical_id
520
    return
521

    
522
  def ToDict(self):
523
    """Disk-specific conversion to standard python types.
524

525
    This replaces the children lists of objects with lists of
526
    standard python types.
527

528
    """
529
    bo = super(Disk, self).ToDict()
530

    
531
    for attr in ("children",):
532
      alist = bo.get(attr, None)
533
      if alist:
534
        bo[attr] = self._ContainerToDicts(alist)
535
    return bo
536

    
537
  @classmethod
538
  def FromDict(cls, val):
539
    """Custom function for Disks
540

541
    """
542
    obj = super(Disk, cls).FromDict(val)
543
    if obj.children:
544
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
545
    if obj.logical_id and isinstance(obj.logical_id, list):
546
      obj.logical_id = tuple(obj.logical_id)
547
    if obj.physical_id and isinstance(obj.physical_id, list):
548
      obj.physical_id = tuple(obj.physical_id)
549
    if obj.dev_type in constants.LDS_DRBD:
550
      # we need a tuple of length six here
551
      if len(obj.logical_id) < 6:
552
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
553
    return obj
554

    
555
  def __str__(self):
556
    """Custom str() formatter for disks.
557

558
    """
559
    if self.dev_type == constants.LD_LV:
560
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
561
    elif self.dev_type in constants.LDS_DRBD:
562
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
563
      val = "<DRBD8("
564
      if self.physical_id is None:
565
        phy = "unconfigured"
566
      else:
567
        phy = ("configured as %s:%s %s:%s" %
568
               (self.physical_id[0], self.physical_id[1],
569
                self.physical_id[2], self.physical_id[3]))
570

    
571
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
572
              (node_a, minor_a, node_b, minor_b, port, phy))
573
      if self.children and self.children.count(None) == 0:
574
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
575
      else:
576
        val += "no local storage"
577
    else:
578
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
579
             (self.dev_type, self.logical_id, self.physical_id, self.children))
580
    if self.iv_name is None:
581
      val += ", not visible"
582
    else:
583
      val += ", visible as /dev/%s" % self.iv_name
584
    if isinstance(self.size, int):
585
      val += ", size=%dm)>" % self.size
586
    else:
587
      val += ", size='%s')>" % (self.size,)
588
    return val
589

    
590
  def Verify(self):
591
    """Checks that this disk is correctly configured.
592

593
    """
594
    all_errors = []
595
    if self.mode not in constants.DISK_ACCESS_SET:
596
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
597
    return all_errors
598

    
599

    
600
class Instance(TaggableObject):
601
  """Config object representing an instance."""
602
  __slots__ = TaggableObject.__slots__ + [
603
    "name",
604
    "primary_node",
605
    "os",
606
    "hypervisor",
607
    "hvparams",
608
    "beparams",
609
    "admin_up",
610
    "nics",
611
    "disks",
612
    "disk_template",
613
    "network_port",
614
    "serial_no",
615
    ]
616

    
617
  def _ComputeSecondaryNodes(self):
618
    """Compute the list of secondary nodes.
619

620
    This is a simple wrapper over _ComputeAllNodes.
621

622
    """
623
    all_nodes = set(self._ComputeAllNodes())
624
    all_nodes.discard(self.primary_node)
625
    return tuple(all_nodes)
626

    
627
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
628
                             "List of secondary nodes")
629

    
630
  def _ComputeAllNodes(self):
631
    """Compute the list of all nodes.
632

633
    Since the data is already there (in the drbd disks), keeping it as
634
    a separate normal attribute is redundant and if not properly
635
    synchronised can cause problems. Thus it's better to compute it
636
    dynamically.
637

638
    """
639
    def _Helper(nodes, device):
640
      """Recursively computes nodes given a top device."""
641
      if device.dev_type in constants.LDS_DRBD:
642
        nodea, nodeb = device.logical_id[:2]
643
        nodes.add(nodea)
644
        nodes.add(nodeb)
645
      if device.children:
646
        for child in device.children:
647
          _Helper(nodes, child)
648

    
649
    all_nodes = set()
650
    all_nodes.add(self.primary_node)
651
    for device in self.disks:
652
      _Helper(all_nodes, device)
653
    return tuple(all_nodes)
654

    
655
  all_nodes = property(_ComputeAllNodes, None, None,
656
                       "List of all nodes of the instance")
657

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

661
    This function figures out what logical volumes should belong on
662
    which nodes, recursing through a device tree.
663

664
    @param lvmap: optional dictionary to receive the
665
        'node' : ['lv', ...] data.
666

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

670
    """
671
    if node == None:
672
      node = self.primary_node
673

    
674
    if lvmap is None:
675
      lvmap = { node : [] }
676
      ret = lvmap
677
    else:
678
      if not node in lvmap:
679
        lvmap[node] = []
680
      ret = None
681

    
682
    if not devs:
683
      devs = self.disks
684

    
685
    for dev in devs:
686
      if dev.dev_type == constants.LD_LV:
687
        lvmap[node].append(dev.logical_id[1])
688

    
689
      elif dev.dev_type in constants.LDS_DRBD:
690
        if dev.children:
691
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
692
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
693

    
694
      elif dev.children:
695
        self.MapLVsByNode(lvmap, dev.children, node)
696

    
697
    return ret
698

    
699
  def FindDisk(self, idx):
700
    """Find a disk given having a specified index.
701

702
    This is just a wrapper that does validation of the index.
703

704
    @type idx: int
705
    @param idx: the disk index
706
    @rtype: L{Disk}
707
    @return: the corresponding disk
708
    @raise errors.OpPrereqError: when the given index is not valid
709

710
    """
711
    try:
712
      idx = int(idx)
713
      return self.disks[idx]
714
    except ValueError, err:
715
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
716
    except IndexError:
717
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
718
                                 " 0 to %d" % (idx, len(self.disks)))
719

    
720
  def ToDict(self):
721
    """Instance-specific conversion to standard python types.
722

723
    This replaces the children lists of objects with lists of standard
724
    python types.
725

726
    """
727
    bo = super(Instance, self).ToDict()
728

    
729
    for attr in "nics", "disks":
730
      alist = bo.get(attr, None)
731
      if alist:
732
        nlist = self._ContainerToDicts(alist)
733
      else:
734
        nlist = []
735
      bo[attr] = nlist
736
    return bo
737

    
738
  @classmethod
739
  def FromDict(cls, val):
740
    """Custom function for instances.
741

742
    """
743
    obj = super(Instance, cls).FromDict(val)
744
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
745
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
746
    return obj
747

    
748

    
749
class OS(ConfigObject):
750
  """Config object representing an operating system."""
751
  __slots__ = [
752
    "name",
753
    "path",
754
    "api_versions",
755
    "create_script",
756
    "export_script",
757
    "import_script",
758
    "rename_script",
759
    ]
760

    
761

    
762
class Node(TaggableObject):
763
  """Config object representing a node."""
764
  __slots__ = TaggableObject.__slots__ + [
765
    "name",
766
    "primary_ip",
767
    "secondary_ip",
768
    "serial_no",
769
    "master_candidate",
770
    "offline",
771
    "drained",
772
    ]
773

    
774

    
775
class Cluster(TaggableObject):
776
  """Config object representing the cluster."""
777
  __slots__ = TaggableObject.__slots__ + [
778
    "serial_no",
779
    "rsahostkeypub",
780
    "highest_used_port",
781
    "tcpudp_port_pool",
782
    "mac_prefix",
783
    "volume_group_name",
784
    "default_bridge",
785
    "default_hypervisor",
786
    "master_node",
787
    "master_ip",
788
    "master_netdev",
789
    "cluster_name",
790
    "file_storage_dir",
791
    "enabled_hypervisors",
792
    "hvparams",
793
    "beparams",
794
    "nicparams",
795
    "candidate_pool_size",
796
    "modify_etc_hosts",
797
    ]
798

    
799
  def UpgradeConfig(self):
800
    """Fill defaults for missing configuration values.
801

802
    """
803
    if self.hvparams is None:
804
      self.hvparams = constants.HVC_DEFAULTS
805
    else:
806
      for hypervisor in self.hvparams:
807
        self.hvparams[hypervisor] = FillDict(
808
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
809

    
810
    self.beparams = UpgradeGroupedParams(self.beparams,
811
                                         constants.BEC_DEFAULTS)
812
    migrate_default_bridge = not self.nicparams
813
    self.nicparams = UpgradeGroupedParams(self.nicparams,
814
                                          constants.NICC_DEFAULTS)
815
    if migrate_default_bridge:
816
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
817
        self.default_bridge
818

    
819
    if self.modify_etc_hosts is None:
820
      self.modify_etc_hosts = True
821

    
822
    # default_bridge is no longer used it 2.1. The slot is left there to
823
    # support auto-upgrading, but will be removed in 2.2
824
    if self.default_bridge is not None:
825
      self.default_bridge = None
826

    
827
    # default_hypervisor is just the first enabled one in 2.1
828
    if self.default_hypervisor is not None:
829
      self.enabled_hypervisors = [self.default_hypervisor] + \
830
        [hvname for hvname in self.enabled_hypervisors
831
         if hvname != self.default_hypervisor]
832
      self.default_hypervisor = None
833

    
834

    
835
  def ToDict(self):
836
    """Custom function for cluster.
837

838
    """
839
    mydict = super(Cluster, self).ToDict()
840
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
841
    return mydict
842

    
843
  @classmethod
844
  def FromDict(cls, val):
845
    """Custom function for cluster.
846

847
    """
848
    obj = super(Cluster, cls).FromDict(val)
849
    if not isinstance(obj.tcpudp_port_pool, set):
850
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
851
    return obj
852

    
853
  def FillHV(self, instance):
854
    """Fill an instance's hvparams dict.
855

856
    @type instance: L{objects.Instance}
857
    @param instance: the instance parameter to fill
858
    @rtype: dict
859
    @return: a copy of the instance's hvparams with missing keys filled from
860
        the cluster defaults
861

862
    """
863
    return FillDict(self.hvparams.get(instance.hypervisor, {}),
864
                         instance.hvparams)
865

    
866
  def FillBE(self, instance):
867
    """Fill an instance's beparams dict.
868

869
    @type instance: L{objects.Instance}
870
    @param instance: the instance parameter to fill
871
    @rtype: dict
872
    @return: a copy of the instance's beparams with missing keys filled from
873
        the cluster defaults
874

875
    """
876
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
877
                          instance.beparams)
878

    
879

    
880
class BlockDevStatus(ConfigObject):
881
  """Config object representing the status of a block device."""
882
  __slots__ = [
883
    "dev_path",
884
    "major",
885
    "minor",
886
    "sync_percent",
887
    "estimated_time",
888
    "is_degraded",
889
    "ldisk_status",
890
    ]
891

    
892

    
893
class ConfdRequest(ConfigObject):
894
  """Object holding a confd request.
895

896
  @ivar protocol: confd protocol version
897
  @ivar type: confd query type
898
  @ivar query: query request
899
  @ivar rsalt: requested reply salt
900

901
  """
902
  __slots__ = [
903
    "protocol",
904
    "type",
905
    "query",
906
    "rsalt",
907
    ]
908

    
909

    
910
class ConfdReply(ConfigObject):
911
  """Object holding a confd reply.
912

913
  @ivar protocol: confd protocol version
914
  @ivar status: reply status code (ok, error)
915
  @ivar answer: confd query reply
916
  @ivar serial: configuration serial number
917

918
  """
919
  __slots__ = [
920
    "protocol",
921
    "status",
922
    "answer",
923
    "serial",
924
    ]
925

    
926

    
927
class SerializableConfigParser(ConfigParser.SafeConfigParser):
928
  """Simple wrapper over ConfigParse that allows serialization.
929

930
  This class is basically ConfigParser.SafeConfigParser with two
931
  additional methods that allow it to serialize/unserialize to/from a
932
  buffer.
933

934
  """
935
  def Dumps(self):
936
    """Dump this instance and return the string representation."""
937
    buf = StringIO()
938
    self.write(buf)
939
    return buf.getvalue()
940

    
941
  @staticmethod
942
  def Loads(data):
943
    """Load data from a string."""
944
    buf = StringIO(data)
945
    cfp = SerializableConfigParser()
946
    cfp.readfp(buf)
947
    return cfp