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