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