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