23bb0f9776b96f4bc97212a56610752a2cb54917
[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"]
41
42
43 class ConfigObject(object):
44   """A generic config object.
45
46   It has the following properties:
47
48     - provides somewhat safe recursive unpickling and pickling for its classes
49     - unset attributes which are defined in slots are always returned
50       as None instead of raising an error
51
52   Classes derived from this must always declare __slots__ (we use many
53   config objects and the memory reduction is useful)
54
55   """
56   __slots__ = []
57
58   def __init__(self, **kwargs):
59     for k, v in kwargs.iteritems():
60       setattr(self, k, v)
61     self.UpgradeConfig()
62
63   def __getattr__(self, name):
64     if name not in self.__slots__:
65       raise AttributeError("Invalid object attribute %s.%s" %
66                            (type(self).__name__, name))
67     return None
68
69   def __setitem__(self, key, value):
70     if key not in self.__slots__:
71       raise KeyError(key)
72     setattr(self, key, value)
73
74   def __getstate__(self):
75     state = {}
76     for name in self.__slots__:
77       if hasattr(self, name):
78         state[name] = getattr(self, name)
79     return state
80
81   def __setstate__(self, state):
82     for name in state:
83       if name in self.__slots__:
84         setattr(self, name, state[name])
85
86   def ToDict(self):
87     """Convert to a dict holding only standard python types.
88
89     The generic routine just dumps all of this object's attributes in
90     a dict. It does not work if the class has children who are
91     ConfigObjects themselves (e.g. the nics list in an Instance), in
92     which case the object should subclass the function in order to
93     make sure all objects returned are only standard python types.
94
95     """
96     return dict([(k, getattr(self, k, None)) for k in self.__slots__])
97
98   @classmethod
99   def FromDict(cls, val):
100     """Create an object from a dictionary.
101
102     This generic routine takes a dict, instantiates a new instance of
103     the given class, and sets attributes based on the dict content.
104
105     As for `ToDict`, this does not work if the class has children
106     who are ConfigObjects themselves (e.g. the nics list in an
107     Instance), in which case the object should subclass the function
108     and alter the objects.
109
110     """
111     if not isinstance(val, dict):
112       raise errors.ConfigurationError("Invalid object passed to FromDict:"
113                                       " expected dict, got %s" % type(val))
114     val_str = dict([(str(k), v) for k, v in val.iteritems()])
115     obj = cls(**val_str)
116     return obj
117
118   @staticmethod
119   def _ContainerToDicts(container):
120     """Convert the elements of a container to standard python types.
121
122     This method converts a container with elements derived from
123     ConfigData to standard python types. If the container is a dict,
124     we don't touch the keys, only the values.
125
126     """
127     if isinstance(container, dict):
128       ret = dict([(k, v.ToDict()) for k, v in container.iteritems()])
129     elif isinstance(container, (list, tuple, set, frozenset)):
130       ret = [elem.ToDict() for elem in container]
131     else:
132       raise TypeError("Invalid type %s passed to _ContainerToDicts" %
133                       type(container))
134     return ret
135
136   @staticmethod
137   def _ContainerFromDicts(source, c_type, e_type):
138     """Convert a container from standard python types.
139
140     This method converts a container with standard python types to
141     ConfigData objects. If the container is a dict, we don't touch the
142     keys, only the values.
143
144     """
145     if not isinstance(c_type, type):
146       raise TypeError("Container type %s passed to _ContainerFromDicts is"
147                       " not a type" % type(c_type))
148     if c_type is dict:
149       ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
150     elif c_type in (list, tuple, set, frozenset):
151       ret = c_type([e_type.FromDict(elem) for elem in source])
152     else:
153       raise TypeError("Invalid container type %s passed to"
154                       " _ContainerFromDicts" % c_type)
155     return ret
156
157   def Copy(self):
158     """Makes a deep copy of the current object and its children.
159
160     """
161     dict_form = self.ToDict()
162     clone_obj = self.__class__.FromDict(dict_form)
163     return clone_obj
164
165   def __repr__(self):
166     """Implement __repr__ for ConfigObjects."""
167     return repr(self.ToDict())
168
169   def UpgradeConfig(self):
170     """Fill defaults for missing configuration values.
171
172     This method will be called at object init time, and its implementation will
173     be object dependent.
174
175     """
176     pass
177
178
179 class TaggableObject(ConfigObject):
180   """An generic class supporting tags.
181
182   """
183   __slots__ = ConfigObject.__slots__ + ["tags"]
184
185   @staticmethod
186   def ValidateTag(tag):
187     """Check if a tag is valid.
188
189     If the tag is invalid, an errors.TagError will be raised. The
190     function has no return value.
191
192     """
193     if not isinstance(tag, basestring):
194       raise errors.TagError("Invalid tag type (not a string)")
195     if len(tag) > constants.MAX_TAG_LEN:
196       raise errors.TagError("Tag too long (>%d characters)" %
197                             constants.MAX_TAG_LEN)
198     if not tag:
199       raise errors.TagError("Tags cannot be empty")
200     if not re.match("^[\w.+*/:-]+$", tag):
201       raise errors.TagError("Tag contains invalid characters")
202
203   def GetTags(self):
204     """Return the tags list.
205
206     """
207     tags = getattr(self, "tags", None)
208     if tags is None:
209       tags = self.tags = set()
210     return tags
211
212   def AddTag(self, tag):
213     """Add a new tag.
214
215     """
216     self.ValidateTag(tag)
217     tags = self.GetTags()
218     if len(tags) >= constants.MAX_TAGS_PER_OBJ:
219       raise errors.TagError("Too many tags")
220     self.GetTags().add(tag)
221
222   def RemoveTag(self, tag):
223     """Remove a tag.
224
225     """
226     self.ValidateTag(tag)
227     tags = self.GetTags()
228     try:
229       tags.remove(tag)
230     except KeyError:
231       raise errors.TagError("Tag not found")
232
233   def ToDict(self):
234     """Taggable-object-specific conversion to standard python types.
235
236     This replaces the tags set with a list.
237
238     """
239     bo = super(TaggableObject, self).ToDict()
240
241     tags = bo.get("tags", None)
242     if isinstance(tags, set):
243       bo["tags"] = list(tags)
244     return bo
245
246   @classmethod
247   def FromDict(cls, val):
248     """Custom function for instances.
249
250     """
251     obj = super(TaggableObject, cls).FromDict(val)
252     if hasattr(obj, "tags") and isinstance(obj.tags, list):
253       obj.tags = set(obj.tags)
254     return obj
255
256
257 class ConfigData(ConfigObject):
258   """Top-level config object."""
259   __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
260
261   def ToDict(self):
262     """Custom function for top-level config data.
263
264     This just replaces the list of instances, nodes and the cluster
265     with standard python types.
266
267     """
268     mydict = super(ConfigData, self).ToDict()
269     mydict["cluster"] = mydict["cluster"].ToDict()
270     for key in "nodes", "instances":
271       mydict[key] = self._ContainerToDicts(mydict[key])
272
273     return mydict
274
275   @classmethod
276   def FromDict(cls, val):
277     """Custom function for top-level config data
278
279     """
280     obj = super(ConfigData, cls).FromDict(val)
281     obj.cluster = Cluster.FromDict(obj.cluster)
282     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
283     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
284     return obj
285
286
287 class NIC(ConfigObject):
288   """Config object representing a network card."""
289   __slots__ = ["mac", "ip", "bridge"]
290
291
292 class Disk(ConfigObject):
293   """Config object representing a block device."""
294   __slots__ = ["dev_type", "logical_id", "physical_id",
295                "children", "iv_name", "size", "mode"]
296
297   def CreateOnSecondary(self):
298     """Test if this device needs to be created on a secondary node."""
299     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
300
301   def AssembleOnSecondary(self):
302     """Test if this device needs to be assembled on a secondary node."""
303     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
304
305   def OpenOnSecondary(self):
306     """Test if this device needs to be opened on a secondary node."""
307     return self.dev_type in (constants.LD_LV,)
308
309   def StaticDevPath(self):
310     """Return the device path if this device type has a static one.
311
312     Some devices (LVM for example) live always at the same /dev/ path,
313     irrespective of their status. For such devices, we return this
314     path, for others we return None.
315
316     """
317     if self.dev_type == constants.LD_LV:
318       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
319     return None
320
321   def ChildrenNeeded(self):
322     """Compute the needed number of children for activation.
323
324     This method will return either -1 (all children) or a positive
325     number denoting the minimum number of children needed for
326     activation (only mirrored devices will usually return >=0).
327
328     Currently, only DRBD8 supports diskless activation (therefore we
329     return 0), for all other we keep the previous semantics and return
330     -1.
331
332     """
333     if self.dev_type == constants.LD_DRBD8:
334       return 0
335     return -1
336
337   def GetNodes(self, node):
338     """This function returns the nodes this device lives on.
339
340     Given the node on which the parent of the device lives on (or, in
341     case of a top-level device, the primary node of the devices'
342     instance), this function will return a list of nodes on which this
343     devices needs to (or can) be assembled.
344
345     """
346     if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
347       result = [node]
348     elif self.dev_type in constants.LDS_DRBD:
349       result = [self.logical_id[0], self.logical_id[1]]
350       if node not in result:
351         raise errors.ConfigurationError("DRBD device passed unknown node")
352     else:
353       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
354     return result
355
356   def ComputeNodeTree(self, parent_node):
357     """Compute the node/disk tree for this disk and its children.
358
359     This method, given the node on which the parent disk lives, will
360     return the list of all (node, disk) pairs which describe the disk
361     tree in the most compact way. For example, a drbd/lvm stack
362     will be returned as (primary_node, drbd) and (secondary_node, drbd)
363     which represents all the top-level devices on the nodes.
364
365     """
366     my_nodes = self.GetNodes(parent_node)
367     result = [(node, self) for node in my_nodes]
368     if not self.children:
369       # leaf device
370       return result
371     for node in my_nodes:
372       for child in self.children:
373         child_result = child.ComputeNodeTree(node)
374         if len(child_result) == 1:
375           # child (and all its descendants) is simple, doesn't split
376           # over multiple hosts, so we don't need to describe it, our
377           # own entry for this node describes it completely
378           continue
379         else:
380           # check if child nodes differ from my nodes; note that
381           # subdisk can differ from the child itself, and be instead
382           # one of its descendants
383           for subnode, subdisk in child_result:
384             if subnode not in my_nodes:
385               result.append((subnode, subdisk))
386             # otherwise child is under our own node, so we ignore this
387             # entry (but probably the other results in the list will
388             # be different)
389     return result
390
391   def RecordGrow(self, amount):
392     """Update the size of this disk after growth.
393
394     This method recurses over the disks's children and updates their
395     size correspondigly. The method needs to be kept in sync with the
396     actual algorithms from bdev.
397
398     """
399     if self.dev_type == constants.LD_LV:
400       self.size += amount
401     elif self.dev_type == constants.LD_DRBD8:
402       if self.children:
403         self.children[0].RecordGrow(amount)
404       self.size += amount
405     else:
406       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
407                                    " disk type %s" % self.dev_type)
408
409   def UnsetSize(self):
410     """Sets recursively the size to zero for the disk and its children.
411
412     """
413     if self.children:
414       for child in self.children:
415         child.UnsetSize()
416     self.size = 0
417
418   def SetPhysicalID(self, target_node, nodes_ip):
419     """Convert the logical ID to the physical ID.
420
421     This is used only for drbd, which needs ip/port configuration.
422
423     The routine descends down and updates its children also, because
424     this helps when the only the top device is passed to the remote
425     node.
426
427     Arguments:
428       - target_node: the node we wish to configure for
429       - nodes_ip: a mapping of node name to ip
430
431     The target_node must exist in in nodes_ip, and must be one of the
432     nodes in the logical ID for each of the DRBD devices encountered
433     in the disk tree.
434
435     """
436     if self.children:
437       for child in self.children:
438         child.SetPhysicalID(target_node, nodes_ip)
439
440     if self.logical_id is None and self.physical_id is not None:
441       return
442     if self.dev_type in constants.LDS_DRBD:
443       pnode, snode, port, pminor, sminor, secret = self.logical_id
444       if target_node not in (pnode, snode):
445         raise errors.ConfigurationError("DRBD device not knowing node %s" %
446                                         target_node)
447       pnode_ip = nodes_ip.get(pnode, None)
448       snode_ip = nodes_ip.get(snode, None)
449       if pnode_ip is None or snode_ip is None:
450         raise errors.ConfigurationError("Can't find primary or secondary node"
451                                         " for %s" % str(self))
452       p_data = (pnode_ip, port)
453       s_data = (snode_ip, port)
454       if pnode == target_node:
455         self.physical_id = p_data + s_data + (pminor, secret)
456       else: # it must be secondary, we tested above
457         self.physical_id = s_data + p_data + (sminor, secret)
458     else:
459       self.physical_id = self.logical_id
460     return
461
462   def ToDict(self):
463     """Disk-specific conversion to standard python types.
464
465     This replaces the children lists of objects with lists of
466     standard python types.
467
468     """
469     bo = super(Disk, self).ToDict()
470
471     for attr in ("children",):
472       alist = bo.get(attr, None)
473       if alist:
474         bo[attr] = self._ContainerToDicts(alist)
475     return bo
476
477   @classmethod
478   def FromDict(cls, val):
479     """Custom function for Disks
480
481     """
482     obj = super(Disk, cls).FromDict(val)
483     if obj.children:
484       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
485     if obj.logical_id and isinstance(obj.logical_id, list):
486       obj.logical_id = tuple(obj.logical_id)
487     if obj.physical_id and isinstance(obj.physical_id, list):
488       obj.physical_id = tuple(obj.physical_id)
489     if obj.dev_type in constants.LDS_DRBD:
490       # we need a tuple of length six here
491       if len(obj.logical_id) < 6:
492         obj.logical_id += (None,) * (6 - len(obj.logical_id))
493     return obj
494
495   def __str__(self):
496     """Custom str() formatter for disks.
497
498     """
499     if self.dev_type == constants.LD_LV:
500       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
501     elif self.dev_type in constants.LDS_DRBD:
502       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
503       val = "<DRBD8("
504       if self.physical_id is None:
505         phy = "unconfigured"
506       else:
507         phy = ("configured as %s:%s %s:%s" %
508                (self.physical_id[0], self.physical_id[1],
509                 self.physical_id[2], self.physical_id[3]))
510
511       val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
512               (node_a, minor_a, node_b, minor_b, port, phy))
513       if self.children and self.children.count(None) == 0:
514         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
515       else:
516         val += "no local storage"
517     else:
518       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
519              (self.dev_type, self.logical_id, self.physical_id, self.children))
520     if self.iv_name is None:
521       val += ", not visible"
522     else:
523       val += ", visible as /dev/%s" % self.iv_name
524     if isinstance(self.size, int):
525       val += ", size=%dm)>" % self.size
526     else:
527       val += ", size='%s')>" % (self.size,)
528     return val
529
530   def Verify(self):
531     """Checks that this disk is correctly configured.
532
533     """
534     all_errors = []
535     if self.mode not in constants.DISK_ACCESS_SET:
536       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
537     return all_errors
538
539
540 class Instance(TaggableObject):
541   """Config object representing an instance."""
542   __slots__ = TaggableObject.__slots__ + [
543     "name",
544     "primary_node",
545     "os",
546     "hypervisor",
547     "hvparams",
548     "beparams",
549     "admin_up",
550     "nics",
551     "disks",
552     "disk_template",
553     "network_port",
554     "serial_no",
555     ]
556
557   def _ComputeSecondaryNodes(self):
558     """Compute the list of secondary nodes.
559
560     This is a simple wrapper over _ComputeAllNodes.
561
562     """
563     all_nodes = set(self._ComputeAllNodes())
564     all_nodes.discard(self.primary_node)
565     return tuple(all_nodes)
566
567   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
568                              "List of secondary nodes")
569
570   def _ComputeAllNodes(self):
571     """Compute the list of all nodes.
572
573     Since the data is already there (in the drbd disks), keeping it as
574     a separate normal attribute is redundant and if not properly
575     synchronised can cause problems. Thus it's better to compute it
576     dynamically.
577
578     """
579     def _Helper(nodes, device):
580       """Recursively computes nodes given a top device."""
581       if device.dev_type in constants.LDS_DRBD:
582         nodea, nodeb = device.logical_id[:2]
583         nodes.add(nodea)
584         nodes.add(nodeb)
585       if device.children:
586         for child in device.children:
587           _Helper(nodes, child)
588
589     all_nodes = set()
590     all_nodes.add(self.primary_node)
591     for device in self.disks:
592       _Helper(all_nodes, device)
593     return tuple(all_nodes)
594
595   all_nodes = property(_ComputeAllNodes, None, None,
596                        "List of all nodes of the instance")
597
598   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
599     """Provide a mapping of nodes to LVs this instance owns.
600
601     This function figures out what logical volumes should belong on
602     which nodes, recursing through a device tree.
603
604     @param lvmap: optional dictionary to receive the
605         'node' : ['lv', ...] data.
606
607     @return: None if lvmap arg is given, otherwise, a dictionary
608         of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
609
610     """
611     if node == None:
612       node = self.primary_node
613
614     if lvmap is None:
615       lvmap = { node : [] }
616       ret = lvmap
617     else:
618       if not node in lvmap:
619         lvmap[node] = []
620       ret = None
621
622     if not devs:
623       devs = self.disks
624
625     for dev in devs:
626       if dev.dev_type == constants.LD_LV:
627         lvmap[node].append(dev.logical_id[1])
628
629       elif dev.dev_type in constants.LDS_DRBD:
630         if dev.children:
631           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
632           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
633
634       elif dev.children:
635         self.MapLVsByNode(lvmap, dev.children, node)
636
637     return ret
638
639   def FindDisk(self, idx):
640     """Find a disk given having a specified index.
641
642     This is just a wrapper that does validation of the index.
643
644     @type idx: int
645     @param idx: the disk index
646     @rtype: L{Disk}
647     @return: the corresponding disk
648     @raise errors.OpPrereqError: when the given index is not valid
649
650     """
651     try:
652       idx = int(idx)
653       return self.disks[idx]
654     except ValueError, err:
655       raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
656     except IndexError:
657       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
658                                  " 0 to %d" % (idx, len(self.disks)))
659
660   def ToDict(self):
661     """Instance-specific conversion to standard python types.
662
663     This replaces the children lists of objects with lists of standard
664     python types.
665
666     """
667     bo = super(Instance, self).ToDict()
668
669     for attr in "nics", "disks":
670       alist = bo.get(attr, None)
671       if alist:
672         nlist = self._ContainerToDicts(alist)
673       else:
674         nlist = []
675       bo[attr] = nlist
676     return bo
677
678   @classmethod
679   def FromDict(cls, val):
680     """Custom function for instances.
681
682     """
683     obj = super(Instance, cls).FromDict(val)
684     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
685     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
686     return obj
687
688
689 class OS(ConfigObject):
690   """Config object representing an operating system."""
691   __slots__ = [
692     "name",
693     "path",
694     "status",
695     "api_versions",
696     "create_script",
697     "export_script",
698     "import_script",
699     "rename_script",
700     ]
701
702   @classmethod
703   def FromInvalidOS(cls, err):
704     """Create an OS from an InvalidOS error.
705
706     This routine knows how to convert an InvalidOS error to an OS
707     object representing the broken OS with a meaningful error message.
708
709     """
710     if not isinstance(err, errors.InvalidOS):
711       raise errors.ProgrammerError("Trying to initialize an OS from an"
712                                    " invalid object of type %s" % type(err))
713
714     return cls(name=err.args[0], path=err.args[1], status=err.args[2])
715
716   def __nonzero__(self):
717     return self.status == constants.OS_VALID_STATUS
718
719   __bool__ = __nonzero__
720
721
722 class Node(TaggableObject):
723   """Config object representing a node."""
724   __slots__ = TaggableObject.__slots__ + [
725     "name",
726     "primary_ip",
727     "secondary_ip",
728     "serial_no",
729     "master_candidate",
730     "offline",
731     "drained",
732     ]
733
734
735 class Cluster(TaggableObject):
736   """Config object representing the cluster."""
737   __slots__ = TaggableObject.__slots__ + [
738     "serial_no",
739     "rsahostkeypub",
740     "highest_used_port",
741     "tcpudp_port_pool",
742     "mac_prefix",
743     "volume_group_name",
744     "default_bridge",
745     "default_hypervisor",
746     "master_node",
747     "master_ip",
748     "master_netdev",
749     "cluster_name",
750     "file_storage_dir",
751     "enabled_hypervisors",
752     "hvparams",
753     "beparams",
754     "candidate_pool_size",
755     ]
756
757   def ToDict(self):
758     """Custom function for cluster.
759
760     """
761     mydict = super(Cluster, self).ToDict()
762     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
763     return mydict
764
765   @classmethod
766   def FromDict(cls, val):
767     """Custom function for cluster.
768
769     """
770     obj = super(Cluster, cls).FromDict(val)
771     if not isinstance(obj.tcpudp_port_pool, set):
772       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
773     return obj
774
775   @staticmethod
776   def FillDict(defaults_dict, custom_dict):
777     """Basic function to apply settings on top a default dict.
778
779     @type defaults_dict: dict
780     @param defaults_dict: dictionary holding the default values
781     @type custom_dict: dict
782     @param custom_dict: dictionary holding customized value
783     @rtype: dict
784     @return: dict with the 'full' values
785
786     """
787     ret_dict = copy.deepcopy(defaults_dict)
788     ret_dict.update(custom_dict)
789     return ret_dict
790
791   def FillHV(self, instance):
792     """Fill an instance's hvparams dict.
793
794     @type instance: L{objects.Instance}
795     @param instance: the instance parameter to fill
796     @rtype: dict
797     @return: a copy of the instance's hvparams with missing keys filled from
798         the cluster defaults
799
800     """
801     return self.FillDict(self.hvparams.get(instance.hypervisor, {}),
802                          instance.hvparams)
803
804   def FillBE(self, instance):
805     """Fill an instance's beparams dict.
806
807     @type instance: L{objects.Instance}
808     @param instance: the instance parameter to fill
809     @rtype: dict
810     @return: a copy of the instance's beparams with missing keys filled from
811         the cluster defaults
812
813     """
814     return self.FillDict(self.beparams.get(constants.BEGR_DEFAULT, {}),
815                          instance.beparams)
816
817
818 class SerializableConfigParser(ConfigParser.SafeConfigParser):
819   """Simple wrapper over ConfigParse that allows serialization.
820
821   This class is basically ConfigParser.SafeConfigParser with two
822   additional methods that allow it to serialize/unserialize to/from a
823   buffer.
824
825   """
826   def Dumps(self):
827     """Dump this instance and return the string representation."""
828     buf = StringIO()
829     self.write(buf)
830     return buf.getvalue()
831
832   @staticmethod
833   def Loads(data):
834     """Load data from a string."""
835     buf = StringIO(data)
836     cfp = SerializableConfigParser()
837     cfp.readfp(buf)
838     return cfp