Merge branch 'stable-2.14' into stable-2.15
authorKlaus Aehlig <aehlig@google.com>
Fri, 15 Jan 2016 10:17:06 +0000 (11:17 +0100)
committerKlaus Aehlig <aehlig@google.com>
Fri, 15 Jan 2016 10:26:19 +0000 (11:26 +0100)
* stable-2.14
  Test disk attachment with different primary nodes
  Check for same primary node before disk attachment
  Add detach/attach sequence test
  Allow disk attachment with external storage

* stable-2.13
  Run ssh-key renewal in debug mode during upgrade

* stable-2.12
  Increase minimal sizes of test online nodes
  Also log the high-level upgrade steps
  Add function to provide logged user feedback
  Run renew-crypto in upgrades in debug mode
  Unconditionally log upgrades at debug level
  Document healthy-majority restriction on master-failover
  Check for healthy majority on master failover with voting
  Add a predicate testing that a majority of nodes is healthy
  Fix outdated comment
  Pass arguments to correct daemons during master-failover
  Fix documentation for master-failover

* stable-2.11
  (no changes)

* stable-2.10
  KVM: explicitly configure routed NICs late

Signed-off-by: Klaus Aehlig <aehlig@google.com>
Reviewed-by: Lisa Velden <velden@google.com>

lib/backend.py
lib/bootstrap.py
lib/cli.py
lib/client/gnt_cluster.py
lib/cmdlib/instance_set_params.py
lib/hypervisor/hv_kvm/__init__.py
lib/objects.py
man/gnt-cluster.rst
test/hs/Test/Ganeti/HTools/Node.hs
test/py/cmdlib/instance_unittest.py
tools/post-upgrade

index 376e278..4b523b6 100644 (file)
@@ -435,12 +435,13 @@ def StartMasterDaemons(no_voting):
   """
 
   if no_voting:
-    masterd_args = "--no-voting --yes-do-it"
+    daemon_args = "--no-voting --yes-do-it"
   else:
-    masterd_args = ""
+    daemon_args = ""
 
   env = {
-    "EXTRA_MASTERD_ARGS": masterd_args,
+    "EXTRA_LUXID_ARGS": daemon_args,
+    "EXTRA_WCONFD_ARGS": daemon_args,
     }
 
   result = utils.RunCmd([pathutils.DAEMON_UTIL, "start-master"], env=env)
index d649b8e..7b6fbfe 100644 (file)
@@ -1156,8 +1156,7 @@ def GatherMasterVotes(node_names):
   (if some nodes vote for another master).
 
   @type node_names: list
-  @param node_names: the list of nodes to query for master info; the current
-      node will be removed if it is in the list
+  @param node_names: the list of nodes to query for master info
   @rtype: list
   @return: list of (node, votes)
 
@@ -1193,3 +1192,24 @@ def GatherMasterVotes(node_names):
   vote_list.sort(key=lambda x: (x[1], x[0]), reverse=True)
 
   return vote_list
+
+
+def MajorityHealthy():
+  """Check if the majority of nodes is healthy
+
+  Gather master votes from all nodes known to this node;
+  return True if a strict majority of nodes is reachable and
+  has some opinion on which node is master. Note that this will
+  not guarantee any node to win an election but it ensures that
+  a standard master-failover is still possible.
+
+  """
+  node_names = ssconf.SimpleStore().GetNodeList()
+  node_count = len(node_names)
+  vote_list = GatherMasterVotes(node_names)
+  if vote_list is None:
+    return False
+  total_votes = sum([count for (node, count) in vote_list if node is not None])
+  logging.info("Total %d nodes, %d votes: %s", node_count, total_votes,
+               vote_list)
+  return 2 * total_votes > node_count
index 2b5a046..2001ed9 100644 (file)
@@ -86,6 +86,7 @@ __all__ = [
   "UsesRPC",
   # Formatting functions
   "ToStderr", "ToStdout",
+  "ToStdoutAndLoginfo",
   "FormatError",
   "FormatQueryResult",
   "FormatParamsDictInfo",
@@ -2369,6 +2370,12 @@ def ToStdout(txt, *args):
   _ToStream(sys.stdout, txt, *args)
 
 
+def ToStdoutAndLoginfo(txt, *args):
+  """Write a message to stdout and additionally log it at INFO level"""
+  ToStdout(txt, *args)
+  logging.info(txt, *args)
+
+
 def ToStderr(txt, *args):
   """Write a message to stderr only, bypassing the logging system
 
