Re-wrap some lines to keep them under 80 chars
[ganeti-local] / lib / config.py
index b1661fd..9256bb9 100644 (file)
@@ -84,6 +84,7 @@ class ConfigWriter:
     # better to raise an error before starting to modify the config
     # file than after it was modified
     self._my_hostname = utils.HostInfo().name
+    self._last_cluster_serial = -1
     self._OpenConfig()
 
   # this method needs to be static, so that we can call it on the class
@@ -226,10 +227,13 @@ class ConfigWriter:
 
     return result
 
-  @locking.ssynchronized(_config_lock, shared=1)
-  def VerifyConfig(self):
+  def _UnlockedVerifyConfig(self):
     """Verify function.
 
+    @rtype: list
+    @return: a list of error messages; a non-empty list signifies
+        configuration errors
+
     """
     result = []
     seen_macs = []
@@ -265,6 +269,11 @@ class ConfigWriter:
           ports[net_port] = []
         ports[net_port].append((instance.name, "network port"))
 
+      # instance disk verify
+      for idx, disk in enumerate(instance.disks):
+        result.extend(["instance '%s' disk %d error: %s" %
+                       (instance.name, idx, msg) for msg in disk.Verify()])
+
     # cluster-wide pool of free ports
     for free_port in data.cluster.tcpudp_port_pool:
       if free_port not in ports:
@@ -289,13 +298,33 @@ class ConfigWriter:
     if not data.nodes[data.cluster.master_node].master_candidate:
       result.append("Master node is not a master candidate")
 
+    # master candidate checks
     mc_now, mc_max = self._UnlockedGetMasterCandidateStats()
     if mc_now < mc_max:
       result.append("Not enough master candidates: actual %d, target %d" %
                     (mc_now, mc_max))
 
+    # drbd minors check
+    d_map, duplicates = self._UnlockedComputeDRBDMap()
+    for node, minor, instance_a, instance_b in duplicates:
+      result.append("DRBD minor %d on node %s is assigned twice to instances"
+                    " %s and %s" % (minor, node, instance_a, instance_b))
+
     return result
 
+  @locking.ssynchronized(_config_lock, shared=1)
+  def VerifyConfig(self):
+    """Verify function.
+
+    This is just a wrapper over L{_UnlockedVerifyConfig}.
+
+    @rtype: list
+    @return: a list of error messages; a non-empty list signifies
+        configuration errors
+
+    """
+    return self._UnlockedVerifyConfig()
+
   def _UnlockedSetDiskID(self, disk, node_name):
     """Convert the unique ID to the ID needed on the target nodes.
 
@@ -388,37 +417,61 @@ class ConfigWriter:
     self._WriteConfig()
     return port
 
-  def _ComputeDRBDMap(self, instance):
+  def _UnlockedComputeDRBDMap(self):
     """Compute the used DRBD minor/nodes.
 
