Statistics
| Branch: | Tag: | Revision:

root / lib / objects.py @ 066f465d

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

    
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
def FillDict(defaults_dict, custom_dict):
43
  """Basic function to apply settings on top a default dict.
44

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

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

    
57

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

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

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

    
74

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

78
  It has the following properties:
79

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

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

87
  """
88
  __slots__ = []
89

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

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

    
101
  def __setitem__(self, key, value):
102
    if key not in self.__slots__:
103
      raise KeyError(key)
104
    setattr(self, key, value)
105

    
106
  def __getstate__(self):
107
    state = {}
108
    for name in self.__slots__:
109
      if hasattr(self, name):
110
        state[name] = getattr(self, name)
111
    return state
112

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

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

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

127
    """
128
    return dict([(k, getattr(self, k, None)) for k in self.__slots__])
129

    
130
  @classmethod
131
  def FromDict(cls, val):
132
    """Create an object from a dictionary.
133

134
    This generic routine takes a dict, instantiates a new instance of
135
    the given class, and sets attributes based on the dict content.
136

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

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

    
150
  @staticmethod
151
  def _ContainerToDicts(container):
152
    """Convert the elements of a container to standard python types.
153

154
    This method converts a container with elements derived from
155
    ConfigData to standard python types. If the container is a dict,
156
    we don't touch the keys, only the values.
157

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

    
168
  @staticmethod
169
  def _ContainerFromDicts(source, c_type, e_type):
170
    """Convert a container from standard python types.
171

172
    This method converts a container with standard python types to
173
    ConfigData objects. If the container is a dict, we don't touch the
174
    keys, only the values.
175

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

    
189
  def __repr__(self):
190
    """Implement __repr__ for ConfigObjects."""
191
    return repr(self.ToDict())
192

    
193
  def UpgradeConfig(self):
194
    """Fill defaults for missing configuration values.
195

196
    This method will be called at object init time, and its implementation will
197
    be object dependent.
198

199
    """
200
    pass
201

    
202

    
203
class TaggableObject(ConfigObject):
204
  """An generic class supporting tags.
205

206
  """
207
  __slots__ = ConfigObject.__slots__ + ["tags"]
208

    
209
  @staticmethod
210
  def ValidateTag(tag):
211
    """Check if a tag is valid.
212

213
    If the tag is invalid, an errors.TagError will be raised. The
214
    function has no return value.
215

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

    
227
  def GetTags(self):
228
    """Return the tags list.
229

230
    """
231
    tags = getattr(self, "tags", None)
232
    if tags is None:
233
      tags = self.tags = set()
234
    return tags
235

    
236
  def AddTag(self, tag):
237
    """Add a new tag.
238

239
    """
240
    self.ValidateTag(tag)
241
    tags = self.GetTags()
242
    if len(tags) >= constants.MAX_TAGS_PER_OBJ:
243
      raise errors.TagError("Too many tags")
244
    self.GetTags().add(tag)
245

    
246
  def RemoveTag(self, tag):
247
    """Remove a tag.
248

249
    """
250
    self.ValidateTag(tag)
251
    tags = self.GetTags()
252
    try:
253
      tags.remove(tag)
254
    except KeyError:
255
      raise errors.TagError("Tag not found")
256

    
257
  def ToDict(self):
258
    """Taggable-object-specific conversion to standard python types.
259

260
    This replaces the tags set with a list.
261

262
    """
263
    bo = super(TaggableObject, self).ToDict()
264

    
265
    tags = bo.get("tags", None)
266
    if isinstance(tags, set):
267
      bo["tags"] = list(tags)
268
    return bo
269

    
270
  @classmethod
271
  def FromDict(cls, val):
272
    """Custom function for instances.
273

274
    """
275
    obj = super(TaggableObject, cls).FromDict(val)
276
    if hasattr(obj, "tags") and isinstance(obj.tags, list):
277
      obj.tags = set(obj.tags)
278
    return obj
279

    
280

    
281
class ConfigData(ConfigObject):
282
  """Top-level config object."""
283
  __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
284

    
285
  def ToDict(self):
286
    """Custom function for top-level config data.
