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