RAPI: fix evacuate node resource
[ganeti-local] / lib / objects.py
index 6524d13..5d1b78f 100644 (file)
@@ -1,7 +1,7 @@
 #
 #
 
 #
 #
 
-# Copyright (C) 2006, 2007 Google Inc.
+# Copyright (C) 2006, 2007, 2008, 2009, 2010 Google Inc.
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License as published by
@@ -26,6 +26,12 @@ pass to and from external parties.
 
 """
 
 
 """
 
+# pylint: disable-msg=E0203,W0201
+
+# E0203: Access to member %r before its definition, since we use
+# objects.py which doesn't explicitely initialise its members
+
+# W0201: Attribute '%s' defined outside __init__
 
 import ConfigParser
 import re
 
 import ConfigParser
 import re
@@ -35,24 +41,37 @@ from cStringIO import StringIO
 from ganeti import errors
 from ganeti import constants
 
 from ganeti import errors
 from ganeti import constants
 
+from socket import AF_INET
+
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
-           "OS", "Node", "Cluster", "FillDict"]
+           "OS", "Node", "NodeGroup", "Cluster", "FillDict"]
 
 
+_TIMESTAMPS = ["ctime", "mtime"]
+_UUID = ["uuid"]
 
 
-def FillDict(defaults_dict, custom_dict):
+
+def FillDict(defaults_dict, custom_dict, skip_keys=None):
   """Basic function to apply settings on top a default dict.
 
   @type defaults_dict: dict
   @param defaults_dict: dictionary holding the default values
   @type custom_dict: dict
   @param custom_dict: dictionary holding customized value
   """Basic function to apply settings on top a default dict.
 
   @type defaults_dict: dict
   @param defaults_dict: dictionary holding the default values
   @type custom_dict: dict
   @param custom_dict: dictionary holding customized value
+  @type skip_keys: list
+  @param skip_keys: which keys not to fill
   @rtype: dict
   @return: dict with the 'full' values
 
   """
   ret_dict = copy.deepcopy(defaults_dict)
   ret_dict.update(custom_dict)
   @rtype: dict
   @return: dict with the 'full' values
 
   """
   ret_dict = copy.deepcopy(defaults_dict)
   ret_dict.update(custom_dict)
+  if skip_keys:
+    for k in skip_keys:
+      try:
+        del ret_dict[k]
+      except KeyError:
+        pass
   return ret_dict
 
 
   return ret_dict
 
 
@@ -91,24 +110,29 @@ class ConfigObject(object):
   def __init__(self, **kwargs):
     for k, v in kwargs.iteritems():
       setattr(self, k, v)
   def __init__(self, **kwargs):
     for k, v in kwargs.iteritems():
       setattr(self, k, v)
-    self.UpgradeConfig()
 
   def __getattr__(self, name):
 
   def __getattr__(self, name):
-    if name not in self.__slots__:
+    if name not in self._all_slots():
       raise AttributeError("Invalid object attribute %s.%s" %
                            (type(self).__name__, name))
     return None
 
       raise AttributeError("Invalid object attribute %s.%s" %
                            (type(self).__name__, name))
     return None
 
-  def __setitem__(self, key, value):
-    if key not in self.__slots__:
-      raise KeyError(key)
-    setattr(self, key, value)
-
   def __setstate__(self, state):
   def __setstate__(self, state):
+    slots = self._all_slots()
     for name in state:
     for name in state:
-      if name in self.__slots__:
+      if name in slots:
         setattr(self, name, state[name])
 
         setattr(self, name, state[name])
 
+  @classmethod
+  def _all_slots(cls):
+    """Compute the list of all declared slots for a class.
+
+    """
+    slots = []
+    for parent in cls.__mro__:
+      slots.extend(getattr(parent, "__slots__", []))
+    return slots
+
   def ToDict(self):
     """Convert to a dict holding only standard python types.
 
   def ToDict(self):
     """Convert to a dict holding only standard python types.
 