index 27877a7..5c2c576 100644 (file)
@@ -877,6 +877,14 @@ def MasterFailover(opts, args):
   @return: the desired exit code
 
   """
+  if not opts.no_voting:
+    # Verify that a majority of nodes is still healthy
+    if not bootstrap.MajorityHealthy():
+      ToStderr("Master-failover with voting is only possible if the majority"
+               " of nodes is still healthy; use the --no-voting option after"
+               " ensuring by other means that you won't end up in a dual-master"
+               " scenario.")
+      return 1
   if opts.no_voting and not opts.yes_do_it:
     usertext = ("This will perform the failover even if most other nodes"
                 " are down, or if this node is outdated. This is dangerous"
@@ -2119,7 +2127,7 @@ def _UpgradeBeforeConfigurationChange(versionstring):
   rollback.append(
     lambda: utils.RunCmd(["rm", "-f", pathutils.INTENT_TO_UPGRADE]))
 
-  ToStdout("Draining queue")
+  ToStdoutAndLoginfo("Draining queue")
   client = GetClient()
   client.SetQueueDrainFlag(True)
 
@@ -2131,11 +2139,11 @@ def _UpgradeBeforeConfigurationChange(versionstring):
     ToStderr("Failed to completely empty the queue.")
     return (False, rollback)
 
-  ToStdout("Pausing the watcher for one hour.")
+  ToStdoutAndLoginfo("Pausing the watcher for one hour.")
   rollback.append(lambda: GetClient().SetWatcherPause(None))
   GetClient().SetWatcherPause(time.time() + 60 * 60)
 
-  ToStdout("Stopping daemons on master node.")
+  ToStdoutAndLoginfo("Stopping daemons on master node.")
   if not _RunCommandAndReport([pathutils.DAEMON_UTIL, "stop-all"]):
     return (False, rollback)
 
@@ -2143,7 +2151,7 @@ def _UpgradeBeforeConfigurationChange(versionstring):
     utils.RunCmd([pathutils.DAEMON_UTIL, "start-all"])
     return (False, rollback)
 
-  ToStdout("Stopping daemons everywhere.")
+  ToStdoutAndLoginfo("Stopping daemons everywhere.")
   rollback.append(lambda: _VerifyCommand([pathutils.DAEMON_UTIL, "start-all"]))
   badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "stop-all"])
   if badnodes:
@@ -2151,7 +2159,7 @@ def _UpgradeBeforeConfigurationChange(versionstring):
     return (False, rollback)
 
   backuptar = os.path.join(pathutils.BACKUP_DIR, "ganeti%d.tar" % time.time())
-  ToStdout("Backing up configuration as %s" % backuptar)
+  ToStdoutAndLoginfo("Backing up configuration as %s", backuptar)
   if not _RunCommandAndReport(["mkdir", "-p", pathutils.BACKUP_DIR]):
     return (False, rollback)
 
@@ -2179,7 +2187,7 @@ def _VersionSpecificDowngrade():
 
   @return: True upon success
   """
-  ToStdout("Performing version-specific downgrade tasks.")
+  ToStdoutAndLoginfo("Performing version-specific downgrade tasks.")
 
   return True
 
@@ -2200,7 +2208,7 @@ def _SwitchVersionAndConfig(versionstring, downgrade):
   """
   rollback = []
   if downgrade:
-    ToStdout("Downgrading configuration")
+    ToStdoutAndLoginfo("Downgrading configuration")
     if not _RunCommandAndReport([pathutils.CFGUPGRADE, "--downgrade", "-f"]):
       return (False, rollback)
     # Note: version specific downgrades need to be done before switching
@@ -2212,7 +2220,7 @@ def _SwitchVersionAndConfig(versionstring, downgrade):
   # Configuration change is the point of no return. From then onwards, it is
   # safer to push through the up/dowgrade than to try to roll it back.
 
-  ToStdout("Switching to version %s on all nodes" % versionstring)
+  ToStdoutAndLoginfo("Switching to version %s on all nodes", versionstring)
   rollback.append(lambda: _SetGanetiVersion(constants.DIR_VERSION))
   badnodes = _SetGanetiVersion(versionstring)
   if badnodes:
@@ -2227,7 +2235,7 @@ def _SwitchVersionAndConfig(versionstring, downgrade):
   # commands using their canonical (version independent) path.
 
   if not downgrade:
-    ToStdout("Upgrading configuration")
+    ToStdoutAndLoginfo("Upgrading configuration")
     if not _RunCommandAndReport([pathutils.CFGUPGRADE, "-f"]):
       return (False, rollback)
 
@@ -2252,24 +2260,24 @@ def _UpgradeAfterConfigurationChange(oldversion):
   """
   returnvalue = 0
 
