Merge branch 'devel-2.1'
[ganeti-local] / lib / objects.py
index 106b704..d4c3fd3 100644 (file)
@@ -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
@@ -39,19 +45,31 @@ from ganeti import constants
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
            "OS", "Node", "Cluster", "FillDict"]
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
            "OS", "Node", "Cluster", "FillDict"]
 
-def FillDict(defaults_dict, custom_dict):
+_TIMESTAMPS = ["ctime", "mtime"]
+_UUID = ["uuid"]
+
+
+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
 
 
@@ -90,31 +108,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 __getstate__(self):
-    state = {}
-    for name in self.__slots__:
-      if hasattr(self, name):
-        state[name] = getattr(self, name)
-    return state
-
   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.
 
@@ -125,7 +141,14 @@ class ConfigObject(object):
     make sure all objects returned are only standard python types.
 
     """
     make sure all objects returned are only standard python types.
 
     """
-    return dict([(k, getattr(self, k, None)) for k in self.__slots__])
+    result = {}
+    for name in self._all_slots():
+      value = getattr(self, name, None)
+      if value is not None:
+        result[name] = value
+    return result
+
+  __getstate__ = ToDict
 
   @classmethod
   def FromDict(cls, val):
 
   @classmethod
   def FromDict(cls, val):
@@ -144,7 +167,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
@@ -186,6 +209,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())
@@ -193,8 +224,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
@@ -204,10 +235,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
@@ -221,7 +253,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):
@@ -280,7 +312,8 @@ 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", "instances", "serial_no"] +
+               _TIMESTAMPS)
 
   def ToDict(self):
     """Custom function for top-level config data.
 
   def ToDict(self):
     """Custom function for top-level config data.
@@ -307,6 +340,16 @@ class ConfigData(ConfigObject):
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
     return obj
 
     obj.instances = cls._ContainerFromDicts(obj.instances, dict, Instance)
     return obj
 
+  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()
+
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
@@ -325,7 +368,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)
@@ -369,6 +412,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])
@@ -452,7 +498,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:
@@ -462,6 +508,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.
 
@@ -583,10 +638,19 @@ 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",
     "name",
     "primary_node",
     "os",
@@ -599,7 +663,7 @@ class Instance(TaggableObject):
     "disk_template",
     "network_port",
     "serial_no",
     "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.
@@ -698,11 +762,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.
@@ -732,6 +798,21 @@ 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
+
 
 class OS(ConfigObject):
   """Config object representing an operating system."""
 
 class OS(ConfigObject):
   """Config object representing an operating system."""
@@ -743,12 +824,13 @@ class OS(ConfigObject):
     "export_script",
     "import_script",
     "rename_script",
     "export_script",
     "import_script",
     "rename_script",
+    "supported_variants",
     ]
 
 
 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",
@@ -756,12 +838,12 @@ class Node(TaggableObject):
     "master_candidate",
     "offline",
     "drained",
     "master_candidate",
     "offline",
     "drained",
-    ]
+    ] + _TIMESTAMPS + _UUID
 
 
 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",
     "serial_no",
     "rsahostkeypub",
     "highest_used_port",
@@ -777,16 +859,22 @@ class Cluster(TaggableObject):
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
     "file_storage_dir",
     "enabled_hypervisors",
     "hvparams",
+    "os_hvp",
     "beparams",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
     "beparams",
     "nicparams",
     "candidate_pool_size",
     "modify_etc_hosts",
-    ]
+    "modify_ssh_setup",
+    "maintain_node_health",
+    "uid_pool",
+    ] + _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:
@@ -794,6 +882,10 @@ 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])
 
+    # TODO: Figure out if it's better to put this into OS than Cluster
+    if self.os_hvp is None:
+      self.os_hvp = {}
+
     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
@@ -806,11 +898,28 @@ 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
     # support auto-upgrading, but will be removed in 2.2
     if self.default_bridge is not None:
       self.default_bridge = None
 
     # 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
     if self.default_bridge is not None:
       self.default_bridge = None
 
+    # default_hypervisor is just the first enabled one in 2.1
+    if self.default_hypervisor is not None:
+      self.enabled_hypervisors = ([self.default_hypervisor] +
+        [hvname for hvname in self.enabled_hypervisors
+         if hvname != self.default_hypervisor])
+      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 = []
+
   def ToDict(self):
     """Custom function for cluster.
 
   def ToDict(self):
     """Custom function for cluster.
 
@@ -829,18 +938,51 @@ 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):
+  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 FillHV(self, instance, skip_globals=False):
     """Fill an instance's hvparams dict.
 
     @type instance: L{objects.Instance}
     @param instance: the instance parameter to fill
     """Fill an instance's hvparams dict.
 
     @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)
+    if skip_globals:
+      skip_keys = constants.HVC_GLOBALS
+    else:
+      skip_keys = []
+
+    def_dict = self.GetHVDefaults(instance.hypervisor, instance.os,
+                                  skip_keys=skip_keys)
+    return FillDict(def_dict, instance.hvparams, skip_keys=skip_keys)
 
   def FillBE(self, instance):
     """Fill an instance's beparams dict.
 
   def FillBE(self, instance):
     """Fill an instance's beparams dict.
@@ -853,7 +995,65 @@ class Cluster(TaggableObject):
 
     """
     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
 
     """
     return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
-                          instance.beparams)
+                    instance.beparams)
+
+
+class BlockDevStatus(ConfigObject):
+  """Config object representing the status of a block device."""
+  __slots__ = [
+    "dev_path",
+    "major",
+    "minor",
+    "sync_percent",
+    "estimated_time",
+    "is_degraded",
+    "ldisk_status",
+    ]
+
+
+class ImportExportStatus(ConfigObject):
+  """Config object representing the status of an import or export."""
+  __slots__ = [
+    "recent_output",
+    "listen_port",
+    "connected",
+    "exit_status",
+    "error_message",
+    ] + _TIMESTAMPS
+
+
+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):
 
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
@@ -870,10 +1070,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