Use object for blockdev_find RPC call result
[ganeti-local] / lib / objects.py
index a26907d..1cdc98f 100644 (file)
@@ -29,6 +29,7 @@ pass to and from external parties.
 
 import ConfigParser
 import re
 
 import ConfigParser
 import re
+import copy
 from cStringIO import StringIO
 
 from ganeti import errors
 from cStringIO import StringIO
 
 from ganeti import errors
@@ -36,7 +37,40 @@ from ganeti import constants
 
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
 
 
 __all__ = ["ConfigObject", "ConfigData", "NIC", "Disk", "Instance",
-           "OS", "Node", "Cluster"]
+           "OS", "Node", "Cluster", "FillDict"]
+
+
+def FillDict(defaults_dict, custom_dict):
+  """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
+  @rtype: dict
+  @return: dict with the 'full' values
+
+  """
+  ret_dict = copy.deepcopy(defaults_dict)
+  ret_dict.update(custom_dict)
+  return ret_dict
+
+
+def UpgradeGroupedParams(target, defaults):
+  """Update all groups for the target parameter.
+
+  @type target: dict of dicts
+  @param target: {group: {parameter: value}}
+  @type defaults: dict
+  @param defaults: default parameter values
+
+  """
+  if target is None:
+    target = {constants.PP_DEFAULT: defaults}
+  else:
+    for group in target:
+      target[group] = FillDict(defaults, target[group])
+  return target
 
 
 class ConfigObject(object):
 
 
 class ConfigObject(object):
@@ -49,7 +83,7 @@ class ConfigObject(object):
       as None instead of raising an error
 
   Classes derived from this must always declare __slots__ (we use many
       as None instead of raising an error
 
   Classes derived from this must always declare __slots__ (we use many
-  config objects and the memory reduction is useful.
+  config objects and the memory reduction is useful)
 
   """
   __slots__ = []
 
   """
   __slots__ = []
@@ -57,6 +91,7 @@ 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):
     if name not in self.__slots__:
 
   def __getattr__(self, name):
     if name not in self.__slots__:
@@ -156,6 +191,15 @@ class ConfigObject(object):
     """Implement __repr__ for ConfigObjects."""
     return repr(self.ToDict())
 
     """Implement __repr__ for ConfigObjects."""
     return repr(self.ToDict())
 
