cde0779cfa90b17e0ce02c4ce4447132778a9253
[ganeti-local] / lib / objects.py
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     errors = []
582     if self.mode not in constants.DISK_ACCESS_SET:
583       errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
584     return 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     "status",
742     "api_versions",
743     "create_script",
744     "export_script",
745     "import_script",
746     "rename_script",
747     ]
748
749   @classmethod
750   def FromInvalidOS(cls, err):
751     """Create an OS from an InvalidOS error.
752
753     This routine knows how to convert an InvalidOS error to an OS
754     object representing the broken OS with a meaningful error message.
755
756     """
757     if not isinstance(err, errors.InvalidOS):
758       raise errors.ProgrammerError("Trying to initialize an OS from an"
759                                    " invalid object of type %s" % type(err))
760
761     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
762
763   def __nonzero__(self):
764     return self.status == constants.OS_VALID_STATUS
765
766   __bool__ = __nonzero__
767
768
769 class Node(TaggableObject):
770   """Config object representing a node."""
771   __slots__ = TaggableObject.__slots__ + [
772     "name",
773     "primary_ip",
774     "secondary_ip",
775     "serial_no",
776     "master_candidate",
777     "offline",
778     "drained",
779     ]
780
781
782 class Cluster(TaggableObject):
783   """Config object representing the cluster."""
784   __slots__ = TaggableObject.__slots__ + [
785     "serial_no",
786     "rsahostkeypub",
787     "highest_used_port",
788     "tcpudp_port_pool",
789     "mac_prefix",
790     "volume_group_name",
791     "default_bridge",
792     "default_hypervisor",
793     "master_node",
794     "master_ip",
795     "master_netdev",
796     "cluster_name",
797     "file_storage_dir",
798     "enabled_hypervisors",
799     "hvparams",
800     "beparams",
801     "nicparams",
802     "candidate_pool_size",
803     "modify_etc_hosts",
804     ]
805
806   def UpgradeConfig(self):
807     """Fill defaults for missing configuration values.
808
809     """
810     if self.hvparams is None:
811       self.hvparams = constants.HVC_DEFAULTS
812     else:
813       for hypervisor in self.hvparams:
814         self.hvparams[hypervisor] = FillDict(
815             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
816
817     self.beparams = UpgradeGroupedParams(self.beparams,
818                                          constants.BEC_DEFAULTS)
819     migrate_default_bridge = not self.nicparams
820     self.nicparams = UpgradeGroupedParams(self.nicparams,
821                                           constants.NICC_DEFAULTS)
822     if migrate_default_bridge:
823       self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
824         self.default_bridge
825
826     if self.modify_etc_hosts is None:
827       self.modify_etc_hosts = True
828
829     # default_bridge is no longer used it 2.1. The slot is left there to
830     # support auto-upgrading, but will be removed in 2.2
831     if self.default_bridge is not None:
832       self.default_bridge = None
833
834   def ToDict(self):
835     """Custom function for cluster.
836
837     """
838     mydict = super(Cluster, self).ToDict()
839     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
840     return mydict
841
842   @classmethod
843   def FromDict(cls, val):
844     """Custom function for cluster.
845
846     """
847     obj = super(Cluster, cls).FromDict(val)
848     if not isinstance(obj.tcpudp_port_pool, set):
849       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
850     return obj
851
852   def FillHV(self, instance):
853     """Fill an instance's hvparams dict.
854
855     @type instance: object
856     @param instance: the instance parameter to fill
857     @rtype: dict
858     @return: a copy of the instance's hvparams with missing keys filled from
859         the cluster defaults
860
861     """
862     return FillDict(self.hvparams.get(instance.hypervisor, {}),
863                          instance.hvparams)
864
865   def FillBE(self, instance):
866     """Fill an instance's beparams dict.
867
868     @type instance: object
869     @param instance: the instance parameter to fill
870     @rtype: dict
871     @return: a copy of the instance's beparams with missing keys filled from
872         the cluster defaults
873
874     """
875     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
876                           instance.beparams)
877
878
879 class SerializableConfigParser(ConfigParser.SafeConfigParser):
880   """Simple wrapper over ConfigParse that allows serialization.
881
882   This class is basically ConfigParser.SafeConfigParser with two
883   additional methods that allow it to serialize/unserialize to/from a
884   buffer.
885
886   """
887   def Dumps(self):
888     """Dump this instance and return the string representation."""
889     buf = StringIO()
890     self.write(buf)
891     return buf.getvalue()
892
893   @staticmethod
894   def Loads(data):
895     """Load data from a string."""
896     buf = StringIO(data)
897     cfp = SerializableConfigParser()
898     cfp.readfp(buf)
899     return cfp