@@ -120,7 +144,7 @@ class ConfigObject(object):
 
     """
     result = {}
 
     """
     result = {}
-    for name in self.__slots__:
+    for name in self._all_slots():
       value = getattr(self, name, None)
       if value is not None:
         result[name] = value
       value = getattr(self, name, None)
       if value is not None:
         result[name] = value
@@ -145,7 +169,7 @@ class ConfigObject(object):
       raise errors.ConfigurationError("Invalid object passed to FromDict:"
                                       " expected dict, got %s" % type(val))
     val_str = dict([(str(k), v) for k, v in val.iteritems()])
       raise errors.ConfigurationError("Invalid object passed to FromDict:"
                                       " expected dict, got %s" % type(val))
     val_str = dict([(str(k), v) for k, v in val.iteritems()])
-    obj = cls(**val_str)
+    obj = cls(**val_str) # pylint: disable-msg=W0142
     return obj
 
   @staticmethod
     return obj
 
   @staticmethod
@@ -178,6 +202,8 @@ class ConfigObject(object):
     if not isinstance(c_type, type):
       raise TypeError("Container type %s passed to _ContainerFromDicts is"
                       " not a type" % type(c_type))
     if not isinstance(c_type, type):
       raise TypeError("Container type %s passed to _ContainerFromDicts is"
                       " not a type" % type(c_type))
+    if source is None:
+      source = c_type()
     if c_type is dict:
       ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
     elif c_type in (list, tuple, set, frozenset):
     if c_type is dict:
       ret = dict([(k, e_type.FromDict(v)) for k, v in source.iteritems()])
     elif c_type in (list, tuple, set, frozenset):
@@ -187,6 +213,14 @@ class ConfigObject(object):
                       " _ContainerFromDicts" % c_type)
     return ret
 
                       " _ContainerFromDicts" % c_type)
     return ret
 
+  def Copy(self):
+    """Makes a deep copy of the current object and its children.
+
+    """
+    dict_form = self.ToDict()
+    clone_obj = self.__class__.FromDict(dict_form)
+    return clone_obj
+
   def __repr__(self):
     """Implement __repr__ for ConfigObjects."""
     return repr(self.ToDict())
   def __repr__(self):
     """Implement __repr__ for ConfigObjects."""
     return repr(self.ToDict())
@@ -194,8 +228,8 @@ class ConfigObject(object):
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
-    This method will be called at object init time, and its implementation will
-    be object dependent.
+    This method will be called at configuration load time, and its
+    implementation will be object dependent.
 
     """
     pass
 
     """
     pass
@@ -205,10 +239,11 @@ class TaggableObject(ConfigObject):
   """An generic class supporting tags.
 
   """
   """An generic class supporting tags.
 
   """
-  __slots__ = ConfigObject.__slots__ + ["tags"]
+  __slots__ = ["tags"]
+  VALID_TAG_RE = re.compile("^[\w.+*/:@-]+$")
 
 
-  @staticmethod
-  def ValidateTag(tag):
+  @classmethod
+  def ValidateTag(cls, tag):
     """Check if a tag is valid.
 
     If the tag is invalid, an errors.TagError will be raised. The
     """Check if a tag is valid.
 
     If the tag is invalid, an errors.TagError will be raised. The
@@ -222,7 +257,7 @@ class TaggableObject(ConfigObject):
                             constants.MAX_TAG_LEN)
     if not tag:
       raise errors.TagError("Tags cannot be empty")
                             constants.MAX_TAG_LEN)
     if not tag:
       raise errors.TagError("Tags cannot be empty")
-    if not re.match("^[\w.+*/:-]+$", tag):
+    if not cls.VALID_TAG_RE.match(tag):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
@@ -281,7 +316,14 @@ class TaggableObject(ConfigObject):
 
 class ConfigData(ConfigObject):
   """Top-level config object."""
 
 class ConfigData(ConfigObject):
   """Top-level config object."""
