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