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. --- NEWS | 4 ++ lib/client/gnt_cluster.py | 2 +- lib/client/gnt_node.py | 101 +++++++++++++++++++++++++++++++++++---------- lib/pathutils.py | 1 + man/gnt-node.rst | 4 -- tools/cluster-merge | 2 +- 6 files changed, 86 insertions(+), 28 deletions(-) diff --git a/NEWS b/NEWS index 7571ff4..b411cb1 100644 --- 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 diff --git a/lib/client/gnt_cluster.py b/lib/client/gnt_cluster.py index b71f5ac..06bdaf6 100644 --- a/lib/client/gnt_cluster.py +++ b/lib/client/gnt_cluster.py @@ -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) diff --git a/lib/client/gnt_node.py b/lib/client/gnt_node.py index 86cdf0a..b4b537e 100644 --- a/lib/client/gnt_node.py +++ b/lib/client/gnt_node.py @@ -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,91 @@ def ConvertStorageType(user_storage_type): errors.ECODE_INVAL) -def _RunSetupSSH(options, nodes): - """Wrapper around utils.RunCmd to call setup-ssh +def _TryReadFile(path): + try: + return utils.ReadFile(path) + except EnvironmentError, err: + if err.errno == errno.ENOENT: + return None + else: + raise - @param options: The command line options - @param nodes: The nodes to setup - """ +def _ReadSshKeys(keyfiles, _tostderr_fn=ToStderr): + result = [] + + for (kind, (private_file, public_file)) in keyfiles.items(): + private_key = _TryReadFile(private_file) + public_key = _TryReadFile(public_file) + + 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) - assert nodes, "Empty node list" + 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.SETUP_SSH] + cmd = [pathutils.PREPARE_NODE_JOIN] - # Pass --debug|--verbose to the external script if set on our invocation - # --debug overrides --verbose + # 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 +264,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 +275,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) diff --git a/lib/pathutils.py b/lib/pathutils.py index bb1371a..fba77d8 100644 --- a/lib/pathutils.py +++ b/lib/pathutils.py @@ -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 diff --git a/man/gnt-node.rst b/man/gnt-node.rst index 9d05836..5e18c26 100644 --- a/man/gnt-node.rst +++ b/man/gnt-node.rst @@ -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. diff --git a/tools/cluster-merge b/tools/cluster-merge index 1ff4b64..d45d381 100755 --- a/tools/cluster-merge +++ b/tools/cluster-merge @@ -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) -- 1.7.7.3