+  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.
+
+    """
+    pass
+
 
 class TaggableObject(ConfigObject):
   """An generic class supporting tags.
 
 class TaggableObject(ConfigObject):
   """An generic class supporting tags.
@@ -178,7 +222,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 re.match("^[\w.+*/:-]+$", tag):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
       raise errors.TagError("Tag contains invalid characters")
 
   def GetTags(self):
@@ -237,7 +281,7 @@ class TaggableObject(ConfigObject):
 
 class ConfigData(ConfigObject):
   """Top-level config object."""
 
 class ConfigData(ConfigObject):
   """Top-level config object."""
-  __slots__ = ["cluster", "nodes", "instances"]
+  __slots__ = ["version", "cluster", "nodes", "instances", "serial_no"]
 
   def ToDict(self):
     """Custom function for top-level config data.
 
   def ToDict(self):
     """Custom function for top-level config data.
@@ -267,13 +311,45 @@ class ConfigData(ConfigObject):
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
 
 class NIC(ConfigObject):
   """Config object representing a network card."""
-  __slots__ = ["mac", "ip", "bridge"]
+  __slots__ = ["mac", "ip", "bridge", "nicparams"]
+
+  @classmethod
+  def CheckParameterSyntax(cls, nicparams):
+    """Check the given parameters for validity.
+
+    @type nicparams:  dict
+    @param nicparams: dictionary with parameter names/value
+    @raise errors.ConfigurationError: when a parameter is not valid
+
+    """
+    if nicparams[constants.NIC_MODE] not in constants.NIC_VALID_MODES:
+      err = "Invalid nic mode: %s" % nicparams[constants.NIC_MODE]
+      raise errors.ConfigurationError(err)
+
+    if (nicparams[constants.NIC_MODE] is constants.NIC_MODE_BRIDGED and
+        not nicparams[constants.NIC_LINK]):
+      err = "Missing bridged nic link"
+      raise errors.ConfigurationError(err)
+
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    if self.nicparams is None:
+      self.nicparams = {}
+      if self.bridge is not None:
+        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
+    if self.bridge is not None:
+      self.bridge = None
 
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
   __slots__ = ["dev_type", "logical_id", "physical_id",
 
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
   __slots__ = ["dev_type", "logical_id", "physical_id",
-               "children", "iv_name", "size"]
+               "children", "iv_name", "size", "mode"]
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
 
   def CreateOnSecondary(self):
     """Test if this device needs to be created on a secondary node."""
@@ -412,7 +488,7 @@ class Disk(ConfigObject):
     if self.logical_id is None and self.physical_id is not None:
       return
     if self.dev_type in constants.LDS_DRBD:
     if self.logical_id is None and self.physical_id is not None:
       return
     if self.dev_type in constants.LDS_DRBD:
-      pnode, snode, port, pminor, sminor = self.logical_id
+      pnode, snode, port, pminor, sminor, secret = self.logical_id
       if target_node not in (pnode, snode):
         raise errors.ConfigurationError("DRBD device not knowing node %s" %
                                         target_node)
       if target_node not in (pnode, snode):
         raise errors.ConfigurationError("DRBD device not knowing node %s" %
                                         target_node)
@@ -424,9 +500,9 @@ class Disk(ConfigObject):
       p_data = (pnode_ip, port)
       s_data = (snode_ip, port)
       if pnode == target_node:
       p_data = (pnode_ip, port)
       s_data = (snode_ip, port)
       if pnode == target_node:
-        self.physical_id = p_data + s_data + (pminor,)
+        self.physical_id = p_data + s_data + (pminor, secret)
       else: # it must be secondary, we tested above
       else: # it must be secondary, we tested above
-        self.physical_id = s_data + p_data + (sminor,)
+        self.physical_id = s_data + p_data + (sminor, secret)
     else:
       self.physical_id = self.logical_id
     return
     else:
       self.physical_id = self.logical_id
     return
@@ -458,9 +534,10 @@ class Disk(ConfigObject):
       obj.logical_id = tuple(obj.logical_id)
     if obj.physical_id and isinstance(obj.physical_id, list):
       obj.physical_id = tuple(obj.physical_id)
       obj.logical_id = tuple(obj.logical_id)
     if obj.physical_id and isinstance(obj.physical_id, list):
       obj.physical_id = tuple(obj.physical_id)
-    if obj.dev_type in constants.LDS_DRBD and len(obj.logical_id) == 3:
-      # old non-minor based disk type
-      obj.logical_id += (None, None)
+    if obj.dev_type in constants.LDS_DRBD:
+      # we need a tuple of length six here
+      if len(obj.logical_id) < 6:
+        obj.logical_id += (None,) * (6 - len(obj.logical_id))
     return obj
 
   def __str__(self):
     return obj
 
   def __str__(self):
@@ -470,6 +547,7 @@ class Disk(ConfigObject):
     if self.dev_type == constants.LD_LV:
       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
     elif self.dev_type in constants.LDS_DRBD:
     if self.dev_type == constants.LD_LV:
       val =  "<LogicalVolume(/dev/%s/%s" % self.logical_id
     elif self.dev_type in constants.LDS_DRBD:
+      node_a, node_b, port, minor_a, minor_b = self.logical_id[:5]
       val = "<DRBD8("
       if self.physical_id is None:
         phy = "unconfigured"
       val = "<DRBD8("
       if self.physical_id is None:
         phy = "unconfigured"
@@ -478,9 +556,8 @@ class Disk(ConfigObject):
                (self.physical_id[0], self.physical_id[1],
                 self.physical_id[2], self.physical_id[3]))
 
                (self.physical_id[0], self.physical_id[1],
                 self.physical_id[2], self.physical_id[3]))
 
-      val += ("hosts=%s-%s, port=%s, %s, " %
-              (self.logical_id[0], self.logical_id[1], self.logical_id[2],
-               phy))
+      val += ("hosts=%s/%d-%s/%d, port=%s, %s, " %
+              (node_a, minor_a, node_b, minor_b, port, phy))
       if self.children and self.children.count(None) == 0:
         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
       else:
       if self.children and self.children.count(None) == 0:
         val += "backend=%s, metadev=%s" % (self.children[0], self.children[1])
       else:
@@ -492,9 +569,21 @@ class Disk(ConfigObject):
       val += ", not visible"
     else:
       val += ", visible as /dev/%s" % self.iv_name
       val += ", not visible"
     else:
       val += ", visible as /dev/%s" % self.iv_name
-    val += ", size=%dm)>" % self.size
+    if isinstance(self.size, int):
+      val += ", size=%dm)>" % self.size
+    else:
+      val += ", size='%s')>" % (self.size,)
     return val
 
     return val
 
+  def Verify(self):
+    """Checks that this disk is correctly configured.
+
+    """
+    all_errors = []
+    if self.mode not in constants.DISK_ACCESS_SET:
+      all_errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
+    return all_errors
+
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
@@ -502,67 +591,69 @@ class Instance(TaggableObject):
     "name",
     "primary_node",
     "os",
     "name",
     "primary_node",
     "os",
-    "status",
-    "memory",
-    "vcpus",
+    "hypervisor",
+    "hvparams",
+    "beparams",
+    "admin_up",
     "nics",
     "disks",
     "disk_template",
     "network_port",
     "nics",
     "disks",
     "disk_template",
     "network_port",
-    "kernel_path",
-    "initrd_path",
-    "hvm_boot_order",
-    "hvm_acpi",
-    "hvm_pae",
-    "hvm_cdrom_image_path",
-    "hvm_nic_type",
-    "hvm_disk_type",
-    "vnc_bind_address",
+    "serial_no",
     ]
 
   def _ComputeSecondaryNodes(self):
     """Compute the list of secondary nodes.
 
     ]
 
   def _ComputeSecondaryNodes(self):
     """Compute the list of secondary nodes.
 
+    This is a simple wrapper over _ComputeAllNodes.
+
+    """
+    all_nodes = set(self._ComputeAllNodes())
+    all_nodes.discard(self.primary_node)
+    return tuple(all_nodes)
+
+  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
+                             "List of secondary nodes")
+
+  def _ComputeAllNodes(self):
+    """Compute the list of all nodes.
+
     Since the data is already there (in the drbd disks), keeping it as
     a separate normal attribute is redundant and if not properly
     synchronised can cause problems. Thus it's better to compute it
     dynamically.
 
     """
     Since the data is already there (in the drbd disks), keeping it as
     a separate normal attribute is redundant and if not properly
     synchronised can cause problems. Thus it's better to compute it
     dynamically.
 
     """
-    def _Helper(primary, sec_nodes, device):
-      """Recursively computes secondary nodes given a top device."""
+    def _Helper(nodes, device):
+      """Recursively computes nodes given a top device."""
       if device.dev_type in constants.LDS_DRBD:
       if device.dev_type in constants.LDS_DRBD:
-        nodea, nodeb, dummy = device.logical_id[:3]
-        if nodea == primary:
-          candidate = nodeb
-        else:
-          candidate = nodea
-        if candidate not in sec_nodes:
-          sec_nodes.append(candidate)
+        nodea, nodeb = device.logical_id[:2]
+        nodes.add(nodea)
+        nodes.add(nodeb)
       if device.children:
         for child in device.children:
       if device.children:
         for child in device.children:
-          _Helper(primary, sec_nodes, child)
+          _Helper(nodes, child)
 
 
-    secondary_nodes = []
+    all_nodes = set()
+    all_nodes.add(self.primary_node)
     for device in self.disks:
     for device in self.disks:
-      _Helper(self.primary_node, secondary_nodes, device)
-    return tuple(secondary_nodes)
+      _Helper(all_nodes, device)
+    return tuple(all_nodes)
 
 
-  secondary_nodes = property(_ComputeSecondaryNodes, None, None,
-                             "List of secondary nodes")
+  all_nodes = property(_ComputeAllNodes, None, None,
+                       "List of all nodes of the instance")
 
   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
     """Provide a mapping of nodes to LVs this instance owns.
 
 
   def MapLVsByNode(self, lvmap=None, devs=None, node=None):
     """Provide a mapping of nodes to LVs this instance owns.
 
-    This function figures out what logical volumes should belong on which
-    nodes, recursing through a device tree.
+    This function figures out what logical volumes should belong on
+    which nodes, recursing through a device tree.
 
 
-    Args:
-      lvmap: (optional) a dictionary to receive the 'node' : ['lv', ...] data.
+    @param lvmap: optional dictionary to receive the
+        'node' : ['lv', ...] data.
 
 
-    Returns:
-      None if lvmap arg is given.
-      Otherwise, { 'nodename' : ['volume1', 'volume2', ...], ... }
+    @return: None if lvmap arg is given, otherwise, a dictionary
+        of the form { 'nodename' : ['volume1', 'volume2', ...], ... }
 
     """
     if node == None:
 
     """
     if node == None:
@@ -584,12 +675,6 @@ class Instance(TaggableObject):
         lvmap[node].append(dev.logical_id[1])
 
       elif dev.dev_type in constants.LDS_DRBD:
         lvmap[node].append(dev.logical_id[1])
 
       elif dev.dev_type in constants.LDS_DRBD:
-        if dev.logical_id[0] not in lvmap:
-          lvmap[dev.logical_id[0]] = []
-
-        if dev.logical_id[1] not in lvmap:
-          lvmap[dev.logical_id[1]] = []
-
         if dev.children:
           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
         if dev.children:
           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[0])
           self.MapLVsByNode(lvmap, dev.children, dev.logical_id[1])
@@ -599,17 +684,26 @@ class Instance(TaggableObject):
 
     return ret
 
 
     return ret
 
-  def FindDisk(self, name):
-    """Find a disk given having a specified name.
+  def FindDisk(self, idx):
+    """Find a disk given having a specified index.
 
 
-    This will return the disk which has the given iv_name.
+    This is just a wrapper that does validation of the index.
 
 
-    """
-    for disk in self.disks:
-      if disk.iv_name == name:
-        return disk
+    @type idx: int
+    @param idx: the disk index
+    @rtype: L{Disk}
+    @return: the corresponding disk
+    @raise errors.OpPrereqError: when the given index is not valid
 
 
-    return None
+    """
+    try:
+      idx = int(idx)
+      return self.disks[idx]
+    except ValueError, err:
+      raise errors.OpPrereqError("Invalid disk index: '%s'" % str(err))
+    except IndexError:
+      raise errors.OpPrereqError("Invalid disk index: %d (instace has disks"
+                                 " 0 to %d" % (idx, len(self.disks)))
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
 
   def ToDict(self):
     """Instance-specific conversion to standard python types.
