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