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