@@ -645,33 +739,13 @@ class OS(ConfigObject):
   __slots__ = [
     "name",
     "path",
   __slots__ = [
     "name",
     "path",
-    "status",
-    "api_version",
+    "api_versions",
     "create_script",
     "export_script",
     "import_script",
     "rename_script",
     ]
 
     "create_script",
     "export_script",
     "import_script",
     "rename_script",
     ]
 
-  @classmethod
-  def FromInvalidOS(cls, err):
-    """Create an OS from an InvalidOS error.
-
-    This routine knows how to convert an InvalidOS error to an OS
-    object representing the broken OS with a meaningful error message.
-
-    """
-    if not isinstance(err, errors.InvalidOS):
-      raise errors.ProgrammerError("Trying to initialize an OS from an"
-                                   " invalid object of type %s" % type(err))
-
-    return cls(name=err.args[0], path=err.args[1], status=err.args[2])
-
-  def __nonzero__(self):
-    return self.status == constants.OS_VALID_STATUS
-
-  __bool__ = __nonzero__
-
 
 class Node(TaggableObject):
   """Config object representing a node."""
 
 class Node(TaggableObject):
   """Config object representing a node."""
@@ -679,6 +753,10 @@ class Node(TaggableObject):
     "name",
     "primary_ip",
     "secondary_ip",
     "name",
     "primary_ip",
     "secondary_ip",