-  __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
+  __slots__ = [
+    "version",
+    "cluster",
+    "nodes",
+    "nodegroups",
+    "instances",
+    "serial_no",
+    ] + _TIMESTAMPS
 
   def ToDict(self):
     """Custom function for top-level config data.
 
   def ToDict(self):
     """Custom function for top-level config data.
@@ -292,7 +334,7 @@ class ConfigData(ConfigObject):
     """
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
     """
     mydict = super(ConfigData, self).ToDict()
     mydict["cluster"] = mydict["cluster"].ToDict()
-    for key in "nodes", "instances":
+    for key in "nodes", "instances", "nodegroups":
       mydict[key] = self._ContainerToDicts(mydict[key])
 
     return mydict
       mydict[key] = self._ContainerToDicts(mydict[key])
 
     return mydict
@@ -306,8 +348,44 @@ class ConfigData(ConfigObject):
     obj.cluster = Cluster.FromDict(obj.cluster)
     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
     obj.cluster = Cluster.FromDict(obj.cluster)
     obj.nodes = cls._ContainerFromDicts(obj.nodes, dict, Node)
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
+    obj.nodegroups = cls._ContainerFromDicts(obj.nodegroups, dict, NodeGroup)
     return obj
 
     return obj
 
+  def HasAnyDiskOfType(self, dev_type):
+    """Check if in there is at disk of the given type in the configuration.
+
+    @type dev_type: L{constants.LDS_BLOCK}
+    @param dev_type: the type to look for
+    @rtype: boolean
+    @return: boolean indicating if a disk of the given type was found or not
+
+    """
+    for instance in self.instances.values():
+      for disk in instance.disks:
+        if disk.IsBasedOnDiskType(dev_type):
+          return True
+    return False
+
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    self.cluster.UpgradeConfig()
+    for node in self.nodes.values():
+      node.UpgradeConfig()
+    for instance in self.instances.values():
+      instance.UpgradeConfig()
+    if self.nodegroups is None:
+      self.nodegroups = {}
+    for nodegroup in self.nodegroups.values():
+      nodegroup.UpgradeConfig()
+    if self.cluster.drbd_usermode_helper is None:
+      # To decide if we set an helper let's check if at least one instance has
+      # a DRBD disk. This does not cover all the possible scenarios but it
+      # gives a good approximation.
+      if self.HasAnyDiskOfType(constants.LD_DRBD8):
+        self.cluster.drbd_usermode_helper = constants.DEFAULT_DRBD_HELPER
+
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
@@ -326,7 +404,7 @@ class NIC(ConfigObject):
       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
       raise errors.ConfigurationError(err)
 
       err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
       raise errors.ConfigurationError(err)
 
-    if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
+    if (nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED and
         not nicparams[constants.NIC_LINK]):
       err = "Missing bridged nic link"
       raise errors.ConfigurationError(err)
         not nicparams[constants.NIC_LINK]):
       err = "Missing bridged nic link"
       raise errors.ConfigurationError(err)
@@ -341,7 +419,8 @@ class NIC(ConfigObject):
         self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
         self.nicparams[constants.NIC_LINK] = self.bridge
     # bridge is no longer used it 2.1. The slot is left there to support
         self.nicparams[constants.NIC_MODE] = constants.NIC_MODE_BRIDGED
         self.nicparams[constants.NIC_LINK] = self.bridge
     # bridge is no longer used it 2.1. The slot is left there to support
-    # upgrading, but will be removed in 2.2
+    # upgrading, but can be removed once upgrades to the current version
+    # straight from 2.0 are deprecated.
     if self.bridge is not None:
       self.bridge = None
 
     if self.bridge is not None:
       self.bridge = None
 
@@ -370,6 +449,9 @@ class Disk(ConfigObject):
     irrespective of their status. For such devices, we return this
     path, for others we return None.
 
     irrespective of their status. For such devices, we return this
     path, for others we return None.
 
