Cluster: add nicparams, and update them on upgrade
[ganeti-local] / lib / objects.py
index 26cdc3d..64b4418 100644 (file)
@@ -37,7 +37,39 @@ 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):
@@ -50,7 +82,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__ = []
@@ -58,6 +90,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__:
@@ -157,6 +190,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.
@@ -179,7 +221,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):
@@ -270,6 +312,24 @@ class NIC(ConfigObject):
   """Config object representing a network card."""
   __slots__ = ["mac", "ip", "bridge"]
 
   """Config object representing a network card."""
   __slots__ = ["mac", "ip", "bridge"]
 
+  @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)
+
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
 
 class Disk(ConfigObject):
   """Config object representing a block device."""
@@ -472,6 +532,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"
@@ -480,9 +541,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:
@@ -494,9 +554,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.
+
+    """
+    errors = []
+    if self.mode not in constants.DISK_ACCESS_SET:
+      errors.append("Disk access mode '%s' is invalid" % (self.mode, ))
+    return errors
+
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
 
 class Instance(TaggableObject):
   """Config object representing an instance."""
@@ -507,7 +579,7 @@ class Instance(TaggableObject):
     "hypervisor",
     "hvparams",
     "beparams",
     "hypervisor",
     "hvparams",
     "beparams",
-    "status",
+    "admin_up",
     "nics",
     "disks",
     "disk_template",
     "nics",
     "disks",
     "disk_template",
@@ -518,46 +590,55 @@ class Instance(TaggableObject):
   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:
@@ -579,12 +660,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])
@@ -594,17 +669,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.
@@ -675,6 +759,9 @@ class Node(TaggableObject):
     "primary_ip",
     "secondary_ip",
     "serial_no",
     "primary_ip",
     "secondary_ip",
     "serial_no",
+    "master_candidate",
+    "offline",
+    "drained",
     ]
 
 
     ]
 
 
@@ -697,8 +784,34 @@ class Cluster(TaggableObject):
     "enabled_hypervisors",
     "hvparams",
     "beparams",
     "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
+
   def ToDict(self):
     """Custom function for cluster.
 
   def ToDict(self):
     """Custom function for cluster.
 
@@ -717,22 +830,6 @@ 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
 
-  @staticmethod
-  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 FillHV(self, instance):
     """Fill an instance's hvparams dict.
 
   def FillHV(self, instance):
     """Fill an instance's hvparams dict.
 
@@ -743,7 +840,7 @@ class Cluster(TaggableObject):
         the cluster defaults
 
     """
         the cluster defaults
 
     """
-    return self.FillDict(self.hvparams.get(instance.hypervisor, {}),
+    return FillDict(self.hvparams.get(instance.hypervisor, {}),
                          instance.hvparams)
 
   def FillBE(self, instance):
                          instance.hvparams)
 
   def FillBE(self, instance):
@@ -756,8 +853,8 @@ class Cluster(TaggableObject):
         the cluster defaults
 
     """
         the cluster defaults
 
     """
-    return self.FillDict(self.beparams.get(constants.BEGR_DEFAULT, {}),
-                         instance.beparams)
+    return FillDict(self.beparams.get(constants.PP_DEFAULT, {}),
+                          instance.beparams)
 
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):
 
 
 class SerializableConfigParser(ConfigParser.SafeConfigParser):