-  ToStdout("Ensuring directories everywhere.")
+  ToStdoutAndLoginfo("Ensuring directories everywhere.")
   badnodes = _VerifyCommand([pathutils.ENSURE_DIRS])
   if badnodes:
     ToStderr("Warning: failed to ensure directories on %s." %
              (", ".join(badnodes)))
     returnvalue = 1
 
-  ToStdout("Starting daemons everywhere.")
+  ToStdoutAndLoginfo("Starting daemons everywhere.")
   badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "start-all"])
   if badnodes:
     ToStderr("Warning: failed to start daemons on %s." % (", ".join(badnodes),))
     returnvalue = 1
 
-  ToStdout("Redistributing the configuration.")
+  ToStdoutAndLoginfo("Redistributing the configuration.")
   if not _RunCommandAndReport(["gnt-cluster", "redist-conf", "--yes-do-it"]):
     returnvalue = 1
 
-  ToStdout("Restarting daemons everywhere.")
+  ToStdoutAndLoginfo("Restarting daemons everywhere.")
   badnodes = _VerifyCommand([pathutils.DAEMON_UTIL, "stop-all"])
   badnodes.extend(_VerifyCommand([pathutils.DAEMON_UTIL, "start-all"]))
   if badnodes:
@@ -2277,21 +2285,21 @@ def _UpgradeAfterConfigurationChange(oldversion):
              (", ".join(list(set(badnodes))),))
     returnvalue = 1
 
-  ToStdout("Undraining the queue.")
+  ToStdoutAndLoginfo("Undraining the queue.")
   if not _RunCommandAndReport(["gnt-cluster", "queue", "undrain"]):
     returnvalue = 1
 
   _RunCommandAndReport(["rm", "-f", pathutils.INTENT_TO_UPGRADE])
 
-  ToStdout("Running post-upgrade hooks")
+  ToStdoutAndLoginfo("Running post-upgrade hooks")
   if not _RunCommandAndReport([pathutils.POST_UPGRADE, oldversion]):
     returnvalue = 1
 
-  ToStdout("Unpausing the watcher.")
+  ToStdoutAndLoginfo("Unpausing the watcher.")
   if not _RunCommandAndReport(["gnt-cluster", "watcher", "continue"]):
     returnvalue = 1
 
-  ToStdout("Verifying cluster.")
+  ToStdoutAndLoginfo("Verifying cluster.")
   if not _RunCommandAndReport(["gnt-cluster", "verify"]):
     returnvalue = 1
 
@@ -2330,6 +2338,8 @@ def UpgradeGanetiCommand(opts, args):
                  " finish it first" % (oldversion, versionstring))
         return 1
 
+  utils.SetupLogging(pathutils.LOG_COMMANDS, 'gnt-cluster upgrade', debug=1)
+
   oldversion = constants.RELEASE_VERSION
 
   if opts.resume:
index 911203e..a35e95c 100644 (file)
@@ -399,11 +399,22 @@ class LUInstanceSetParams(LogicalUnit):
                                  errors.ECODE_INVAL)
 
     instance_nodes = self.cfg.GetInstanceNodes(self.instance.uuid)
-    if not set(instance_nodes).issubset(set(disk.nodes)):
+    # Make sure we do not attach disks to instances on wrong nodes. If the
+    # instance is diskless, that instance is associated only to the primary
+    # node, whereas the disk can be associated to two nodes in the case of DRBD,
+    # hence, we have a subset check here.
+    if disk.nodes and not set(instance_nodes).issubset(set(disk.nodes)):
       raise errors.OpPrereqError("Disk nodes are %s while the instance's nodes"
                                  " are %s" %
                                  (disk.nodes, instance_nodes),
                                  errors.ECODE_INVAL)