+    @warning: The path returned is not a normalized pathname; callers
+        should check that it is a valid path.
+
     """
     if self.dev_type == constants.LD_LV:
       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
     """
     if self.dev_type == constants.LD_LV:
       return "/dev/%s/%s" % (self.logical_id[0], self.logical_id[1])
@@ -391,6 +473,21 @@ class Disk(ConfigObject):
       return 0
     return -1
 
       return 0
     return -1
 
+  def IsBasedOnDiskType(self, dev_type):
+    """Check if the disk or its children are based on the given type.
+
+    @type dev_type: L{constants.LDS_BLOCK}
+    @param dev_type: the type to look for
+    @rtype: boolean
+    @return: boolean indicating if a device of the given type was found or not
+
+    """
+    if self.children:
+      for child in self.children:
+        if child.IsBasedOnDiskType(dev_type):
+          return True
+    return self.dev_type == dev_type
+
   def GetNodes(self, node):
     """This function returns the nodes this device lives on.
 
   def GetNodes(self, node):
     """This function returns the nodes this device lives on.
 
@@ -453,7 +550,7 @@ class Disk(ConfigObject):
     actual algorithms from bdev.
 
     """
     actual algorithms from bdev.
 
     """
-    if self.dev_type == constants.LD_LV:
+    if self.dev_type == constants.LD_LV or self.dev_type == constants.LD_FILE:
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
       self.size += amount
     elif self.dev_type == constants.LD_DRBD8:
       if self.children:
@@ -463,6 +560,15 @@ class Disk(ConfigObject):
       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
                                    " disk type %s" % self.dev_type)
 
       raise errors.ProgrammerError("Disk.RecordGrow called for unsupported"
                                    " disk type %s" % self.dev_type)
 
+  def UnsetSize(self):
+    """Sets recursively the size to zero for the disk and its children.
+
+    """
+    if self.children:
+      for child in self.children:
+        child.UnsetSize()
+    self.size = 0
+
   def SetPhysicalID(self, target_node, nodes_ip):
     """Convert the logical ID to the physical ID.
 
   def SetPhysicalID(self, target_node, nodes_ip):
     """Convert the logical ID to the physical ID.
 
@@ -584,23 +690,33 @@ class Disk(ConfigObject):
       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
     return all_errors
 
       all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
     return all_errors
 
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    if self.children:
+      for child in self.children:
+        child.UpgradeConfig()
+    # add here config upgrade for this disk
+
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "name",
     "primary_node",
     "os",
     "hypervisor",
     "hvparams",
     "beparams",
     "name",
     "primary_node",
     "os",
     "hypervisor",
     "hvparams",
     "beparams",
+    "osparams",
     "admin_up",
     "nics",
     "disks",
     "disk_template",
     "network_port",
     "serial_no",
     "admin_up",
     "nics",
     "disks",
     "disk_template",
     "network_port",
     "serial_no",
-    ]
+    ] + _TIMESTAMPS + _UUID
 
   def _ComputeSecondaryNodes(self):
     """Compute the list of secondary nodes.
 
   def _ComputeSecondaryNodes(self):
     """Compute the list of secondary nodes.
@@ -699,11 +815,13 @@ class Instance(TaggableObject):
     try:
       idx = int(idx)
       return self.disks[idx]
     try:
       idx = int(idx)
       return self.disks[idx]
-    except ValueError, err:
-      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
+    except (TypeError, ValueError), err:
+      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err),
+                                 errors.ECODE_INVAL)
     except IndexError:
       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
     except IndexError:
       raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
