Generalize a recursive check on logical disks
[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
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
368
369 class NIC(ConfigObject):
370   """Config object representing a network card."""
371   __slots__ = ["mac", "ip", "bridge", "nicparams"]
372
373   @classmethod
374   def CheckParameterSyntax(cls, nicparams):
375     """Check the given parameters for validity.
376
377     @type nicparams:  dict
378     @param nicparams: dictionary with parameter names/value
379     @raise errors.ConfigurationError: when a parameter is not valid
380
381     """
382     if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
383       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
384       raise errors.ConfigurationError(err)
385
386     if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
387         not nicparams[constants.NIC_LINK]):
388       err = "Missing bridged nic link"
389       raise errors.ConfigurationError(err)
390
391   def UpgradeConfig(self):
392     """Fill defaults for missing configuration values.
393
394     """
395     if self.nicparams is None:
396       self.nicparams = {}
397       if self.bridge is not None:
398         self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
399         self.nicparams[constants.NIC_LINK] = self.bridge
400     # bridge is no longer used it 2.1. The slot is left there to support
401     # upgrading, but can be removed once upgrades to the current version
402     # straight from 2.0 are deprecated.
403     if self.bridge is not None:
404       self.bridge = None
405
406
407 class Disk(ConfigObject):
408   """Config object representing a block device."""
409   __slots__ = ["dev_type", "logical_id", "physical_id",
410                "children", "iv_name", "size", "mode"]
411
412   def CreateOnSecondary(self):
413     """Test if this device needs to be created on a secondary node."""
414     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
415
416   def AssembleOnSecondary(self):
417     """Test if this device needs to be assembled on a secondary node."""
418     return self.dev_type in (constants.LD_DRBD8, constants.LD_LV)
419
420   def OpenOnSecondary(self):
421     """Test if this device needs to be opened on a secondary node."""
422     return self.dev_type in (constants.LD_LV,)
423
424   def StaticDevPath(self):
425     """Return the device path if this device type has a static one.
426
427     Some devices (LVM for example) live always at the same /dev/ path,
428     irrespective of their status. For such devices, we return this
429     path, for others we return None.
430
431     @warning: The path returned is not a normalized pathname; callers
432         should check that it is a valid path.
433
434     """
435     if self.dev_type == constants.LD_LV:
436       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
437     return None
438
439   def ChildrenNeeded(self):
440     """Compute the needed number of children for activation.
441
442     This method will return either -1 (all children) or a positive
443     number denoting the minimum number of children needed for
444     activation (only mirrored devices will usually return >=0).
445
446     Currently, only DRBD8 supports diskless activation (therefore we
447     return 0), for all other we keep the previous semantics and return
448     -1.
449
450     """
451     if self.dev_type == constants.LD_DRBD8:
452       return 0
453     return -1
454
455   def IsBasedOnDiskType(self, dev_type):
456     """Check if the disk or its children are based on the given type.
457
458     @type dev_type: L{constants.LDS_BLOCK}
459     @param dev_type: the type to look for
460     @rtype: boolean
461     @return: boolean indicating if a device of the given type was found or not
462
463     """
464     if self.children:
465       for child in self.children:
466         if child.IsBasedOnDiskType(dev_type):
467           return True
468     return self.dev_type == dev_type
469
470   def GetNodes(self, node):
471     """This function returns the nodes this device lives on.
472
473     Given the node on which the parent of the device lives on (or, in
474     case of a top-level device, the primary node of the devices'
475     instance), this function will return a list of nodes on which this
476     devices needs to (or can) be assembled.
477
478     """
479     if self.dev_type in [constants.LD_LV, constants.LD_FILE]:
480       result = [node]
481     elif self.dev_type in constants.LDS_DRBD:
482       result = [self.logical_id[0], self.logical_id[1]]
483       if node not in result:
484         raise errors.ConfigurationError("DRBD device passed unknown node")
485     else:
486       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
487     return result
488
489   def ComputeNodeTree(self, parent_node):
490     """Compute the node/disk tree for this disk and its children.
491
492     This method, given the node on which the parent disk lives, will
493     return the list of all (node, disk) pairs which describe the disk
494     tree in the most compact way. For example, a drbd/lvm stack
495     will be returned as (primary_node, drbd) and (secondary_node, drbd)
496     which represents all the top-level devices on the nodes.
497
498     """
499     my_nodes = self.GetNodes(parent_node)
500     result = [(node, self) for node in my_nodes]
501     if not self.children:
502       # leaf device
503       return result
504     for node in my_nodes:
505       for child in self.children:
506         child_result = child.ComputeNodeTree(node)
507         if len(child_result) == 1:
508           # child (and all its descendants) is simple, doesn't split
509           # over multiple hosts, so we don't need to describe it, our
510           # own entry for this node describes it completely
511           continue
512         else:
513           # check if child nodes differ from my nodes; note that
514           # subdisk can differ from the child itself, and be instead
515           # one of its descendants
516           for subnode, subdisk in child_result:
517             if subnode not in my_nodes:
518               result.append((subnode, subdisk))
519             # otherwise child is under our own node, so we ignore this
520             # entry (but probably the other results in the list will
521             # be different)
522     return result
523
524   def RecordGrow(self, amount):
525     """Update the size of this disk after growth.
526
527     This method recurses over the disks's children and updates their
528     size correspondigly. The method needs to be kept in sync with the
529     actual algorithms from bdev.
530
531     """
532     if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
533       self.size += amount
534     elif self.dev_type == constants.LD_DRBD8:
535       if self.children:
536         self.children[0].RecordGrow(amount)
537       self.size += amount
538     else:
539       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
540                                    " disk type %s" % self.dev_type)
541
542   def UnsetSize(self):
543     """Sets recursively the size to zero for the disk and its children.
544
545     """
546     if self.children:
547       for child in self.children:
548         child.UnsetSize()
549     self.size = 0
550
551   def SetPhysicalID(self, target_node, nodes_ip):
552     """Convert the logical ID to the physical ID.
553
554     This is used only for drbd, which needs ip/port configuration.
555
556     The routine descends down and updates its children also, because
557     this helps when the only the top device is passed to the remote
558     node.
559
560     Arguments:
561       - target_node: the node we wish to configure for
562       - nodes_ip: a mapping of node name to ip
563
564     The target_node must exist in in nodes_ip, and must be one of the
565     nodes in the logical ID for each of the DRBD devices encountered
566     in the disk tree.
567
568     """
569     if self.children:
570       for child in self.children:
571         child.SetPhysicalID(target_node, nodes_ip)
572
573     if self.logical_id is None and self.physical_id is not None:
574       return
575     if self.dev_type in constants.LDS_DRBD:
576       pnode, snode, port, pminor, sminor, secret = self.logical_id
577       if target_node not in (pnode, snode):
578         raise errors.ConfigurationError("DRBD device not knowing node %s" %
579                                         target_node)
580       pnode_ip = nodes_ip.get(pnode, None)
581       snode_ip = nodes_ip.get(snode, None)
582       if pnode_ip is None or snode_ip is None:
583         raise errors.ConfigurationError("Can't find primary or secondary node"
584                                         " for %s" % str(self))
585       p_data = (pnode_ip, port)
586       s_data = (snode_ip, port)
587       if pnode == target_node:
588         self.physical_id = p_data + s_data + (pminor, secret)
589       else: # it must be secondary, we tested above
590         self.physical_id = s_data + p_data + (sminor, secret)
591     else:
592       self.physical_id = self.logical_id
593     return
594
595   def ToDict(self):
596     """Disk-specific conversion to standard python types.
597
598     This replaces the children lists of objects with lists of
599     standard python types.
600
601     """
602     bo = super(Disk, self).ToDict()
603
604     for attr in ("children",):
605       alist = bo.get(attr, None)
606       if alist:
607         bo[attr] = self._ContainerToDicts(alist)
608     return bo
609
610   @classmethod
611   def FromDict(cls, val):
612     """Custom function for Disks
613
614     """
615     obj = super(Disk, cls).FromDict(val)
616     if obj.children:
617       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
618     if obj.logical_id and isinstance(obj.logical_id, list):
619       obj.logical_id = tuple(obj.logical_id)
620     if obj.physical_id and isinstance(obj.physical_id, list):
621       obj.physical_id = tuple(obj.physical_id)
622     if obj.dev_type in constants.LDS_DRBD:
623       # we need a tuple of length six here
624       if len(obj.logical_id) < 6:
625         obj.logical_id += (None,) * (6 - len(obj.logical_id))
626     return obj
627
628   def __str__(self):
629     """Custom str() formatter for disks.
630
631     """
632     if self.dev_type == constants.LD_LV:
633       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
634     elif self.dev_type in constants.LDS_DRBD:
635       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
636       val = "<DRBD8("
637       if self.physical_id is None:
638         phy = "unconfigured"
639       else:
640         phy = ("configured as %s:%s %s:%s" %
641                (self.physical_id[0], self.physical_id[1],
642                 self.physical_id[2], self.physical_id[3]))
643
644       val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
645               (node_a, minor_a, node_b, minor_b, port, phy))
646       if self.children and self.children.count(None) == 0:
647         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
648       else:
649         val += "no local storage"
650     else:
651       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
652              (self.dev_type, self.logical_id, self.physical_id, self.children))
653     if self.iv_name is None:
654       val += ", not visible"
655     else:
656       val += ", visible as /dev/%s" % self.iv_name
657     if isinstance(self.size, int):
658       val += ", size=%dm)>" % self.size
659     else:
660       val += ", size='%s')>" % (self.size,)
661     return val
662
663   def Verify(self):
664     """Checks that this disk is correctly configured.
665
666     """
667     all_errors = []
668     if self.mode not in constants.DISK_ACCESS_SET:
669       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
670     return all_errors
671
672   def UpgradeConfig(self):
673     """Fill defaults for missing configuration values.
674
675     """
676     if self.children:
677       for child in self.children:
678         child.UpgradeConfig()
679     # add here config upgrade for this disk
680
681
682 class Instance(TaggableObject):
683   """Config object representing an instance."""
684   __slots__ = [
685     "name",
686     "primary_node",
687     "os",
688     "hypervisor",
689     "hvparams",
690     "beparams",
691     "osparams",
692     "admin_up",
693     "nics",
694     "disks",
695     "disk_template",
696     "network_port",
697     "serial_no",
698     ] + _TIMESTAMPS + _UUID
699
700   def _ComputeSecondaryNodes(self):
701     """Compute the list of secondary nodes.
702
703     This is a simple wrapper over _ComputeAllNodes.
704
705     """
706     all_nodes = set(self._ComputeAllNodes())
707     all_nodes.discard(self.primary_node)
708     return tuple(all_nodes)
709
710   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
711                              "List of secondary nodes")
712
713   def _ComputeAllNodes(self):
714     """Compute the list of all nodes.
715
716     Since the data is already there (in the drbd disks), keeping it as
717     a separate normal attribute is redundant and if not properly
718     synchronised can cause problems. Thus it's better to compute it
719     dynamically.
720
721     """
722     def _Helper(nodes, device):
723       """Recursively computes nodes given a top device."""
724       if device.dev_type in constants.LDS_DRBD:
725         nodea, nodeb = device.logical_id[:2]
726         nodes.add(nodea)
727         nodes.add(nodeb)
728       if device.children:
729         for child in device.children:
730           _Helper(nodes, child)
731
732     all_nodes = set()
733     all_nodes.add(self.primary_node)
734     for device in self.disks:
735       _Helper(all_nodes, device)
736     return tuple(all_nodes)
737
738   all_nodes = property(_ComputeAllNodes, None, None,
739                        "List of all nodes of the instance")
740
741   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
742     """Provide a mapping of nodes to LVs this instance owns.
743
744     This function figures out what logical volumes should belong on
745     which nodes, recursing through a device tree.
746
747     @param lvmap: optional dictionary to receive the
748         'node' : ['lv', ...] data.
749
750     @return: None if lvmap arg is given, otherwise, a dictionary
751         of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
752
753     """
754     if node == None:
755       node = self.primary_node
756
757     if lvmap is None:
758       lvmap = { node : [] }
759       ret = lvmap
760     else:
761       if not node in lvmap:
762         lvmap[node] = []
763       ret = None
764
765     if not devs:
766       devs = self.disks
767
768     for dev in devs:
769       if dev.dev_type == constants.LD_LV:
770         lvmap[node].append(dev.logical_id[1])
771
772       elif dev.dev_type in constants.LDS_DRBD:
773         if dev.children:
774           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
775           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
776
777       elif dev.children:
778         self.MapLVsByNode(lvmap, dev.children, node)
779
780     return ret
781
782   def FindDisk(self, idx):
783     """Find a disk given having a specified index.
784
785     This is just a wrapper that does validation of the index.
786
787     @type idx: int
788     @param idx: the disk index
789     @rtype: L{Disk}
790     @return: the corresponding disk
791     @raise errors.OpPrereqError: when the given index is not valid
792
793     """
794     try:
795       idx = int(idx)
796       return self.disks[idx]
797     except (TypeError, ValueError), err:
798       raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
799                                  errors.ECODE_INVAL)
800     except IndexError:
801       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
802                                  " 0 to %d" % (idx, len(self.disks)),
803                                  errors.ECODE_INVAL)
804
805   def ToDict(self):
806     """Instance-specific conversion to standard python types.
807
808     This replaces the children lists of objects with lists of standard
809     python types.
810
811     """
812     bo = super(Instance, self).ToDict()
813
814     for attr in "nics", "disks":
815       alist = bo.get(attr, None)
816       if alist:
817         nlist = self._ContainerToDicts(alist)
818       else:
819         nlist = []
820       bo[attr] = nlist
821     return bo
822
823   @classmethod
824   def FromDict(cls, val):
825     """Custom function for instances.
826
827     """
828     obj = super(Instance, cls).FromDict(val)
829     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
830     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
831     return obj
832
833   def UpgradeConfig(self):
834     """Fill defaults for missing configuration values.
835
836     """
837     for nic in self.nics:
838       nic.UpgradeConfig()
839     for disk in self.disks:
840       disk.UpgradeConfig()
841     if self.hvparams:
842       for key in constants.HVC_GLOBALS:
843         try:
844           del self.hvparams[key]
845         except KeyError:
846           pass
847     if self.osparams is None:
848       self.osparams = {}
849
850
851 class OS(ConfigObject):
852   """Config object representing an operating system.
853
854   @type supported_parameters: list
855   @ivar supported_parameters: a list of tuples, name and description,
856       containing the supported parameters by this OS
857
858   """
859   __slots__ = [
860     "name",
861     "path",
862     "api_versions",
863     "create_script",
864     "export_script",
865     "import_script",
866     "rename_script",
867     "verify_script",
868     "supported_variants",
869     "supported_parameters",
870     ]
871
872
873 class Node(TaggableObject):
874   """Config object representing a node."""
875   __slots__ = [
876     "name",
877     "primary_ip",
878     "secondary_ip",
879     "serial_no",
880     "master_candidate",
881     "offline",
882     "drained",
883     ] + _TIMESTAMPS + _UUID
884
885
886 class Cluster(TaggableObject):
887   """Config object representing the cluster."""
888   __slots__ = [
889     "serial_no",
890     "rsahostkeypub",
891     "highest_used_port",
892     "tcpudp_port_pool",
893     "mac_prefix",
894     "volume_group_name",
895     "drbd_usermode_helper",
896     "default_bridge",
897     "default_hypervisor",
898     "master_node",
899     "master_ip",
900     "master_netdev",
901     "cluster_name",
902     "file_storage_dir",
903     "enabled_hypervisors",
904     "hvparams",
905     "os_hvp",
906     "beparams",
907     "osparams",
908     "nicparams",
909     "candidate_pool_size",
910     "modify_etc_hosts",
911     "modify_ssh_setup",
912     "maintain_node_health",
913     "uid_pool",
914     ] + _TIMESTAMPS + _UUID
915
916   def UpgradeConfig(self):
917     """Fill defaults for missing configuration values.
918
919     """
920     # pylint: disable-msg=E0203
921     # because these are "defined" via slots, not manually
922     if self.hvparams is None:
923       self.hvparams = constants.HVC_DEFAULTS
924     else:
925       for hypervisor in self.hvparams:
926         self.hvparams[hypervisor] = FillDict(
927             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
928
929     if self.os_hvp is None:
930       self.os_hvp = {}
931
932     # osparams added before 2.2
933     if self.osparams is None:
934       self.osparams = {}
935
936     self.beparams = UpgradeGroupedParams(self.beparams,
937                                          constants.BEC_DEFAULTS)
938     migrate_default_bridge = not self.nicparams
939     self.nicparams = UpgradeGroupedParams(self.nicparams,
940                                           constants.NICC_DEFAULTS)
941     if migrate_default_bridge:
942       self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
943         self.default_bridge
944
945     if self.modify_etc_hosts is None:
946       self.modify_etc_hosts = True
947
948     if self.modify_ssh_setup is None:
949       self.modify_ssh_setup = True
950
951     # default_bridge is no longer used it 2.1. The slot is left there to
952     # support auto-upgrading. It can be removed once we decide to deprecate
953     # upgrading straight from 2.0.
954     if self.default_bridge is not None:
955       self.default_bridge = None
956
957     # default_hypervisor is just the first enabled one in 2.1. This slot and
958     # code can be removed once upgrading straight from 2.0 is deprecated.
959     if self.default_hypervisor is not None:
960       self.enabled_hypervisors = ([self.default_hypervisor] +
961         [hvname for hvname in self.enabled_hypervisors
962          if hvname != self.default_hypervisor])
963       self.default_hypervisor = None
964
965     # maintain_node_health added after 2.1.1
966     if self.maintain_node_health is None:
967       self.maintain_node_health = False
968
969     if self.uid_pool is None:
970       self.uid_pool = []
971
972   def ToDict(self):
973     """Custom function for cluster.
974
975     """
976     mydict = super(Cluster, self).ToDict()
977     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
978     return mydict
979
980   @classmethod
981   def FromDict(cls, val):
982     """Custom function for cluster.
983
984     """
985     obj = super(Cluster, cls).FromDict(val)
986     if not isinstance(obj.tcpudp_port_pool, set):
987       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
988     return obj
989
990   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
991     """Get the default hypervisor parameters for the cluster.
992
993     @param hypervisor: the hypervisor name
994     @param os_name: if specified, we'll also update the defaults for this OS
995     @param skip_keys: if passed, list of keys not to use
996     @return: the defaults dict
997
998     """
999     if skip_keys is None:
1000       skip_keys = []
1001
1002     fill_stack = [self.hvparams.get(hypervisor, {})]
1003     if os_name is not None:
1004       os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1005       fill_stack.append(os_hvp)
1006
1007     ret_dict = {}
1008     for o_dict in fill_stack:
1009       ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1010
1011     return ret_dict
1012
1013   def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1014     """Fill a given hvparams dict with cluster defaults.
1015
1016     @type hv_name: string
1017     @param hv_name: the hypervisor to use
1018     @type os_name: string
1019     @param os_name: the OS to use for overriding the hypervisor defaults
1020     @type skip_globals: boolean
1021     @param skip_globals: if True, the global hypervisor parameters will
1022         not be filled
1023     @rtype: dict
1024     @return: a copy of the given hvparams with missing keys filled from
1025         the cluster defaults
1026
1027     """
1028     if skip_globals:
1029       skip_keys = constants.HVC_GLOBALS
1030     else:
1031       skip_keys = []
1032
1033     def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1034     return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1035
1036   def FillHV(self, instance, skip_globals=False):
1037     """Fill an instance's hvparams dict with cluster defaults.
1038
1039     @type instance: L{objects.Instance}
1040     @param instance: the instance parameter to fill
1041     @type skip_globals: boolean
1042     @param skip_globals: if True, the global hypervisor parameters will
1043         not be filled
1044     @rtype: dict
1045     @return: a copy of the instance's hvparams with missing keys filled from
1046         the cluster defaults
1047
1048     """
1049     return self.SimpleFillHV(instance.hypervisor, instance.os,
1050                              instance.hvparams, skip_globals)
1051
1052   def SimpleFillBE(self, beparams):
1053     """Fill a given beparams dict with cluster defaults.
1054
1055     @type beparams: dict
1056     @param beparams: the dict to fill
1057     @rtype: dict
1058     @return: a copy of the passed in beparams with missing keys filled
1059         from the cluster defaults
1060
1061     """
1062     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1063
1064   def FillBE(self, instance):
1065     """Fill an instance's beparams dict with cluster defaults.
1066
1067     @type instance: L{objects.Instance}
1068     @param instance: the instance parameter to fill
1069     @rtype: dict
1070     @return: a copy of the instance's beparams with missing keys filled from
1071         the cluster defaults
1072
1073     """
1074     return self.SimpleFillBE(instance.beparams)
1075
1076   def SimpleFillNIC(self, nicparams):
1077     """Fill a given nicparams dict with cluster defaults.
1078
1079     @type nicparams: dict
1080     @param nicparams: the dict to fill
1081     @rtype: dict
1082     @return: a copy of the passed in nicparams with missing keys filled
1083         from the cluster defaults
1084
1085     """
1086     return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1087
1088   def SimpleFillOS(self, os_name, os_params):
1089     """Fill an instance's osparams dict with cluster defaults.
1090
1091     @type os_name: string
1092     @param os_name: the OS name to use
1093     @type os_params: dict
1094     @param os_params: the dict to fill with default values
1095     @rtype: dict
1096     @return: a copy of the instance's osparams with missing keys filled from
1097         the cluster defaults
1098
1099     """
1100     name_only = os_name.split("+", 1)[0]
1101     # base OS
1102     result = self.osparams.get(name_only, {})
1103     # OS with variant
1104     result = FillDict(result, self.osparams.get(os_name, {}))
1105     # specified params
1106     return FillDict(result, os_params)
1107
1108
1109 class BlockDevStatus(ConfigObject):
1110   """Config object representing the status of a block device."""
1111   __slots__ = [
1112     "dev_path",
1113     "major",
1114     "minor",
1115     "sync_percent",
1116     "estimated_time",
1117     "is_degraded",
1118     "ldisk_status",
1119     ]
1120
1121
1122 class ImportExportStatus(ConfigObject):
1123   """Config object representing the status of an import or export."""
1124   __slots__ = [
1125     "recent_output",
1126     "listen_port",
1127     "connected",
1128     "progress_mbytes",
1129     "progress_throughput",
1130     "progress_eta",
1131     "progress_percent",
1132     "exit_status",
1133     "error_message",
1134     ] + _TIMESTAMPS
1135
1136
1137 class ImportExportOptions(ConfigObject):
1138   """Options for import/export daemon
1139
1140   @ivar key_name: X509 key name (None for cluster certificate)
1141   @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1142   @ivar compress: Compression method (one of L{constants.IEC_ALL})
1143   @ivar magic: Used to ensure the connection goes to the right disk
1144
1145   """
1146   __slots__ = [
1147     "key_name",
1148     "ca_pem",
1149     "compress",
1150     "magic",
1151     ]
1152
1153
1154 class ConfdRequest(ConfigObject):
1155   """Object holding a confd request.
1156
1157   @ivar protocol: confd protocol version
1158   @ivar type: confd query type
1159   @ivar query: query request
1160   @ivar rsalt: requested reply salt
1161
1162   """
1163   __slots__ = [
1164     "protocol",
1165     "type",
1166     "query",
1167     "rsalt",
1168     ]
1169
1170
1171 class ConfdReply(ConfigObject):
1172   """Object holding a confd reply.
1173
1174   @ivar protocol: confd protocol version
1175   @ivar status: reply status code (ok, error)
1176   @ivar answer: confd query reply
1177   @ivar serial: configuration serial number
1178
1179   """
1180   __slots__ = [
1181     "protocol",
1182     "status",
1183     "answer",
1184     "serial",
1185     ]
1186
1187
1188 class SerializableConfigParser(ConfigParser.SafeConfigParser):
1189   """Simple wrapper over ConfigParse that allows serialization.
1190
1191   This class is basically ConfigParser.SafeConfigParser with two
1192   additional methods that allow it to serialize/unserialize to/from a
1193   buffer.
1194
1195   """
1196   def Dumps(self):
1197     """Dump this instance and return the string representation."""
1198     buf = StringIO()
1199     self.write(buf)
1200     return buf.getvalue()
1201
1202   @classmethod
1203   def Loads(cls, data):
1204     """Load data from a string."""
1205     buf = StringIO(data)
1206     cfp = cls()
1207     cfp.readfp(buf)
1208     return cfp