+    @rtype: (dict, list)
     @return: dictionary of node_name: dict of minor: instance_name;
         the returned dict will have all the nodes in it (even if with
-        an empty list).
+        an empty list), and a list of duplicates; if the duplicates
+        list is not empty, the configuration is corrupted and its caller
+        should raise an exception
 
     """
     def _AppendUsedPorts(instance_name, disk, used):
+      duplicates = []
       if disk.dev_type == constants.LD_DRBD8 and len(disk.logical_id) >= 5:
         nodeA, nodeB, dummy, minorA, minorB = disk.logical_id[:5]
         for node, port in ((nodeA, minorA), (nodeB, minorB)):
-          assert node in used, "Instance node not found in node list"
+          assert node in used, ("Node '%s' of instance '%s' not found"
+                                " in node list" % (node, instance_name))
           if port in used[node]:
-            raise errors.ProgrammerError("DRBD minor already used:"
-                                         " %s/%s, %s/%s" %
-                                         (node, port, instance_name,
-                                          used[node][port]))
-
-          used[node][port] = instance_name
+            duplicates.append((node, port, instance_name, used[node][port]))
+          else:
+            used[node][port] = instance_name
       if disk.children:
         for child in disk.children:
-          _AppendUsedPorts(instance_name, child, used)
+          duplicates.extend(_AppendUsedPorts(instance_name, child, used))
+      return duplicates
 
+    duplicates = []
     my_dict = dict((node, {}) for node in self._config_data.nodes)
-    for (node, minor), instance in self._temporary_drbds.iteritems():
-      my_dict[node][minor] = instance
     for instance in self._config_data.instances.itervalues():
       for disk in instance.disks:
-        _AppendUsedPorts(instance.name, disk, my_dict)
-    return my_dict
+        duplicates.extend(_AppendUsedPorts(instance.name, disk, my_dict))
+    for (node, minor), instance in self._temporary_drbds.iteritems():
+      if minor in my_dict[node] and my_dict[node][minor] != instance:
+        duplicates.append((node, minor, instance, my_dict[node][minor]))
+      else:
+        my_dict[node][minor] = instance
+    return my_dict, duplicates
+
+  @locking.ssynchronized(_config_lock)
+  def ComputeDRBDMap(self):
+    """Compute the used DRBD minor/nodes.
+
+    This is just a wrapper over L{_UnlockedComputeDRBDMap}.
+
+    @return: dictionary of node_name: dict of minor: instance_name;
+        the returned dict will have all the nodes in it (even if with
+        an empty list).
+
+    """
+    d_map, duplicates = self._UnlockedComputeDRBDMap()
+    if duplicates:
+      raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
+                                      str(duplicates))
+    return d_map
 
   @locking.ssynchronized(_config_lock)
   def AllocateDRBDMinor(self, nodes, instance):
@@ -429,8 +482,17 @@ class ConfigWriter:
     multiple minors. The result is the list of minors, in the same
     order as the passed nodes.
 
+    @type instance: string
+    @param instance: the instance for which we allocate minors
+
     """
-    d_map = self._ComputeDRBDMap(instance)
+    assert isinstance(instance, basestring), \
+           "Invalid argument '%s' passed to AllocateDRBDMinor" % instance
+
+    d_map, duplicates = self._UnlockedComputeDRBDMap()
+    if duplicates:
+      raise errors.ConfigurationError("Duplicate DRBD ports detected: %s" %
+                                      str(duplicates))
     result = []
     for nname in nodes:
       ndata = d_map[nname]
@@ -449,31 +511,55 @@ class ConfigWriter:
         minor = keys[-1] + 1
       else:
         minor = ffree
-      result.append(minor)
+      # double-check minor against current instances
+      assert minor not in d_map[nname], \
+             ("Attempt to reuse allocated DRBD minor %d on node %s,"
+              " already allocated to instance %s" %
+              (minor, nname, d_map[nname][minor]))
       ndata[minor] = instance
-      assert (nname, minor) not in self._temporary_drbds, \
-             "Attempt to reuse reserved DRBD minor"
-      self._temporary_drbds[(nname, minor)] = instance
+      # double-check minor against reservation
+      r_key = (nname, minor)
+      assert r_key not in self._temporary_drbds, \
+             ("Attempt to reuse reserved DRBD minor %d on node %s,"
+              " reserved for instance %s" %
+              (minor, nname, self._temporary_drbds[r_key]))
+      self._temporary_drbds[r_key] = instance
+      result.append(minor)
     logging.debug("Request to allocate drbd minors, input: %s, returning %s",
                   nodes, result)
     return result
 
-  @locking.ssynchronized(_config_lock)
-  def ReleaseDRBDMinors(self, instance):
+  def _UnlockedReleaseDRBDMinors(self, instance):
     """Release temporary drbd minors allocated for a given instance.
 
-    This should be called on both the error paths and on the success
-    paths (after the instance has been added or updated).
-
     @type instance: string
     @param instance: the instance for which temporary minors should be
                      released
 
     """
+    assert isinstance(instance, basestring), \
+           "Invalid argument passed to ReleaseDRBDMinors"
     for key, name in self._temporary_drbds.items():
       if name == instance:
         del self._temporary_drbds[key]
 
