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