287

288
    This just replaces the list of instances, nodes and the cluster
289
    with standard python types.
290

291
    """
292
    mydict = super(ConfigData, self).ToDict()
293
    mydict["cluster"] = mydict["cluster"].ToDict()
294
    for key in "nodes", "instances":
295
      mydict[key] = self._ContainerToDicts(mydict[key])
296

    
297
    return mydict
298

    
299
  @classmethod
300
  def FromDict(cls, val):
301
    """Custom function for top-level config data
302

303
    """
304
    obj = super(ConfigData, cls).FromDict(val)
305
    obj.cluster = Cluster.FromDict(obj.cluster)
306
    obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
307
    obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
308
    return obj
309

    
310

    
311
class NIC(ConfigObject):
312
  """Config object representing a network card."""
313
  __slots__ = ["mac", "ip", "bridge", "nicparams"]
314

    
315
  @classmethod
316
  def CheckParameterSyntax(cls, nicparams):
317
    """Check the given parameters for validity.
318

319
    @type nicparams:  dict
320
    @param nicparams: dictionary with parameter names/value
321
    @raise errors.ConfigurationError: when a parameter is not valid
322

323
    """
324
    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
325
      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
326
      raise errors.ConfigurationError(err)
327

    
328
    if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
329
        not nicparams[constants.NIC_LINK]):
330
      err = "Missing bridged nic link"
331
      raise errors.ConfigurationError(err)
332

    
333
  def UpgradeConfig(self):
334
    """Fill defaults for missing configuration values.
335

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

    
347

    
348
class Disk(ConfigObject):
349
  """Config object representing a block device."""
350
  __slots__ = ["dev_type", "logical_id", "physical_id",
351
               "children", "iv_name", "size", "mode"]
352

    
353
  def CreateOnSecondary(self):
354
    """Test if this device needs to be created on a secondary node."""
355
    return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
356

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

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

    
365
  def StaticDevPath(self):
366
    """Return the device path if this device type has a static one.
367

368
    Some devices (LVM for example) live always at the same /dev/ path,
369
    irrespective of their status. For such devices, we return this
370
    path, for others we return None.
371

372
    """
373
    if self.dev_type == constants.LD_LV:
374
      return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
375
    return None
376

    
377
  def ChildrenNeeded(self):
378
    """Compute the needed number of children for activation.
379

380
    This method will return either -1 (all children) or a positive
381
    number denoting the minimum number of children needed for
382
    activation (only mirrored devices will usually return >=0).
383

384
    Currently, only DRBD8 supports diskless activation (therefore we
385
    return 0), for all other we keep the previous semantics and return
386
    -1.
387

388
    """
389
    if self.dev_type == constants.LD_DRBD8:
390
      return 0
391
    return -1
392

    
393
  def GetNodes(self, node):
394
    """This function returns the nodes this device lives on.
395

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

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

    
412
  def ComputeNodeTree(self, parent_node):
413
    """Compute the node/disk tree for this disk and its children.
414

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

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

    
447
  def RecordGrow(self, amount):
448
    """Update the size of this disk after growth.
449

450
    This method recurses over the disks's children and updates their
451
    size correspondigly. The method needs to be kept in sync with the
452
    actual algorithms from bdev.
453

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

    
465
  def SetPhysicalID(self, target_node, nodes_ip):
466
    """Convert the logical ID to the physical ID.
467

468
    This is used only for drbd, which needs ip/port configuration.
469

470
    The routine descends down and updates its children also, because
471
    this helps when the only the top device is passed to the remote
472
    node.
473

474
    Arguments:
475
      - target_node: the node we wish to configure for
476
      - nodes_ip: a mapping of node name to ip
477

478
    The target_node must exist in in nodes_ip, and must be one of the
479
    nodes in the logical ID for each of the DRBD devices encountered
480
    in the disk tree.
481