+    # Make sure a DRBD disk has the same primary node as the instance where it
+    # will be attached to.
+    disk_primary = disk.GetPrimaryNode(self.instance.primary_node)
+    if self.instance.primary_node != disk_primary:
+      raise errors.OpExecError("The disks' primary node is %s whereas the "
+                               "instance's primary node is %s."
+                               % (disk_primary, self.instance.primary_node))
 
   def ExpandNames(self):
     self._ExpandAndLockInstance()
index 340ddb1..4df0246 100644 (file)
@@ -1668,11 +1668,12 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       kvm_cmd.extend(["-qmp", "unix:%s,server,nowait" %
                       self._InstanceKvmdMonitor(instance.name)])
 
-    # Configure the network now for starting instances and bridged interfaces,
-    # during FinalizeMigration for incoming instances' routed interfaces
+    # Configure the network now for starting instances and bridged/OVS
+    # interfaces, during FinalizeMigration for incoming instances' routed
+    # interfaces.
     for nic_seq, nic in enumerate(kvm_nics):
       if (incoming and
-          nic.nicparams[constants.NIC_MODE] != constants.NIC_MODE_BRIDGED):
+          nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_ROUTED):
         continue
       self._ConfigureNIC(instance, nic_seq, nic, taps[nic_seq])
 
@@ -2136,8 +2137,8 @@ class KVMHypervisor(hv_base.BaseHypervisor):
       kvm_nics = kvm_runtime[1]
 
       for nic_seq, nic in enumerate(kvm_nics):
-        if nic.nicparams[constants.NIC_MODE] == constants.NIC_MODE_BRIDGED:
-          # Bridged interfaces have already been configured
+        if nic.nicparams[constants.NIC_MODE] != constants.NIC_MODE_ROUTED:
+          # Bridged/OVS interfaces have already been configured
           continue
         try:
           tap = utils.ReadFile(self._InstanceNICFile(instance.name, nic_seq))
index 633353d..96e7092 100644 (file)
@@ -675,6 +675,17 @@ class Disk(ConfigObject):
       raise errors.ProgrammerError("Unhandled device type %s" % self.dev_type)
     return result
 
