gnt-node add: Use prepare-node-join
authorMichael Hanselmann <hansmi@google.com>
Tue, 23 Oct 2012 18:16:25 +0000 (20:16 +0200)
committerMichael Hanselmann <hansmi@google.com>
Fri, 26 Oct 2012 14:27:25 +0000 (16:27 +0200)
This patch changes “gnt-node add” to use the newly added
“prepare-node-join” tool. Hereby Paramiko is no longer a hard dependency
for setting up SSH on nodes.

In “gnt_cluster.py”, a positional parameter is no longer passed as a
keyword parameter.

Signed-off-by: Michael Hanselmann <hansmi@google.com>
Reviewed-by: Iustin Pop <iustin@google.com>

NEWS
lib/client/gnt_cluster.py
lib/client/gnt_node.py
lib/pathutils.py
man/gnt-node.rst
tools/cluster-merge

diff --git a/NEWS b/NEWS
index 7571ff4..b411cb1 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -25,6 +25,10 @@ Version 2.7.0 beta1
 - The parsing of the variants file for OSes (see
   :manpage:`ganeti-os-interface(8)` has been slightly changed: now empty
   lines and comment lines are ignored for better readability.
+- The ``setup-ssh`` tool added in Ganeti 2.2 has been replaced.
+  ``gnt-node add`` now invokes a new tool on the destination node, named
+  ``prepare-node-join``, to configure the SSH daemon. Paramiko is no
+  longer necessary to configure nodes' SSH daemons via ``gnt-node add``.
 
 
 Version 2.6.1
index b71f5ac..06bdaf6 100644 (file)
@@ -503,7 +503,7 @@ def ClusterCopyFile(opts, args):
                            secondary_ips=opts.use_replication_network,
                            nodegroup=opts.nodegroup)
 
-  srun = ssh.SshRunner(cluster_name=cluster_name)
+  srun = ssh.SshRunner(cluster_name)
   for node in results:
     if not srun.CopyFileToNode(node, filename):
       ToStderr("Copy of file %s to node %s failed", filename, node)
index 86cdf0a..7c8add0 100644 (file)
@@ -27,6 +27,8 @@
 # C0103: Invalid name gnt-node
 
 import itertools
+import errno
+import tempfile
 
 from ganeti.cli import *
 from ganeti import cli
@@ -37,6 +39,8 @@ from ganeti import constants
 from ganeti import errors
 from ganeti import netutils
 from ganeti import pathutils
+from ganeti import serializer
+from ganeti import ssh
 from cStringIO import StringIO
 
 from ganeti import confd
@@ -134,37 +138,111 @@ def ConvertStorageType(user_storage_type):
                                errors.ECODE_INVAL)
 
 
-def _RunSetupSSH(options, nodes):
-  """Wrapper around utils.RunCmd to call setup-ssh
+def _TryReadFile(path):
+  """Tries to read a file.
 