-                                 " 0 to %d" % (idx, len(self.disks)))
+                                 " 0 to %d" % (idx, len(self.disks)),
+                                 errors.ECODE_INVAL)
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
@@ -733,9 +851,35 @@ class Instance(TaggableObject):
     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
     return obj
 
     obj.disks = cls._ContainerFromDicts(obj.disks, list, Disk)
     return obj
 
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    for nic in self.nics:
+      nic.UpgradeConfig()
+    for disk in self.disks:
+      disk.UpgradeConfig()
+    if self.hvparams:
+      for key in constants.HVC_GLOBALS:
+        try:
+          del self.hvparams[key]
+        except KeyError:
+          pass
+    if self.osparams is None:
+      self.osparams = {}
+
 
 class OS(ConfigObject):
 
 class OS(ConfigObject):
-  """Config object representing an operating system."""
+  """Config object representing an operating system.
+
+  @type supported_parameters: list
+  @ivar supported_parameters: a list of tuples, name and description,
+      containing the supported parameters by this OS
+
+  @type VARIANT_DELIM: string
+  @cvar VARIANT_DELIM: the variant delimiter
+
+  """
   __slots__ = [
     "name",
     "path",
   __slots__ = [
     "name",
     "path",
@@ -744,12 +888,50 @@ class OS(ConfigObject):
     "export_script",
     "import_script",
     "rename_script",
     "export_script",
     "import_script",
     "rename_script",
+    "verify_script",
+    "supported_variants",
+    "supported_parameters",
     ]
 
     ]
 
+  VARIANT_DELIM = "+"
+
+  @classmethod
+  def SplitNameVariant(cls, name):
+    """Splits the name into the proper name and variant.
+
+    @param name: the OS (unprocessed) name
+    @rtype: list
+    @return: a list of two elements; if the original name didn't
+        contain a variant, it's returned as an empty string
+
+    """
+    nv = name.split(cls.VARIANT_DELIM, 1)
+    if len(nv) == 1:
+      nv.append("")
+    return nv
+
+  @classmethod
+  def GetName(cls, name):
+    """Returns the proper name of the os (without the variant).
+
+    @param name: the OS (unprocessed) name
+
+    """
+    return cls.SplitNameVariant(name)[0]
+
+  @classmethod
+  def GetVariant(cls, name):
+    """Returns the variant the os (without the base name).
+
+    @param name: the OS (unprocessed) name
+
+    """
+    return cls.SplitNameVariant(name)[1]
+
 
 class Node(TaggableObject):
   """Config object representing a node."""
 
 class Node(TaggableObject):
   """Config object representing a node."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "name",
     "primary_ip",
     "secondary_ip",
     "name",
     "primary_ip",
     "secondary_ip",
@@ -757,18 +939,65 @@ class Node(TaggableObject):
     "master_candidate",
     "offline",
     "drained",
     "master_candidate",
     "offline",
     "drained",
-    ]
+    "group",
+    "master_capable",
+    "vm_capable",
+    ] + _TIMESTAMPS + _UUID
+
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    # pylint: disable-msg=E0203
+    # because these are "defined" via slots, not manually
+    if self.master_capable is None:
+      self.master_capable = True
+
+    if self.vm_capable is None:
+      self.vm_capable = True
+
+
+class NodeGroup(ConfigObject):
+  """Config object representing a node group."""
+  __slots__ = [
+    "name",
+    "members",
+    ] + _TIMESTAMPS + _UUID
+
+  def ToDict(self):
+    """Custom function for nodegroup.
+
+    This discards the members object, which gets recalculated and is only kept
+    in memory.
+
+    """
+    mydict = super(NodeGroup, self).ToDict()
+    del mydict["members"]
+    return mydict
+
+  @classmethod
+  def FromDict(cls, val):
+    """Custom function for nodegroup.
+
+    The members slot is initialized to an empty list, upon deserialization.
+
+    """
+    obj = super(NodeGroup, cls).FromDict(val)
+    obj.members = []
+    return obj
 
 
 class Cluster(TaggableObject):
   """Config object representing the cluster."""
 
 
 class Cluster(TaggableObject):
   """Config object representing the cluster."""
-  __slots__ = TaggableObject.__slots__ + [
+  __slots__ = [
     "serial_no",
     "rsahostkeypub",
     "highest_used_port",
     "tcpudp_port_pool",
     "mac_prefix",
     "volume_group_name",
     "serial_no",
     "rsahostkeypub",
     "highest_used_port",
     "tcpudp_port_pool",
     "mac_prefix",
     "volume_group_name",
+    "reserved_lvs",
+    "drbd_usermode_helper",
     "default_bridge",
     "default_hypervisor",
     "master_node",
     "default_bridge",
     "default_hypervisor",
     "master_node",
@@ -778,16 +1007,28 @@ class Cluster(TaggableObject):
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
+    "os_hvp",
     "beparams",
     "beparams",
+    "osparams",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
-    ]
+    "modify_ssh_setup",
+    "maintain_node_health",
+    "uid_pool",
+    "default_iallocator",
+    "hidden_os",
+    "blacklisted_os",
+    "primary_ip_family",
+    "prealloc_wipe_disks",
+    ] + _TIMESTAMPS + _UUID
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
     """
 
   def UpgradeConfig(self):
     """Fill defaults for missing configuration values.
 
     """