+  @locking.ssynchronized(_config_lock)
+  def ReleaseDRBDMinors(self, instance):
+    """Release temporary drbd minors allocated for a given instance.
+
+    This should be called on the error paths, on the success paths
+    it's automatically called by the ConfigWriter add and update
+    functions.
+
+    This function is just a wrapper over L{_UnlockedReleaseDRBDMinors}.
+
+    @type instance: string
+    @param instance: the instance for which temporary minors should be
+                     released
+
+    """
+    self._UnlockedReleaseDRBDMinors(instance)
+
   @locking.ssynchronized(_config_lock, shared=1)
   def GetConfigVersion(self):
     """Get the configuration version.
@@ -560,23 +646,22 @@ class ConfigWriter:
 
     instance.serial_no = 1
     self._config_data.instances[instance.name] = instance
+    self._UnlockedReleaseDRBDMinors(instance.name)
     self._WriteConfig()
 
   def _SetInstanceStatus(self, instance_name, status):
     """Set the instance's status to a given value.
 
     """
-    if status not in ("up", "down"):
-      raise errors.ProgrammerError("Invalid status '%s' passed to"
-                                   " ConfigWriter._SetInstanceStatus()" %
-                                   status)
+    assert isinstance(status, bool), \
+           "Invalid status '%s' passed to SetInstanceStatus" % (status,)
 
     if instance_name not in self._config_data.instances:
       raise errors.ConfigurationError("Unknown instance '%s'" %
                                       instance_name)
     instance = self._config_data.instances[instance_name]
-    if instance.status != status:
-      instance.status = status
+    if instance.admin_up != status:
+      instance.admin_up = status
       instance.serial_no += 1
       self._WriteConfig()
 
@@ -585,7 +670,7 @@ class ConfigWriter:
     """Mark the instance status to up in the config.
 
     """
-    self._SetInstanceStatus(instance_name, "up")
+    self._SetInstanceStatus(instance_name, True)
 
   @locking.ssynchronized(_config_lock)
   def RemoveInstance(self, instance_name):
@@ -629,7 +714,7 @@ class ConfigWriter:
     """Mark the status of an instance to down in the configuration.
 
     """
-    self._SetInstanceStatus(instance_name, "down")
+    self._SetInstanceStatus(instance_name, False)
 
   def _UnlockedGetInstanceList(self):
     """Get the list of instances.
@@ -786,6 +871,15 @@ class ConfigWriter:
     return self._UnlockedGetNodeList()
 
   @locking.ssynchronized(_config_lock, shared=1)
+  def GetOnlineNodeList(self):
+    """Return the list of nodes which are online.
+
+    """
+    all_nodes = [self._UnlockedGetNodeInfo(node)
+                 for node in self._UnlockedGetNodeList()]
+    return [node.name for node in all_nodes if not node.offline]
+
+  @locking.ssynchronized(_config_lock, shared=1)
   def GetAllNodesInfo(self):
     """Get the configuration of all nodes.
 
@@ -886,7 +980,7 @@ class ConfigWriter:
       raise errors.ConfigurationError("Incomplete configuration"
                                       " (missing cluster.rsahostkeypub)")
     self._config_data = data
-    # init the last serial as -1 so that the next write will cause
+    # reset the last serial as -1 so that the next write will cause
     # ssconf update
     self._last_cluster_serial = -1
 
@@ -930,6 +1024,11 @@ class ConfigWriter:
     """Write the configuration data to persistent storage.
 
     """
+    config_errors = self._UnlockedVerifyConfig()
+    if config_errors:
+      raise errors.ConfigurationError("Configuration data is not"
+                                      " consistent: %s" %
+                                      (", ".join(config_errors)))
     if destination is None:
       destination = self._cfg_file
     self._BumpSerialNo()
@@ -982,6 +1081,7 @@ class ConfigWriter:
       constants.SS_MASTER_NODE: cluster.master_node,
       constants.SS_NODE_LIST: node_data,
       constants.SS_OFFLINE_NODES: off_data,
+      constants.SS_RELEASE_VERSION: constants.RELEASE_VERSION,
       }
 
   @locking.ssynchronized(_config_lock)
@@ -1088,4 +1188,7 @@ class ConfigWriter:
       # for node updates, we need to increase the cluster serial too
       self._config_data.cluster.serial_no += 1
 
+    if isinstance(target, objects.Instance):
+      self._UnlockedReleaseDRBDMinors(target.name)
+
     self._WriteConfig()