482
    """
483
    if self.children:
484
      for child in self.children:
485
        child.SetPhysicalID(target_node, nodes_ip)
486

    
487
    if self.logical_id is None and self.physical_id is not None:
488
      return
489
    if self.dev_type in constants.LDS_DRBD:
490
      pnode, snode, port, pminor, sminor, secret = self.logical_id
491
      if target_node not in (pnode, snode):
492
        raise errors.ConfigurationError("DRBD device not knowing node %s" %
493
                                        target_node)
494
      pnode_ip = nodes_ip.get(pnode, None)
495
      snode_ip = nodes_ip.get(snode, None)
496
      if pnode_ip is None or snode_ip is None:
497
        raise errors.ConfigurationError("Can't find primary or secondary node"
498
                                        " for %s" % str(self))
499
      p_data = (pnode_ip, port)
500
      s_data = (snode_ip, port)
501
      if pnode == target_node:
502
        self.physical_id = p_data + s_data + (pminor, secret)
503
      else: # it must be secondary, we tested above
504
        self.physical_id = s_data + p_data + (sminor, secret)
505
    else:
506
      self.physical_id = self.logical_id
507
    return
508

    
509
  def ToDict(self):
510
    """Disk-specific conversion to standard python types.
511

512
    This replaces the children lists of objects with lists of
513
    standard python types.
514

515
    """
516
    bo = super(Disk, self).ToDict()
517

    
518
    for attr in ("children",):
519
      alist = bo.get(attr, None)
520
      if alist:
521
        bo[attr] = self._ContainerToDicts(alist)
522
    return bo
523

    
524
  @classmethod
525
  def FromDict(cls, val):
526
    """Custom function for Disks
527

528
    """
529
    obj = super(Disk, cls).FromDict(val)
530
    if obj.children:
531
      obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
532
    if obj.logical_id and isinstance(obj.logical_id, list):
533
      obj.logical_id = tuple(obj.logical_id)
534
    if obj.physical_id and isinstance(obj.physical_id, list):
535
      obj.physical_id = tuple(obj.physical_id)
536
    if obj.dev_type in constants.LDS_DRBD:
537
      # we need a tuple of length six here
538
      if len(obj.logical_id) < 6:
539
        obj.logical_id += (None,) * (6 - len(obj.logical_id))
540
    return obj
541

    
542
  def __str__(self):
543
    """Custom str() formatter for disks.
544

545
    """
546
    if self.dev_type == constants.LD_LV:
547
      val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
548
    elif self.dev_type in constants.LDS_DRBD:
549
      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
550
      val = "<DRBD8("
551
      if self.physical_id is None:
552
        phy = "unconfigured"
553
      else:
554
        phy = ("configured as %s:%s %s:%s" %
555
               (self.physical_id[0], self.physical_id[1],
556
                self.physical_id[2], self.physical_id[3]))
557

    
558
      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
559
              (node_a, minor_a, node_b, minor_b, port, phy))
560
      if self.children and self.children.count(None) == 0:
561
        val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
562
      else:
563
        val += "no local storage"
564
    else:
565
      val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
566
             (self.dev_type, self.logical_id, self.physical_id, self.children))
567
    if self.iv_name is None:
568
      val += ", not visible"
569
    else:
570
      val += ", visible as /dev/%s" % self.iv_name
571
    if isinstance(self.size, int):
572
      val += ", size=%dm)>" % self.size
573
    else:
574
      val += ", size='%s')>" % (self.size,)
575
    return val
576

    
577
  def Verify(self):
578
    """Checks that this disk is correctly configured.
579

580
    """
581
    all_errors = []
582
    if self.mode not in constants.DISK_ACCESS_SET:
583
      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
584
    return all_errors
585

    
586

    
587
class Instance(TaggableObject):
588
  """Config object representing an instance."""
589
  __slots__ = TaggableObject.__slots__ + [
590
    "name",
591
    "primary_node",
592
    "os",
593
    "hypervisor",
594
    "hvparams",
595
    "beparams",
596
    "admin_up",
597
    "nics",
598
    "disks",
599
    "disk_template",
600
    "network_port",
601
    "serial_no",
602
    ]
603

    
604
  def _ComputeSecondaryNodes(self):
605
    """Compute the list of secondary nodes.