+    # pylint: disable-msg=E0203
+    # because these are "defined" via slots, not manually
     if self.hvparams is None:
       self.hvparams = constants.HVC_DEFAULTS
     else:
     if self.hvparams is None:
       self.hvparams = constants.HVC_DEFAULTS
     else:
@@ -795,6 +1036,13 @@ class Cluster(TaggableObject):
         self.hvparams[hypervisor] = FillDict(
             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
 
         self.hvparams[hypervisor] = FillDict(
             constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
 
+    if self.os_hvp is None:
+      self.os_hvp = {}
+
+    # osparams added before 2.2
+    if self.osparams is None:
+      self.osparams = {}
+
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
     migrate_default_bridge = not self.nicparams
     self.beparams = UpgradeGroupedParams(self.beparams,
                                          constants.BEC_DEFAULTS)
     migrate_default_bridge = not self.nicparams
@@ -807,18 +1055,50 @@ class Cluster(TaggableObject):
     if self.modify_etc_hosts is None:
       self.modify_etc_hosts = True
 
     if self.modify_etc_hosts is None:
       self.modify_etc_hosts = True
 
+    if self.modify_ssh_setup is None:
+      self.modify_ssh_setup = True
+
     # default_bridge is no longer used it 2.1. The slot is left there to
     # default_bridge is no longer used it 2.1. The slot is left there to
-    # support auto-upgrading, but will be removed in 2.2
+    # support auto-upgrading. It can be removed once we decide to deprecate
+    # upgrading straight from 2.0.
     if self.default_bridge is not None:
       self.default_bridge = None
 
     if self.default_bridge is not None:
       self.default_bridge = None
 
-    # default_hypervisor is just the first enabled one in 2.1
+    # default_hypervisor is just the first enabled one in 2.1. This slot and
+    # code can be removed once upgrading straight from 2.0 is deprecated.
     if self.default_hypervisor is not None:
     if self.default_hypervisor is not None:
-      self.enabled_hypervisors = [self.default_hypervisor] + \
+      self.enabled_hypervisors = ([self.default_hypervisor] +
         [hvname for hvname in self.enabled_hypervisors
         [hvname for hvname in self.enabled_hypervisors
-         if hvname != self.default_hypervisor]
+         if hvname != self.default_hypervisor])
       self.default_hypervisor = None
 
       self.default_hypervisor = None
 
+    # maintain_node_health added after 2.1.1
+    if self.maintain_node_health is None:
+      self.maintain_node_health = False
+
+    if self.uid_pool is None:
+      self.uid_pool = []
+
+    if self.default_iallocator is None:
+      self.default_iallocator = ""
+
+    # reserved_lvs added before 2.2
+    if self.reserved_lvs is None:
+      self.reserved_lvs = []
+
+    # hidden and blacklisted operating systems added before 2.2.1
+    if self.hidden_os is None:
+      self.hidden_os = []
+
+    if self.blacklisted_os is None:
+      self.blacklisted_os = []
+
+    # primary_ip_family added before 2.3
+    if self.primary_ip_family is None:
+      self.primary_ip_family = AF_INET
+
+    if self.prealloc_wipe_disks is None:
+      self.prealloc_wipe_disks = False
 
   def ToDict(self):
     """Custom function for cluster.
 
   def ToDict(self):
     """Custom function for cluster.
@@ -838,21 +1118,82 @@ class Cluster(TaggableObject):
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
       obj.tcpudp_port_pool = set(obj.tcpudp_port_pool)
     return obj
 
-  def FillHV(self, instance):
-    """Fill an instance's hvparams dict.
+  def GetHVDefaults(self, hypervisor, os_name=None, skip_keys=None):
+    """Get the default hypervisor parameters for the cluster.
+
+    @param hypervisor: the hypervisor name
+    @param os_name: if specified, we'll also update the defaults for this OS
+    @param skip_keys: if passed, list of keys not to use
+    @return: the defaults dict
+
+    """
+    if skip_keys is None:
+      skip_keys = []
+
+    fill_stack = [self.hvparams.get(hypervisor, {})]
+    if os_name is not None:
+      os_hvp = self.os_hvp.get(os_name, {}).get(hypervisor, {})
+      fill_stack.append(os_hvp)
+
+    ret_dict = {}
+    for o_dict in fill_stack:
+      ret_dict = FillDict(ret_dict, o_dict, skip_keys=skip_keys)
+
+    return ret_dict
+
+  def SimpleFillHV(self, hv_name, os_name, hvparams, skip_globals=False):
+    """Fill a given hvparams dict with cluster defaults.
+
+    @type hv_name: string
+    @param hv_name: the hypervisor to use
+    @type os_name: string
+    @param os_name: the OS to use for overriding the hypervisor defaults
+    @type skip_globals: boolean
+    @param skip_globals: if True, the global hypervisor parameters will
+        not be filled
+    @rtype: dict
+    @return: a copy of the given hvparams with missing keys filled from
+        the cluster defaults
+
+    """
+    if skip_globals:
+      skip_keys = constants.HVC_GLOBALS
+    else:
+      skip_keys = []
+
+    def_dict = self.GetHVDefaults(hv_name, os_name, skip_keys=skip_keys)
+    return FillDict(def_dict, hvparams, skip_keys=skip_keys)
+
+  def FillHV(self, instance, skip_globals=False):
+    """Fill an instance's hvparams dict with cluster defaults.
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
+    @type skip_globals: boolean
+    @param skip_globals: if True, the global hypervisor parameters will
+        not be filled
     @rtype: dict
     @return: a copy of the instance's hvparams with missing keys filled from
         the cluster defaults
 
     """
     @rtype: dict
     @return: a copy of the instance's hvparams with missing keys filled from
         the cluster defaults
 
     """
-    return FillDict(self.hvparams.get(instance.hypervisor, {}),
-                         instance.hvparams)
+    return self.SimpleFillHV(instance.hypervisor, instance.os,
+                             instance.hvparams, skip_globals)
+
+  def SimpleFillBE(self, beparams):
+    """Fill a given beparams dict with cluster defaults.
+
+    @type beparams: dict
+    @param beparams: the dict to fill
+    @rtype: dict
+    @return: a copy of the passed in beparams with missing keys filled
+        from the cluster defaults
+
+    """
+    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}), beparams)
 
   def FillBE(self, instance):
 
   def FillBE(self, instance):