+  def GetPrimaryNode(self, node_uuid):
+    """This function returns the primary node of the device.
+
+    If the device is not a DRBD device, we still return the node the device
+    lives on.
+
+    """
+    if self.dev_type in constants.DTS_DRBD:
+      return self.logical_id[0]
+    return node_uuid
+
   def ComputeNodeTree(self, parent_node_uuid):
     """Compute the node/disk tree for this disk and its children.
 
index a04d50c..f34677a 100644 (file)
@@ -645,15 +645,22 @@ over the master role in a 2 node cluster with the original master
 down). If the original master then comes up, it won't be able to
 start its master daemon because it won't have enough votes, but so
 won't the new master, if the master daemon ever needs a restart.
-You can pass ``--no-voting`` to **ganeti-masterd** on the new
-master to solve this problem, and run **gnt-cluster redist-conf**
-to make sure the cluster is consistent again.
+You can pass ``--no-voting`` to **ganeti-luxid** and **ganeti-wconfd**
+on the new master to solve this problem, and run
+**gnt-cluster redist-conf** to make sure the cluster is consistent
+again.
 
 The option ``--yes-do-it`` is used together with ``--no-voting``, for
 skipping the interactive checks. This is even more dangerous, and should
 only be used in conjunction with other means (e.g. a HA suite) to
 confirm that the operation is indeed safe.
 
+Note that in order for remote node agreement checks to work, a strict
+majority of nodes still needs to be functional. To avoid situations with
+daemons not starting up on the new master, master-failover without
+the ``--no-voting`` option verifies a healthy majority of nodes and refuses
+the operation otherwise.
+
 MASTER-PING
 ~~~~~~~~~~~
 
index b930a43..e7f46e2 100644 (file)
@@ -114,10 +114,10 @@ genOnlineNode :: Gen Node.Node
 genOnlineNode =
   arbitrary `suchThat` (\n -> not (Node.offline n) &&
                               not (Node.failN1 n) &&
-                              Node.availDisk n > 0 &&
-                              Node.availMem n > 0 &&
-                              Node.availCpu n > 0 &&
-                              Node.tSpindles n > 0)
+                              Node.availDisk n > 2 * Types.unitDsk &&
+                              Node.availMem n > 2 * Types.unitMem &&
+                              Node.availCpu n > 2 &&
+                              Node.tSpindles n > 2)
 
 -- | Helper function to generate a sane empty node with consistent
 -- internal data.
index e1ddbb5..1a4a45b 100644 (file)
@@ -2968,6 +2968,73 @@ class TestLUInstanceSetParams(CmdlibTestCase):
     self.ExecOpCode(op)
     self.assertEqual([disk], self.cfg.GetInstanceDisks(inst.uuid))
 
+  def testDetachAttachDrbdDiskWithWrongPrimaryNode(self):
+    """Check if disk attachment with a wrong primary node fails.
+
+    """
+    disk1 = self.cfg.CreateDisk(uuid=self.mocked_disk_uuid,
+                               primary_node=self.master.uuid,
+                               secondary_node=self.snode.uuid,
+                               dev_type=constants.DT_DRBD8)
+
+    inst1 = self.cfg.AddNewInstance(disks=[disk1], primary_node=self.master,
+                                    secondary_node=self.snode)
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst1.name,
+                         disks=[[constants.DDM_DETACH,
+                         self.mocked_disk_uuid, {}]])
+
+    self.ExecOpCode(op)
+    self.assertEqual([], self.cfg.GetInstanceDisks(inst1.uuid))
+
+    disk2 = self.cfg.CreateDisk(uuid="mock_uuid_1135",
+                               primary_node=self.snode.uuid,
+                               secondary_node=self.master.uuid,
+                               dev_type=constants.DT_DRBD8)
+
+    inst2 = self.cfg.AddNewInstance(disks=[disk2], primary_node=self.snode,
+                                    secondary_node=self.master)
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst2.name,
+                         disks=[[constants.DDM_ATTACH, 0,
+                                 {
+                                   'uuid': self.mocked_disk_uuid
+                                 }]])
+
+    self.assertRaises(errors.OpExecError, self.ExecOpCode, op)
+
+
+  def testDetachAttachExtDisk(self):
+    """Check attach/detach functionality of ExtStorage disks.
+
+    """
+    disk = self.cfg.CreateDisk(uuid=self.mocked_disk_uuid,
+                               dev_type=constants.DT_EXT,
+                               params={
+                                 constants.IDISK_PROVIDER: "pvdr"
+                               })
+
+    inst = self.cfg.AddNewInstance(disks=[disk], primary_node=self.master)
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         disks=[[constants.DDM_DETACH,
+                         self.mocked_disk_uuid, {}]])
+
+    self.ExecOpCode(op)
+    self.assertEqual([], self.cfg.GetInstanceDisks(inst.uuid))
+
+    op = self.CopyOpCode(self.op,
+                         instance_name=inst.name,
+                         disks=[[constants.DDM_ATTACH, 0,
+                                 {
+                                   'uuid': self.mocked_disk_uuid
+                                 }]])
+    self.ExecOpCode(op)
+    self.assertEqual([disk], self.cfg.GetInstanceDisks(inst.uuid))
+
   def testRemoveDiskRemovesStorageDir(self):
     inst = self.cfg.AddNewInstance(disks=[self.cfg.CreateDisk(dev_type='file')])
     op = self.CopyOpCode(self.op,
index 530124b..4d673e0 100644 (file)
@@ -55,7 +55,7 @@ def main():
 
   if utils.version.IsBefore(version, 2, 12, 5):
     result = utils.RunCmd(["gnt-cluster", "renew-crypto",
-                           "--new-node-certificates", "-f"])
+                           "--new-node-certificates", "-f", "-d"])
     if result.failed:
       cli.ToStderr("Failed to create node certificates: %s; Output %s" %
                    (result.fail_reason, result.output))
@@ -63,7 +63,8 @@ def main():
 
   if utils.version.IsBefore(version, 2, 13, 0):
     result = utils.RunCmd(["gnt-cluster", "renew-crypto",
-                           "--new-ssh-keys", "--no-ssh-key-check", "-f"])
+                           "--new-ssh-keys", "--no-ssh-key-check", "-f", "-d"])
+
     if result.failed:
       cli.ToStderr("Failed to create SSH keys: %s; Output %s" %
                    (result.fail_reason, result.output))