606

607
    This is a simple wrapper over _ComputeAllNodes.
608

609
    """
610
    all_nodes = set(self._ComputeAllNodes())
611
    all_nodes.discard(self.primary_node)
612
    return tuple(all_nodes)
613

    
614
  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
615
                             "List of secondary nodes")
616

    
617
  def _ComputeAllNodes(self):
618
    """Compute the list of all nodes.
619

620
    Since the data is already there (in the drbd disks), keeping it as
621
    a separate normal attribute is redundant and if not properly
622
    synchronised can cause problems. Thus it's better to compute it
623
    dynamically.
624

625
    """
626
    def _Helper(nodes, device):
627
      """Recursively computes nodes given a top device."""
628
      if device.dev_type in constants.LDS_DRBD:
629
        nodea, nodeb = device.logical_id[:2]
630
        nodes.add(nodea)
631
        nodes.add(nodeb)
632
      if device.children:
633
        for child in device.children:
634
          _Helper(nodes, child)
635

    
636
    all_nodes = set()
637
    all_nodes.add(self.primary_node)
638
    for device in self.disks:
639
      _Helper(all_nodes, device)
640
    return tuple(all_nodes)
641

    
642
  all_nodes = property(_ComputeAllNodes, None, None,
643
                       "List of all nodes of the instance")
644

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

648
    This function figures out what logical volumes should belong on
649
    which nodes, recursing through a device tree.
650

651
    @param lvmap: optional dictionary to receive the
652
        'node' : ['lv', ...] data.
653

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

657
    """
658
    if node == None:
659
      node = self.primary_node
660

    
661
    if lvmap is None:
662
      lvmap = { node : [] }
663
      ret = lvmap
664
    else:
665
      if not node in lvmap:
666
        lvmap[node] = []
667
      ret = None
668

    
669
    if not devs:
670
      devs = self.disks
671

    
672
    for dev in devs:
673
      if dev.dev_type == constants.LD_LV:
674
        lvmap[node].append(dev.logical_id[1])
675

    
676
      elif dev.dev_type in constants.LDS_DRBD:
677
        if dev.children:
678
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
679
          self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
680

    
681
      elif dev.children:
682
        self.MapLVsByNode(lvmap, dev.children, node)
683

    
684
    return ret
685

    
686
  def FindDisk(self, idx):
687
    """Find a disk given having a specified index.
688

689
    This is just a wrapper that does validation of the index.
690

691
    @type idx: int
692
    @param idx: the disk index
693
    @rtype: L{Disk}
694
    @return: the corresponding disk
695
    @raise errors.OpPrereqError: when the given index is not valid
696

697
    """
698
    try:
699
      idx = int(idx)
700
      return self.disks[idx]
701
    except ValueError, err:
702
      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
703
    except IndexError:
704
      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
705
                                 " 0 to %d" % (idx, len(self.disks)))
706

    
707
  def ToDict(self):
708
    """Instance-specific conversion to standard python types.
709

710
    This replaces the children lists of objects with lists of standard
711
    python types.
712

713
    """
714
    bo = super(Instance, self).ToDict()
715

    
716
    for attr in "nics", "disks":
717
      alist = bo.get(attr, None)
718
      if alist:
719
        nlist = self._ContainerToDicts(alist)
720
      else:
721
        nlist = []
722
      bo[attr] = nlist
723
    return bo
724

    
725
  @classmethod
726
  def FromDict(cls, val):
727
    """Custom function for instances.
728

729
    """
730
    obj = super(Instance, cls).FromDict(val)
731
    obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
732
    obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
733
    return obj
734

    
735

    
736
class OS(ConfigObject):
737
  """Config object representing an operating system."""
738
  __slots__ = [
739
    "name",
740
    "path",
741
    "api_versions",
742
    "create_script",
743
    "export_script",
744
    "import_script",
745
    "rename_script",
746
    ]
747

    
748

    
749
class Node(TaggableObject):
750
  """Config object representing a node."""