-    """Fill an instance's beparams dict.
+    """Fill an instance's beparams dict with cluster defaults.
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
@@ -861,8 +1202,39 @@ class Cluster(TaggableObject):
         the cluster defaults
 
     """
         the cluster defaults
 
     """
-    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
-                          instance.beparams)
+    return self.SimpleFillBE(instance.beparams)
+
+  def SimpleFillNIC(self, nicparams):
+    """Fill a given nicparams dict with cluster defaults.
+
+    @type nicparams: dict
+    @param nicparams: the dict to fill
+    @rtype: dict
+    @return: a copy of the passed in nicparams with missing keys filled
+        from the cluster defaults
+
+    """
+    return FillDict(self.nicparams.get(constants.PP_DEFAULT, {}), nicparams)
+
+  def SimpleFillOS(self, os_name, os_params):
+    """Fill an instance's osparams dict with cluster defaults.
+
+    @type os_name: string
+    @param os_name: the OS name to use
+    @type os_params: dict
+    @param os_params: the dict to fill with default values
+    @rtype: dict
+    @return: a copy of the instance's osparams with missing keys filled from
+        the cluster defaults
+
+    """
+    name_only = os_name.split("+", 1)[0]
+    # base OS
+    result = self.osparams.get(name_only, {})
+    # OS with variant
+    result = FillDict(result, self.osparams.get(os_name, {}))
+    # specified params
+    return FillDict(result, os_params)
 
 
 class BlockDevStatus(ConfigObject):
 
 
 class BlockDevStatus(ConfigObject):
