Merge branch 'devel-2.5'
[ganeti-local] / lib / objects.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2008, 2009, 2010, 2011 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 and
405         nicparams[constants.NIC_MODE] != constants.VALUE_AUTO):
406       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
407       raise errors.ConfigurationError(err)
408
409     if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
410         not nicparams[constants.NIC_LINK]):
411       err = "Missing bridged nic link"
412       raise errors.ConfigurationError(err)
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     elif self.dev_type == constants.LD_BLOCKDEV:
446       return self.logical_id[1]
447     return None
448
449   def ChildrenNeeded(self):
450     """Compute the needed number of children for activation.
451
452     This method will return either -1 (all children) or a positive
453     number denoting the minimum number of children needed for
454     activation (only mirrored devices will usually return >=0).
455
456     Currently, only DRBD8 supports diskless activation (therefore we
457     return 0), for all other we keep the previous semantics and return
458     -1.
459
460     """
461     if self.dev_type == constants.LD_DRBD8:
462       return 0
463     return -1
464
465   def IsBasedOnDiskType(self, dev_type):
466     """Check if the disk or its children are based on the given type.
467
468     @type dev_type: L{constants.LDS_BLOCK}
469     @param dev_type: the type to look for
470     @rtype: boolean
471     @return: boolean indicating if a device of the given type was found or not
472
473     """
474     if self.children:
475       for child in self.children:
476         if child.IsBasedOnDiskType(dev_type):
477           return True
478     return self.dev_type == dev_type
479
480   def GetNodes(self, node):
481     """This function returns the nodes this device lives on.
482
483     Given the node on which the parent of the device lives on (or, in
484     case of a top-level device, the primary node of the devices'
485     instance), this function will return a list of nodes on which this
486     devices needs to (or can) be assembled.
487
488     """
489     if self.dev_type in [constants.LD_LV, constants.LD_FILE,
490                          constants.LD_BLOCKDEV]:
491       result = [node]
492     elif self.dev_type in constants.LDS_DRBD:
493       result = [self.logical_id[0], self.logical_id[1]]
494       if node not in result:
495         raise errors.ConfigurationError("DRBD device passed unknown node")
496     else:
497       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
498     return result
499
500   def ComputeNodeTree(self, parent_node):
501     """Compute the node/disk tree for this disk and its children.
502
503     This method, given the node on which the parent disk lives, will
504     return the list of all (node, disk) pairs which describe the disk
505     tree in the most compact way. For example, a drbd/lvm stack
506     will be returned as (primary_node, drbd) and (secondary_node, drbd)
507     which represents all the top-level devices on the nodes.
508
509     """
510     my_nodes = self.GetNodes(parent_node)
511     result = [(node, self) for node in my_nodes]
512     if not self.children:
513       # leaf device
514       return result
515     for node in my_nodes:
516       for child in self.children:
517         child_result = child.ComputeNodeTree(node)
518         if len(child_result) == 1:
519           # child (and all its descendants) is simple, doesn't split
520           # over multiple hosts, so we don't need to describe it, our
521           # own entry for this node describes it completely
522           continue
523         else:
524           # check if child nodes differ from my nodes; note that
525           # subdisk can differ from the child itself, and be instead
526           # one of its descendants
527           for subnode, subdisk in child_result:
528             if subnode not in my_nodes:
529               result.append((subnode, subdisk))
530             # otherwise child is under our own node, so we ignore this
531             # entry (but probably the other results in the list will
532             # be different)
533     return result
534
535   def ComputeGrowth(self, amount):
536     """Compute the per-VG growth requirements.
537
538     This only works for VG-based disks.
539
540     @type amount: integer
541     @param amount: the desired increase in (user-visible) disk space
542     @rtype: dict
543     @return: a dictionary of volume-groups and the required size
544
545     """
546     if self.dev_type == constants.LD_LV:
547       return {self.logical_id[0]: amount}
548     elif self.dev_type == constants.LD_DRBD8:
549       if self.children:
550         return self.children[0].ComputeGrowth(amount)
551       else:
552         return {}
553     else:
554       # Other disk types do not require VG space
555       return {}
556
557   def RecordGrow(self, amount):
558     """Update the size of this disk after growth.
559
560     This method recurses over the disks's children and updates their
561     size correspondigly. The method needs to be kept in sync with the
562     actual algorithms from bdev.
563
564     """
565     if self.dev_type in (constants.LD_LV, constants.LD_FILE):
566       self.size += amount
567     elif self.dev_type == constants.LD_DRBD8:
568       if self.children:
569         self.children[0].RecordGrow(amount)
570       self.size += amount
571     else:
572       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
573                                    " disk type %s" % self.dev_type)
574
575   def UnsetSize(self):
576     """Sets recursively the size to zero for the disk and its children.
577
578     """
579     if self.children:
580       for child in self.children:
581         child.UnsetSize()
582     self.size = 0
583
584   def SetPhysicalID(self, target_node, nodes_ip):
585     """Convert the logical ID to the physical ID.
586
587     This is used only for drbd, which needs ip/port configuration.
588
589     The routine descends down and updates its children also, because
590     this helps when the only the top device is passed to the remote
591     node.
592
593     Arguments:
594       - target_node: the node we wish to configure for
595       - nodes_ip: a mapping of node name to ip
596
597     The target_node must exist in in nodes_ip, and must be one of the
598     nodes in the logical ID for each of the DRBD devices encountered
599     in the disk tree.
600
601     """
602     if self.children:
603       for child in self.children:
604         child.SetPhysicalID(target_node, nodes_ip)
605
606     if self.logical_id is None and self.physical_id is not None:
607       return
608     if self.dev_type in constants.LDS_DRBD:
609       pnode, snode, port, pminor, sminor, secret = self.logical_id
610       if target_node not in (pnode, snode):
611         raise errors.ConfigurationError("DRBD device not knowing node %s" %
612                                         target_node)
613       pnode_ip = nodes_ip.get(pnode, None)
614       snode_ip = nodes_ip.get(snode, None)
615       if pnode_ip is None or snode_ip is None:
616         raise errors.ConfigurationError("Can't find primary or secondary node"
617                                         " for %s" % str(self))
618       p_data = (pnode_ip, port)
619       s_data = (snode_ip, port)
620       if pnode == target_node:
621         self.physical_id = p_data + s_data + (pminor, secret)
622       else: # it must be secondary, we tested above
623         self.physical_id = s_data + p_data + (sminor, secret)
624     else:
625       self.physical_id = self.logical_id
626     return
627
628   def ToDict(self):
629     """Disk-specific conversion to standard python types.
630
631     This replaces the children lists of objects with lists of
632     standard python types.
633
634     """
635     bo = super(Disk, self).ToDict()
636
637     for attr in ("children",):
638       alist = bo.get(attr, None)
639       if alist:
640         bo[attr] = self._ContainerToDicts(alist)
641     return bo
642
643   @classmethod
644   def FromDict(cls, val):
645     """Custom function for Disks
646
647     """
648     obj = super(Disk, cls).FromDict(val)
649     if obj.children:
650       obj.children = cls._ContainerFromDicts(obj.children, list, Disk)
651     if obj.logical_id and isinstance(obj.logical_id, list):
652       obj.logical_id = tuple(obj.logical_id)
653     if obj.physical_id and isinstance(obj.physical_id, list):
654       obj.physical_id = tuple(obj.physical_id)
655     if obj.dev_type in constants.LDS_DRBD:
656       # we need a tuple of length six here
657       if len(obj.logical_id) < 6:
658         obj.logical_id += (None,) * (6 - len(obj.logical_id))
659     return obj
660
661   def __str__(self):
662     """Custom str() formatter for disks.
663
664     """
665     if self.dev_type == constants.LD_LV:
666       val = "<LogicalVolume(/dev/%s/%s" % self.logical_id
667     elif self.dev_type in constants.LDS_DRBD:
668       node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
669       val = "<DRBD8("
670       if self.physical_id is None:
671         phy = "unconfigured"
672       else:
673         phy = ("configured as %s:%s %s:%s" %
674                (self.physical_id[0], self.physical_id[1],
675                 self.physical_id[2], self.physical_id[3]))
676
677       val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
678               (node_a, minor_a, node_b, minor_b, port, phy))
679       if self.children and self.children.count(None) == 0:
680         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
681       else:
682         val += "no local storage"
683     else:
684       val = ("<Disk(type=%s, logical_id=%s, physical_id=%s, children=%s" %
685              (self.dev_type, self.logical_id, self.physical_id, self.children))
686     if self.iv_name is None:
687       val += ", not visible"
688     else:
689       val += ", visible as /dev/%s" % self.iv_name
690     if isinstance(self.size, int):
691       val += ", size=%dm)>" % self.size
692     else:
693       val += ", size='%s')>" % (self.size,)
694     return val
695
696   def Verify(self):
697     """Checks that this disk is correctly configured.
698
699     """
700     all_errors = []
701     if self.mode not in constants.DISK_ACCESS_SET:
702       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
703     return all_errors
704
705   def UpgradeConfig(self):
706     """Fill defaults for missing configuration values.
707
708     """
709     if self.children:
710       for child in self.children:
711         child.UpgradeConfig()
712     # add here config upgrade for this disk
713
714
715 class Instance(TaggableObject):
716   """Config object representing an instance."""
717   __slots__ = [
718     "name",
719     "primary_node",
720     "os",
721     "hypervisor",
722     "hvparams",
723     "beparams",
724     "osparams",
725     "admin_up",
726     "nics",
727     "disks",
728     "disk_template",
729     "network_port",
730     "serial_no",
731     ] + _TIMESTAMPS + _UUID
732
733   def _ComputeSecondaryNodes(self):
734     """Compute the list of secondary nodes.
735
736     This is a simple wrapper over _ComputeAllNodes.
737
738     """
739     all_nodes = set(self._ComputeAllNodes())
740     all_nodes.discard(self.primary_node)
741     return tuple(all_nodes)
742
743   secondary_nodes = property(_ComputeSecondaryNodes, None, None,
744                              "List of secondary nodes")
745
746   def _ComputeAllNodes(self):
747     """Compute the list of all nodes.
748
749     Since the data is already there (in the drbd disks), keeping it as
750     a separate normal attribute is redundant and if not properly
751     synchronised can cause problems. Thus it's better to compute it
752     dynamically.
753
754     """
755     def _Helper(nodes, device):
756       """Recursively computes nodes given a top device."""
757       if device.dev_type in constants.LDS_DRBD:
758         nodea, nodeb = device.logical_id[:2]
759         nodes.add(nodea)
760         nodes.add(nodeb)
761       if device.children:
762         for child in device.children:
763           _Helper(nodes, child)
764
765     all_nodes = set()
766     all_nodes.add(self.primary_node)
767     for device in self.disks:
768       _Helper(all_nodes, device)
769     return tuple(all_nodes)
770
771   all_nodes = property(_ComputeAllNodes, None, None,
772                        "List of all nodes of the instance")
773
774   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
775     """Provide a mapping of nodes to LVs this instance owns.
776
777     This function figures out what logical volumes should belong on
778     which nodes, recursing through a device tree.
779
780     @param lvmap: optional dictionary to receive the
781         'node' : ['lv', ...] data.
782
783     @return: None if lvmap arg is given, otherwise, a dictionary of
784         the form { 'nodename' : ['volume1', 'volume2', ...], ... };
785         volumeN is of the form "vg_name/lv_name", compatible with
786         GetVolumeList()
787
788     """
789     if node == None:
790       node = self.primary_node
791
792     if lvmap is None:
793       lvmap = {
794         node: [],
795         }
796       ret = lvmap
797     else:
798       if not node in lvmap:
799         lvmap[node] = []
800       ret = None
801
802     if not devs:
803       devs = self.disks
804
805     for dev in devs:
806       if dev.dev_type == constants.LD_LV:
807         lvmap[node].append(dev.logical_id[0] + "/" + dev.logical_id[1])
808
809       elif dev.dev_type in constants.LDS_DRBD:
810         if dev.children:
811           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
812           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
813
814       elif dev.children:
815         self.MapLVsByNode(lvmap, dev.children, node)
816
817     return ret
818
819   def FindDisk(self, idx):
820     """Find a disk given having a specified index.
821
822     This is just a wrapper that does validation of the index.
823
824     @type idx: int
825     @param idx: the disk index
826     @rtype: L{Disk}
827     @return: the corresponding disk
828     @raise errors.OpPrereqError: when the given index is not valid
829
830     """
831     try:
832       idx = int(idx)
833       return self.disks[idx]
834     except (TypeError, ValueError), err:
835       raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
836                                  errors.ECODE_INVAL)
837     except IndexError:
838       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
839                                  " 0 to %d" % (idx, len(self.disks) - 1),
840                                  errors.ECODE_INVAL)
841
842   def ToDict(self):
843     """Instance-specific conversion to standard python types.
844
845     This replaces the children lists of objects with lists of standard
846     python types.
847
848     """
849     bo = super(Instance, self).ToDict()
850
851     for attr in "nics", "disks":
852       alist = bo.get(attr, None)
853       if alist:
854         nlist = self._ContainerToDicts(alist)
855       else:
856         nlist = []
857       bo[attr] = nlist
858     return bo
859
860   @classmethod
861   def FromDict(cls, val):
862     """Custom function for instances.
863
864     """
865     obj = super(Instance, cls).FromDict(val)
866     obj.nics = cls._ContainerFromDicts(obj.nics, list, NIC)
867     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
868     return obj
869
870   def UpgradeConfig(self):
871     """Fill defaults for missing configuration values.
872
873     """
874     for nic in self.nics:
875       nic.UpgradeConfig()
876     for disk in self.disks:
877       disk.UpgradeConfig()
878     if self.hvparams:
879       for key in constants.HVC_GLOBALS:
880         try:
881           del self.hvparams[key]
882         except KeyError:
883           pass
884     if self.osparams is None:
885       self.osparams = {}
886
887
888 class OS(ConfigObject):
889   """Config object representing an operating system.
890
891   @type supported_parameters: list
892   @ivar supported_parameters: a list of tuples, name and description,
893       containing the supported parameters by this OS
894
895   @type VARIANT_DELIM: string
896   @cvar VARIANT_DELIM: the variant delimiter
897
898   """
899   __slots__ = [
900     "name",
901     "path",
902     "api_versions",
903     "create_script",
904     "export_script",
905     "import_script",
906     "rename_script",
907     "verify_script",
908     "supported_variants",
909     "supported_parameters",
910     ]
911
912   VARIANT_DELIM = "+"
913
914   @classmethod
915   def SplitNameVariant(cls, name):
916     """Splits the name into the proper name and variant.
917
918     @param name: the OS (unprocessed) name
919     @rtype: list
920     @return: a list of two elements; if the original name didn't
921         contain a variant, it's returned as an empty string
922
923     """
924     nv = name.split(cls.VARIANT_DELIM, 1)
925     if len(nv) == 1:
926       nv.append("")
927     return nv
928
929   @classmethod
930   def GetName(cls, name):
931     """Returns the proper name of the os (without the variant).
932
933     @param name: the OS (unprocessed) name
934
935     """
936     return cls.SplitNameVariant(name)[0]
937
938   @classmethod
939   def GetVariant(cls, name):
940     """Returns the variant the os (without the base name).
941
942     @param name: the OS (unprocessed) name
943
944     """
945     return cls.SplitNameVariant(name)[1]
946
947
948 class Node(TaggableObject):
949   """Config object representing a node."""
950   __slots__ = [
951     "name",
952     "primary_ip",
953     "secondary_ip",
954     "serial_no",
955     "master_candidate",
956     "offline",
957     "drained",
958     "group",
959     "master_capable",
960     "vm_capable",
961     "ndparams",
962     "powered",
963     ] + _TIMESTAMPS + _UUID
964
965   def UpgradeConfig(self):
966     """Fill defaults for missing configuration values.
967
968     """
969     # pylint: disable-msg=E0203
970     # because these are "defined" via slots, not manually
971     if self.master_capable is None:
972       self.master_capable = True
973
974     if self.vm_capable is None:
975       self.vm_capable = True
976
977     if self.ndparams is None:
978       self.ndparams = {}
979
980     if self.powered is None:
981       self.powered = True
982
983
984 class NodeGroup(TaggableObject):
985   """Config object representing a node group."""
986   __slots__ = [
987     "name",
988     "members",
989     "ndparams",
990     "serial_no",
991     "alloc_policy",
992     ] + _TIMESTAMPS + _UUID
993
994   def ToDict(self):
995     """Custom function for nodegroup.
996
997     This discards the members object, which gets recalculated and is only kept
998     in memory.
999
1000     """
1001     mydict = super(NodeGroup, self).ToDict()
1002     del mydict["members"]
1003     return mydict
1004
1005   @classmethod
1006   def FromDict(cls, val):
1007     """Custom function for nodegroup.
1008
1009     The members slot is initialized to an empty list, upon deserialization.
1010
1011     """
1012     obj = super(NodeGroup, cls).FromDict(val)
1013     obj.members = []
1014     return obj
1015
1016   def UpgradeConfig(self):
1017     """Fill defaults for missing configuration values.
1018
1019     """
1020     if self.ndparams is None:
1021       self.ndparams = {}
1022
1023     if self.serial_no is None:
1024       self.serial_no = 1
1025
1026     if self.alloc_policy is None:
1027       self.alloc_policy = constants.ALLOC_POLICY_PREFERRED
1028
1029     # We only update mtime, and not ctime, since we would not be able to provide
1030     # a correct value for creation time.
1031     if self.mtime is None:
1032       self.mtime = time.time()
1033
1034   def FillND(self, node):
1035     """Return filled out ndparams for L{objects.Node}
1036
1037     @type node: L{objects.Node}
1038     @param node: A Node object to fill
1039     @return a copy of the node's ndparams with defaults filled
1040
1041     """
1042     return self.SimpleFillND(node.ndparams)
1043
1044   def SimpleFillND(self, ndparams):
1045     """Fill a given ndparams dict with defaults.
1046
1047     @type ndparams: dict
1048     @param ndparams: the dict to fill
1049     @rtype: dict
1050     @return: a copy of the passed in ndparams with missing keys filled
1051         from the node group defaults
1052
1053     """
1054     return FillDict(self.ndparams, ndparams)
1055
1056
1057 class Cluster(TaggableObject):
1058   """Config object representing the cluster."""
1059   __slots__ = [
1060     "serial_no",
1061     "rsahostkeypub",
1062     "highest_used_port",
1063     "tcpudp_port_pool",
1064     "mac_prefix",
1065     "volume_group_name",
1066     "reserved_lvs",
1067     "drbd_usermode_helper",
1068     "default_bridge",
1069     "default_hypervisor",
1070     "master_node",
1071     "master_ip",
1072     "master_netdev",
1073     "cluster_name",
1074     "file_storage_dir",
1075     "shared_file_storage_dir",
1076     "enabled_hypervisors",
1077     "hvparams",
1078     "os_hvp",
1079     "beparams",
1080     "osparams",
1081     "nicparams",
1082     "ndparams",
1083     "candidate_pool_size",
1084     "modify_etc_hosts",
1085     "modify_ssh_setup",
1086     "maintain_node_health",
1087     "uid_pool",
1088     "default_iallocator",
1089     "hidden_os",
1090     "blacklisted_os",
1091     "primary_ip_family",
1092     "prealloc_wipe_disks",
1093     ] + _TIMESTAMPS + _UUID
1094
1095   def UpgradeConfig(self):
1096     """Fill defaults for missing configuration values.
1097
1098     """
1099     # pylint: disable-msg=E0203
1100     # because these are "defined" via slots, not manually
1101     if self.hvparams is None:
1102       self.hvparams = constants.HVC_DEFAULTS
1103     else:
1104       for hypervisor in self.hvparams:
1105         self.hvparams[hypervisor] = FillDict(
1106             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
1107
1108     if self.os_hvp is None:
1109       self.os_hvp = {}
1110
1111     # osparams added before 2.2
1112     if self.osparams is None:
1113       self.osparams = {}
1114
1115     if self.ndparams is None:
1116       self.ndparams = constants.NDC_DEFAULTS
1117
1118     self.beparams = UpgradeGroupedParams(self.beparams,
1119                                          constants.BEC_DEFAULTS)
1120     migrate_default_bridge = not self.nicparams
1121     self.nicparams = UpgradeGroupedParams(self.nicparams,
1122                                           constants.NICC_DEFAULTS)
1123     if migrate_default_bridge:
1124       self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
1125         self.default_bridge
1126
1127     if self.modify_etc_hosts is None:
1128       self.modify_etc_hosts = True
1129
1130     if self.modify_ssh_setup is None:
1131       self.modify_ssh_setup = True
1132
1133     # default_bridge is no longer used in 2.1. The slot is left there to
1134     # support auto-upgrading. It can be removed once we decide to deprecate
1135     # upgrading straight from 2.0.
1136     if self.default_bridge is not None:
1137       self.default_bridge = None
1138
1139     # default_hypervisor is just the first enabled one in 2.1. This slot and
1140     # code can be removed once upgrading straight from 2.0 is deprecated.
1141     if self.default_hypervisor is not None:
1142       self.enabled_hypervisors = ([self.default_hypervisor] +
1143         [hvname for hvname in self.enabled_hypervisors
1144          if hvname != self.default_hypervisor])
1145       self.default_hypervisor = None
1146
1147     # maintain_node_health added after 2.1.1
1148     if self.maintain_node_health is None:
1149       self.maintain_node_health = False
1150
1151     if self.uid_pool is None:
1152       self.uid_pool = []
1153
1154     if self.default_iallocator is None:
1155       self.default_iallocator = ""
1156
1157     # reserved_lvs added before 2.2
1158     if self.reserved_lvs is None:
1159       self.reserved_lvs = []
1160
1161     # hidden and blacklisted operating systems added before 2.2.1
1162     if self.hidden_os is None:
1163       self.hidden_os = []
1164
1165     if self.blacklisted_os is None:
1166       self.blacklisted_os = []
1167
1168     # primary_ip_family added before 2.3
1169     if self.primary_ip_family is None:
1170       self.primary_ip_family = AF_INET
1171
1172     if self.prealloc_wipe_disks is None:
1173       self.prealloc_wipe_disks = False
1174
1175     # shared_file_storage_dir added before 2.5
1176     if self.shared_file_storage_dir is None:
1177       self.shared_file_storage_dir = ""
1178
1179   def ToDict(self):
1180     """Custom function for cluster.
1181
1182     """
1183     mydict = super(Cluster, self).ToDict()
1184     mydict["tcpudp_port_pool"] = list(self.tcpudp_port_pool)
1185     return mydict
1186
1187   @classmethod
1188   def FromDict(cls, val):
1189     """Custom function for cluster.
1190
1191     """
1192     obj = super(Cluster, cls).FromDict(val)
1193     if not isinstance(obj.tcpudp_port_pool, set):
1194       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
1195     return obj
1196
1197   def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
1198     """Get the default hypervisor parameters for the cluster.
1199
1200     @param hypervisor: the hypervisor name
1201     @param os_name: if specified, we'll also update the defaults for this OS
1202     @param skip_keys: if passed, list of keys not to use
1203     @return: the defaults dict
1204
1205     """
1206     if skip_keys is None:
1207       skip_keys = []
1208
1209     fill_stack = [self.hvparams.get(hypervisor, {})]
1210     if os_name is not None:
1211       os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
1212       fill_stack.append(os_hvp)
1213
1214     ret_dict = {}
1215     for o_dict in fill_stack:
1216       ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
1217
1218     return ret_dict
1219
1220   def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
1221     """Fill a given hvparams dict with cluster defaults.
1222
1223     @type hv_name: string
1224     @param hv_name: the hypervisor to use
1225     @type os_name: string
1226     @param os_name: the OS to use for overriding the hypervisor defaults
1227     @type skip_globals: boolean
1228     @param skip_globals: if True, the global hypervisor parameters will
1229         not be filled
1230     @rtype: dict
1231     @return: a copy of the given hvparams with missing keys filled from
1232         the cluster defaults
1233
1234     """
1235     if skip_globals:
1236       skip_keys = constants.HVC_GLOBALS
1237     else:
1238       skip_keys = []
1239
1240     def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
1241     return FillDict(def_dict, hvparams, skip_keys=skip_keys)
1242
1243   def FillHV(self, instance, skip_globals=False):
1244     """Fill an instance's hvparams dict with cluster defaults.
1245
1246     @type instance: L{objects.Instance}
1247     @param instance: the instance parameter to fill
1248     @type skip_globals: boolean
1249     @param skip_globals: if True, the global hypervisor parameters will
1250         not be filled
1251     @rtype: dict
1252     @return: a copy of the instance's hvparams with missing keys filled from
1253         the cluster defaults
1254
1255     """
1256     return self.SimpleFillHV(instance.hypervisor, instance.os,
1257                              instance.hvparams, skip_globals)
1258
1259   def SimpleFillBE(self, beparams):
1260     """Fill a given beparams dict with cluster defaults.
1261
1262     @type beparams: dict
1263     @param beparams: the dict to fill
1264     @rtype: dict
1265     @return: a copy of the passed in beparams with missing keys filled
1266         from the cluster defaults
1267
1268     """
1269     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
1270
1271   def FillBE(self, instance):
1272     """Fill an instance's beparams dict with cluster defaults.
1273
1274     @type instance: L{objects.Instance}
1275     @param instance: the instance parameter to fill
1276     @rtype: dict
1277     @return: a copy of the instance's beparams with missing keys filled from
1278         the cluster defaults
1279
1280     """
1281     return self.SimpleFillBE(instance.beparams)
1282
1283   def SimpleFillNIC(self, nicparams):
1284     """Fill a given nicparams dict with cluster defaults.
1285
1286     @type nicparams: dict
1287     @param nicparams: the dict to fill
1288     @rtype: dict
1289     @return: a copy of the passed in nicparams with missing keys filled
1290         from the cluster defaults
1291
1292     """
1293     return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
1294
1295   def SimpleFillOS(self, os_name, os_params):
1296     """Fill an instance's osparams dict with cluster defaults.
1297
1298     @type os_name: string
1299     @param os_name: the OS name to use
1300     @type os_params: dict
1301     @param os_params: the dict to fill with default values
1302     @rtype: dict
1303     @return: a copy of the instance's osparams with missing keys filled from
1304         the cluster defaults
1305
1306     """
1307     name_only = os_name.split("+", 1)[0]
1308     # base OS
1309     result = self.osparams.get(name_only, {})
1310     # OS with variant
1311     result = FillDict(result, self.osparams.get(os_name, {}))
1312     # specified params
1313     return FillDict(result, os_params)
1314
1315   def FillND(self, node, nodegroup):
1316     """Return filled out ndparams for L{objects.NodeGroup} and L{objects.Node}
1317
1318     @type node: L{objects.Node}
1319     @param node: A Node object to fill
1320     @type nodegroup: L{objects.NodeGroup}
1321     @param nodegroup: A Node object to fill
1322     @return a copy of the node's ndparams with defaults filled
1323
1324     """
1325     return self.SimpleFillND(nodegroup.FillND(node))
1326
1327   def SimpleFillND(self, ndparams):
1328     """Fill a given ndparams dict with defaults.
1329
1330     @type ndparams: dict
1331     @param ndparams: the dict to fill
1332     @rtype: dict
1333     @return: a copy of the passed in ndparams with missing keys filled
1334         from the cluster defaults
1335
1336     """
1337     return FillDict(self.ndparams, ndparams)
1338
1339
1340 class BlockDevStatus(ConfigObject):
1341   """Config object representing the status of a block device."""
1342   __slots__ = [
1343     "dev_path",
1344     "major",
1345     "minor",
1346     "sync_percent",
1347     "estimated_time",
1348     "is_degraded",
1349     "ldisk_status",
1350     ]
1351
1352
1353 class ImportExportStatus(ConfigObject):
1354   """Config object representing the status of an import or export."""
1355   __slots__ = [
1356     "recent_output",
1357     "listen_port",
1358     "connected",
1359     "progress_mbytes",
1360     "progress_throughput",
1361     "progress_eta",
1362     "progress_percent",
1363     "exit_status",
1364     "error_message",
1365     ] + _TIMESTAMPS
1366
1367
1368 class ImportExportOptions(ConfigObject):
1369   """Options for import/export daemon
1370
1371   @ivar key_name: X509 key name (None for cluster certificate)
1372   @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
1373   @ivar compress: Compression method (one of L{constants.IEC_ALL})
1374   @ivar magic: Used to ensure the connection goes to the right disk
1375   @ivar ipv6: Whether to use IPv6
1376   @ivar connect_timeout: Number of seconds for establishing connection
1377
1378   """
1379   __slots__ = [
1380     "key_name",
1381     "ca_pem",
1382     "compress",
1383     "magic",
1384     "ipv6",
1385     "connect_timeout",
1386     ]
1387
1388
1389 class ConfdRequest(ConfigObject):
1390   """Object holding a confd request.
1391
1392   @ivar protocol: confd protocol version
1393   @ivar type: confd query type
1394   @ivar query: query request
1395   @ivar rsalt: requested reply salt
1396
1397   """
1398   __slots__ = [
1399     "protocol",
1400     "type",
1401     "query",
1402     "rsalt",
1403     ]
1404
1405
1406 class ConfdReply(ConfigObject):
1407   """Object holding a confd reply.
1408
1409   @ivar protocol: confd protocol version
1410   @ivar status: reply status code (ok, error)
1411   @ivar answer: confd query reply
1412   @ivar serial: configuration serial number
1413
1414   """
1415   __slots__ = [
1416     "protocol",
1417     "status",
1418     "answer",
1419     "serial",
1420     ]
1421
1422
1423 class QueryFieldDefinition(ConfigObject):
1424   """Object holding a query field definition.
1425
1426   @ivar name: Field name
1427   @ivar title: Human-readable title
1428   @ivar kind: Field type
1429   @ivar doc: Human-readable description
1430
1431   """
1432   __slots__ = [
1433     "name",
1434     "title",
1435     "kind",
1436     "doc",
1437     ]
1438
1439
1440 class _QueryResponseBase(ConfigObject):
1441   __slots__ = [
1442     "fields",
1443     ]
1444
1445   def ToDict(self):
1446     """Custom function for serializing.
1447
1448     """
1449     mydict = super(_QueryResponseBase, self).ToDict()
1450     mydict["fields"] = self._ContainerToDicts(mydict["fields"])
1451     return mydict
1452
1453   @classmethod
1454   def FromDict(cls, val):
1455     """Custom function for de-serializing.
1456
1457     """
1458     obj = super(_QueryResponseBase, cls).FromDict(val)
1459     obj.fields = cls._ContainerFromDicts(obj.fields, list, QueryFieldDefinition)
1460     return obj
1461
1462
1463 class QueryRequest(ConfigObject):
1464   """Object holding a query request.
1465
1466   """
1467   __slots__ = [
1468     "what",
1469     "fields",
1470     "filter",
1471     ]
1472
1473
1474 class QueryResponse(_QueryResponseBase):
1475   """Object holding the response to a query.
1476
1477   @ivar fields: List of L{QueryFieldDefinition} objects
1478   @ivar data: Requested data
1479
1480   """
1481   __slots__ = [
1482     "data",
1483     ]
1484
1485
1486 class QueryFieldsRequest(ConfigObject):
1487   """Object holding a request for querying available fields.
1488
1489   """
1490   __slots__ = [
1491     "what",
1492     "fields",
1493     ]
1494
1495
1496 class QueryFieldsResponse(_QueryResponseBase):
1497   """Object holding the response to a query for fields.
1498
1499   @ivar fields: List of L{QueryFieldDefinition} objects
1500
1501   """
1502   __slots__ = [
1503     ]
1504
1505
1506 class InstanceConsole(ConfigObject):
1507   """Object describing how to access the console of an instance.
1508
1509   """
1510   __slots__ = [
1511     "instance",
1512     "kind",
1513     "message",
1514     "host",
1515     "port",
1516     "user",
1517     "command",
1518     "display",
1519     ]
1520
1521   def Validate(self):
1522     """Validates contents of this object.
1523
1524     """
1525     assert self.kind in constants.CONS_ALL, "Unknown console type"
1526     assert self.instance, "Missing instance name"
1527     assert self.message or self.kind in [constants.CONS_SSH, constants.CONS_VNC]
1528     assert self.host or self.kind == constants.CONS_MESSAGE
1529     assert self.port or self.kind in [constants.CONS_MESSAGE,
1530                                       constants.CONS_SSH]
1531     assert self.user or self.kind in [constants.CONS_MESSAGE,
1532                                       constants.CONS_VNC]
1533     assert self.command or self.kind in [constants.CONS_MESSAGE,
1534                                          constants.CONS_VNC]
1535     assert self.display or self.kind in [constants.CONS_MESSAGE,
1536                                          constants.CONS_SSH]
1537     return True
1538
1539
1540 class SerializableConfigParser(ConfigParser.SafeConfigParser):
1541   """Simple wrapper over ConfigParse that allows serialization.
1542
1543   This class is basically ConfigParser.SafeConfigParser with two
1544   additional methods that allow it to serialize/unserialize to/from a
1545   buffer.
1546
1547   """
1548   def Dumps(self):
1549     """Dump this instance and return the string representation."""
1550     buf = StringIO()
1551     self.write(buf)
1552     return buf.getvalue()
1553
1554   @classmethod
1555   def Loads(cls, data):
1556     """Load data from a string."""
1557     buf = StringIO(data)
1558     cfp = cls()
1559     cfp.readfp(buf)
1560     return cfp