+    "serial_no",
+    "master_candidate",
+    "offline",
+    "drained",
     ]
 
 
     ]
 
 
@@ -692,8 +770,56 @@ class Cluster(TaggableObject):
     "mac_prefix",
     "volume_group_name",
     "default_bridge",
     "mac_prefix",
     "volume_group_name",
     "default_bridge",
+    "default_hypervisor",
+    "master_node",
+    "master_ip",
+    "master_netdev",
+    "cluster_name",
+    "file_storage_dir",
+    "enabled_hypervisors",
+    "hvparams",
+    "beparams",
+    "nicparams",
+    "candidate_pool_size",
+    "modify_etc_hosts",
     ]
 
     ]
 
+  def UpgradeConfig(self):
+    """Fill defaults for missing configuration values.
+
+    """
+    if self.hvparams is None:
+      self.hvparams = constants.HVC_DEFAULTS
+    else:
+      for hypervisor in self.hvparams:
+        self.hvparams[hypervisor] = FillDict(
+            constants.HVC_DEFAULTS[hypervisor], self.hvparams[hypervisor])
+
+    self.beparams = UpgradeGroupedParams(self.beparams,
+                                         constants.BEC_DEFAULTS)
+    migrate_default_bridge = not self.nicparams
+    self.nicparams = UpgradeGroupedParams(self.nicparams,
+                                          constants.NICC_DEFAULTS)
+    if migrate_default_bridge:
+      self.nicparams[constants.PP_DEFAULT][constants.NIC_LINK] = \
+        self.default_bridge
+
+    if self.modify_etc_hosts is None:
+      self.modify_etc_hosts = 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_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
+
+
   def ToDict(self):
     """Custom function for cluster.
 
   def ToDict(self):
     """Custom function for cluster.
 
@@ -712,6 +838,53 @@ 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.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance parameter to fill
+    @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)
+
+  def FillBE(self, instance):
+    """Fill an instance's beparams dict.
+
+    @type instance: L{objects.Instance}
+    @param instance: the instance parameter to fill
+    @rtype: dict
+    @return: a copy of the instance's beparams with missing keys filled from
+        the cluster defaults
+
+    """
+    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
+                          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_degraded",
+    ]
+
+  def ToLegacyStatus(self):
+    """Converts the device status to a legacy tuple.
+
+    """
+    return (self.dev_path, self.major, self.minor,
+            self.sync_percent, self.estimated_time,
+            self.is_degraded, self.ldisk_degraded)
+
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
   """Simple wrapper over ConfigParse that allows serialization.