@@ -878,6 +1250,74 @@ class BlockDevStatus(ConfigObject):
     ]
 
 
     ]
 
 
+class ImportExportStatus(ConfigObject):
+  """Config object representing the status of an import or export."""
+  __slots__ = [
+    "recent_output",
+    "listen_port",
+    "connected",
+    "progress_mbytes",
+    "progress_throughput",
+    "progress_eta",
+    "progress_percent",
+    "exit_status",
+    "error_message",
+    ] + _TIMESTAMPS
+
+
+class ImportExportOptions(ConfigObject):
+  """Options for import/export daemon
+
+  @ivar key_name: X509 key name (None for cluster certificate)
+  @ivar ca_pem: Remote peer CA in PEM format (None for cluster certificate)
+  @ivar compress: Compression method (one of L{constants.IEC_ALL})
+  @ivar magic: Used to ensure the connection goes to the right disk
+  @ivar connect_timeout: Number of seconds for establishing connection
+
+  """
+  __slots__ = [
+    "key_name",
+    "ca_pem",
+    "compress",
+    "magic",
+    "connect_timeout",
+    ]
+
+
+class ConfdRequest(ConfigObject):
+  """Object holding a confd request.
+
+  @ivar protocol: confd protocol version
+  @ivar type: confd query type
+  @ivar query: query request
+  @ivar rsalt: requested reply salt
+
+  """
+  __slots__ = [
+    "protocol",
+    "type",
+    "query",
+    "rsalt",
+    ]
+
+
+class ConfdReply(ConfigObject):
+  """Object holding a confd reply.
+
+  @ivar protocol: confd protocol version
+  @ivar status: reply status code (ok, error)
+  @ivar answer: confd query reply
+  @ivar serial: configuration serial number
+
+  """
+  __slots__ = [
+    "protocol",
+    "status",
+    "answer",
+    "serial",
+    ]
+
+
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.
 
@@ -892,10 +1332,10 @@ class SerializableConfigParser(ConfigParser.SafeConfigParser):
     self.write(buf)
     return buf.getvalue()
 
     self.write(buf)
     return buf.getvalue()
 
-  @staticmethod
-  def Loads(data):
+  @classmethod
+  def Loads(cls, data):
     """Load data from a string."""
     buf = StringIO(data)
     """Load data from a string."""
     buf = StringIO(data)
-    cfp = SerializableConfigParser()
+    cfp = cls()
     cfp.readfp(buf)
     return cfp
     cfp.readfp(buf)
     return cfp