751
  __slots__ = TaggableObject.__slots__ + [
752
    "name",
753
    "primary_ip",
754
    "secondary_ip",
755
    "serial_no",
756
    "master_candidate",
757
    "offline",
758
    "drained",
759
    ]
760

    
761

    
762
class Cluster(TaggableObject):
763
  """Config object representing the cluster."""
764
  __slots__ = TaggableObject.__slots__ + [
765
    "serial_no",
766
    "rsahostkeypub",
767
    "highest_used_port",
768
    "tcpudp_port_pool",
769
    "mac_prefix",
770
    "volume_group_name",
771
    "default_bridge",
772
    "default_hypervisor",
773
    "master_node",
774
    "master_ip",
775
    "master_netdev",
776
    "cluster_name",
777
    "file_storage_dir",
778
    "enabled_hypervisors",
779
    "hvparams",
780
    "beparams",
781
    "nicparams",
782
    "candidate_pool_size",
783
    "modify_etc_hosts",
784
    ]
785

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

789
    """
790
    if self.hvparams is None:
791
      self.hvparams = constants.HVC_DEFAULTS
792
    else:
793
      for hypervisor in self.hvparams:
794
        self.hvparams[hypervisor] = FillDict(
795
            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
796

    
797
    self.beparams = UpgradeGroupedParams(self.beparams,
798
                                         constants.BEC_DEFAULTS)
799
    migrate_default_bridge = not self.nicparams
800
    self.nicparams = UpgradeGroupedParams(self.nicparams,
801
                                          constants.NICC_DEFAULTS)
802
    if migrate_default_bridge:
803
      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
804
        self.default_bridge
805

    
806
    if self.modify_etc_hosts is None:
807
      self.modify_etc_hosts = True
808

    
809
    # default_bridge is no longer used it 2.1. The slot is left there to
810
    # support auto-upgrading, but will be removed in 2.2
811
    if self.default_bridge is not None:
812
      self.default_bridge = None
813

    
814
    # default_hypervisor is just the first enabled one in 2.1
815
    if self.default_hypervisor is not None:
816
      self.enabled_hypervisors = [self.default_hypervisor] + \
817
        [hvname for hvname in self.enabled_hypervisors
818
         if hvname != self.default_hypervisor]
819
      self.default_hypervisor = None
820

    
821

    
822
  def ToDict(self):
823
    """Custom function for cluster.
824

825
    """
826
    mydict = super(Cluster, self).ToDict()
827
    mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
828
    return mydict
829

    
830
  @classmethod
831
  def FromDict(cls, val):
832
    """Custom function for cluster.
833

834
    """
835
    obj = super(Cluster, cls).FromDict(val)
836
    if not isinstance(obj.tcpudp_port_pool, set):
837
      obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
838
    return obj
839

    
840
  def FillHV(self, instance):
841
    """Fill an instance's hvparams dict.
842

843
    @type instance: L{objects.Instance}
844
    @param instance: the instance parameter to fill
845
    @rtype: dict
846
    @return: a copy of the instance's hvparams with missing keys filled from
847
        the cluster defaults
848

849
    """
850
    return FillDict(self.hvparams.get(instance.hypervisor, {}),
851
                         instance.hvparams)
852

    
853
  def FillBE(self, instance):
854
    """Fill an instance's beparams 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 beparams with missing keys filled from
860
        the cluster defaults
861

862
    """
863
    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
864
                          instance.beparams)
865

    
866

    
867
class SerializableConfigParser(ConfigParser.SafeConfigParser):
868
  """Simple wrapper over ConfigParse that allows serialization.
869

870
  This class is basically ConfigParser.SafeConfigParser with two
871
  additional methods that allow it to serialize/unserialize to/from a
872
  buffer.
873

874
  """
875
  def Dumps(self):
876
    """Dump this instance and return the string representation."""
877
    buf = StringIO()
878
    self.write(buf)
879
    return buf.getvalue()
880

    
881
  @staticmethod
882
  def Loads(data):
883
    """Load data from a string."""
884
    buf = StringIO(data)
885
    cfp = SerializableConfigParser()
886
    cfp.readfp(buf)
887
    return cfp