-  @param options: The command line options
-  @param nodes: The nodes to setup
+  If the file is not found, C{None} is returned.
+
+  @type path: string
+  @param path: Filename
+  @rtype: None or string
+  @todo: Consider adding a generic ENOENT wrapper
 
   """
+  try:
+    return utils.ReadFile(path)
+  except EnvironmentError, err:
+    if err.errno == errno.ENOENT:
+      return None
+    else:
+      raise
+
 
-  assert nodes, "Empty node list"
+def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr):
+  """Reads SSH keys according to C{keyfiles}.
+
+  @type keyfiles: dict
+  @param keyfiles: Dictionary with keys of L{constants.SSHK_ALL} and two-values
+    tuples (private and public key file)
+  @rtype: list
+  @return: List of three-values tuples (L{constants.SSHK_ALL}, private and
+    public key as strings)
+
+  """
+  result = []
 
-  cmd = [pathutils.SETUP_SSH]
+  for (kind, (private_file, public_file)) in keyfiles.items():
+    private_key = _TryReadFile(private_file)
+    public_key = _TryReadFile(public_file)
 
-  # Pass --debug|--verbose to the external script if set on our invocation
-  # --debug overrides --verbose
+    if public_key and private_key:
+      result.append((kind, private_key, public_key))
+    elif public_key or private_key:
+      _tostderr_fn("Couldn't find a complete set of keys for kind '%s'; files"
+                   " '%s' and '%s'", kind, private_file, public_file)
+
+  return result
+
+
+def _SetupSSH(options, cluster_name, node):
+  """Configures a destination node's SSH daemon.
+
+  @param options: Command line options
+  @type cluster_name
+  @param cluster_name: Cluster name
+  @type node: string
+  @param node: Destination node name
+
+  """
+  if options.force_join:
+    ToStderr("The \"--force-join\" option is no longer supported and will be"
+             " ignored.")
+
+  cmd = [pathutils.PREPARE_NODE_JOIN]
+
+  # Pass --debug/--verbose to the external script if set on our invocation
   if options.debug:
     cmd.append("--debug")
-  elif options.verbose:
+
+  if options.verbose:
     cmd.append("--verbose")
-  if not options.ssh_key_check:
-    cmd.append("--no-ssh-key-check")
-  if options.force_join:
-    cmd.append("--force-join")
 
-  cmd.extend(nodes)
+  host_keys = _ReadSshKeys(constants.SSH_DAEMON_KEYFILES)
+
+  (_, root_keyfiles) = \
+    ssh.GetAllUserFiles(constants.SSH_LOGIN_USER, mkdir=False, dircheck=False)
+
+  root_keys = _ReadSshKeys(root_keyfiles)
+
+  (_, cert_pem) = \
+    utils.ExtractX509Certificate(utils.ReadFile(pathutils.NODED_CERT_FILE))
+
+  data = {
+    constants.SSHS_CLUSTER_NAME: cluster_name,
+    constants.SSHS_NODE_DAEMON_CERTIFICATE: cert_pem,
+    constants.SSHS_SSH_HOST_KEY: host_keys,
+    constants.SSHS_SSH_ROOT_KEY: root_keys,
+    }
+
+  srun = ssh.SshRunner(cluster_name)
+  scmd = srun.BuildCmd(node, constants.SSH_LOGIN_USER,
+                       utils.ShellQuoteArgs(cmd),
+                       batch=False, ask_key=options.ssh_key_check,
+                       strict_host_check=options.ssh_key_check, quiet=False,
+                       use_cluster_key=False)
+
+  tempfh = tempfile.TemporaryFile()
+  try:
+    tempfh.write(serializer.DumpJson(data))
+    tempfh.seek(0)
 
-  result = utils.RunCmd(cmd, interactive=True)
+    result = utils.RunCmd(scmd, interactive=True, input_fd=tempfh)
+  finally:
+    tempfh.close()
 
   if result.failed:
-    errmsg = ("Command '%s' failed with exit code %s; output %r" %
-              (result.cmd, result.exit_code, result.output))
-    raise errors.OpExecError(errmsg)
+    raise errors.OpExecError("Command '%s' failed: %s" %
+                             (result.cmd, result.fail_reason))
 
 
 @UsesRPC
@@ -206,8 +284,7 @@ def AddNode(opts, args):
     sip = opts.secondary_ip
 
   # read the cluster name from the master
-  output = cl.QueryConfigValues(["cluster_name"])
-  cluster_name = output[0]
+  (cluster_name, ) = cl.QueryConfigValues(["cluster_name"])
 
   if not readd and opts.node_setup:
     ToStderr("-- WARNING -- \n"
@@ -218,7 +295,7 @@ def AddNode(opts, args):
              "and grant full intra-cluster ssh root access to/from it\n", node)
 
   if opts.node_setup:
-    _RunSetupSSH(opts, [node])
+    _SetupSSH(opts, cluster_name, node)
 
   bootstrap.SetupNodeDaemon(cluster_name, node, opts.ssh_key_check)
 
index bb1371a..fba77d8 100644 (file)
@@ -44,6 +44,7 @@ IMPORT_EXPORT_DAEMON = _autoconf.PKGLIBDIR + "/import-export"
 KVM_CONSOLE_WRAPPER = _autoconf.PKGLIBDIR + "/tools/kvm-console-wrapper"
 KVM_IFUP = _autoconf.PKGLIBDIR + "/kvm-ifup"
 SETUP_SSH = _autoconf.TOOLSDIR + "/setup-ssh"
+PREPARE_NODE_JOIN = _autoconf.PKGLIBDIR + "/prepare-node-join"
 XM_CONSOLE_WRAPPER = _autoconf.PKGLIBDIR + "/tools/xm-console-wrapper"
 ETC_HOSTS = vcluster.ETC_HOSTS
 
index 9d05836..5e18c26 100644 (file)
@@ -52,10 +52,6 @@ secondary IP again, it will reused from the cluster. Also, the
 drained and offline flags of the node will be cleared before
 re-adding it.
 
-The ``--force-join`` option is to proceed with adding a node even if it already
-appears to belong to another cluster. This is used during cluster merging, for
-example.
-
 The ``-g (--node-group)`` option is used to add the new node into a
 specific node group, specified by UUID or name. If only one node group
 exists you can skip this option, otherwise it's mandatory.
index 1ff4b64..d45d381 100755 (executable)
@@ -651,7 +651,7 @@ class Merger(object):
       for node in data.nodes:
         logging.info("Readding node %s", node)
         result = utils.RunCmd(["gnt-node", "add", "--readd",
-                               "--no-ssh-key-check", "--force-join", node])
+                               "--no-ssh-key-check", node])
         if result.failed:
           logging.error("%s failed to be readded. Reason: %s, output: %s",
                          node, result.fail_reason, result.output)