From 05cd934d9225641f6ea3282036eaa629cce35361 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ren=C3=A9=20Nussbaumer?= <rn@google.com>
Date: Tue, 13 Jul 2010 11:38:36 +0200
Subject: [PATCH] Adding tool to setup SSH on a remote host
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This prepares the remote node to be joined into a cluster

Signed-off-by: RenΓ© Nussbaumer <rn@google.com>
Reviewed-by: Michael Hanselmann <hansmi@google.com>
---
 Makefile.am     |   1 +
 tools/setup-ssh | 235 ++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 236 insertions(+)
 create mode 100644 tools/setup-ssh

diff --git a/Makefile.am b/Makefile.am
index c4d350bea..be269bda9 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -264,6 +264,7 @@ dist_tools_SCRIPTS = \
 	tools/cluster-merge \
 	tools/lvmstrap \
 	tools/move-instance \
+	tools/setup-ssh \
 	tools/sanitize-config
 
 pkglib_python_scripts = \
diff --git a/tools/setup-ssh b/tools/setup-ssh
new file mode 100644
index 000000000..4ff862c37
--- /dev/null
+++ b/tools/setup-ssh
@@ -0,0 +1,235 @@
+#!/usr/bin/python
+#
+
+# Copyright (C) 2010 Google Inc.
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; either version 2 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+# General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program; if not, write to the Free Software
+# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
+# 02110-1301, USA.
+
+"""Tool to setup the SSH configuration on a remote node.
+
+This is needed before we can join the node into the cluster.
+
+"""
+
+import getpass
+import logging
+import paramiko
+import os.path
+import optparse
+import sys
+
+from ganeti import cli
+from ganeti import constants
+from ganeti import errors
+from ganeti import netutils
+from ganeti import ssh
+from ganeti import utils
+
+
+class RemoteCommandError(errors.GenericError):
+  """Exception if remote command was not successful.
+
+  """
+
+
+def _RunRemoteCommand(transport, command):
+  """Invokes and wait for the command over SSH.
+
+  @param transport: The paramiko transport instance
+  @param command: The command to be executed
+
+  """
+  chan = transport.open_session()
+  chan.set_combine_stderr(True)
+  output_handler = chan.makefile("r")
+  chan.exec_command(command)
+
+  result = chan.recv_exit_status()
+  msg = output_handler.read()
+
+  out_msg = "'%s' exited with status code %s, output %r" % (command, result,
+                                                            msg)
+
+  # If result is -1 (no exit status provided) we assume it was not successful
+  if result:
+    raise RemoteCommandError(out_msg)
+
+  if msg:
+    logging.info(out_msg)
+
+
+def _InvokeDaemonUtil(transport, command):
+  """Invokes daemon-util on the remote side.
+
+  @param transport: The paramiko transport instance
+  @param command: The daemon-util command to be run
+
+  """
+  _RunRemoteCommand(transport, "%s %s" % (constants.DAEMON_UTIL, command))
+
+
+def _WriteSftpFile(sftp, name, perm, data):
+  """SFTPs data to a remote file.
+
+  @param sftp: A open paramiko SFTP client
+  @param name: The remote file name
+  @param perm: The remote file permission
+  @param data: The data to write
+
+  """
+  remote_file = sftp.open(name, "w")
+  try:
+    sftp.chmod(name, perm)
+    remote_file.write(data)
+  finally:
+    remote_file.close()
+
+
+def SetupSSH(transport):
+  """Sets the SSH up on the other side.
+
+  @param transport: The paramiko transport instance
+
+  """
+  priv_key, pub_key, auth_keys = ssh.GetUserFiles(constants.GANETI_RUNAS)
+  keyfiles = [
+    (constants.SSH_HOST_DSA_PRIV, 0600),
+    (constants.SSH_HOST_DSA_PUB, 0644),
+    (constants.SSH_HOST_RSA_PRIV, 0600),
+    (constants.SSH_HOST_RSA_PUB, 0644),
+    (priv_key, 0600),
+    (pub_key, 0644),
+    ]
+
+  sftp = transport.open_sftp_client()
+
+  filemap = dict((name, (utils.ReadFile(name), perm))
+                 for (name, perm) in keyfiles)
+
+  auth_path = os.path.dirname(auth_keys)
+
+  try:
+    sftp.mkdir(auth_path, 0700)
+  except IOError:
+    # Sadly paramiko doesn't provide errno or similiar
+    # so we can just assume that the path already exists
+    logging.info("Path %s seems already to exist on remote node. Ignore.",
+                 auth_path)
+
+  for name, (data, perm) in filemap.iteritems():
+    _WriteSftpFile(sftp, name, perm, data)
+
+  authorized_keys = sftp.open(auth_keys, "a+")
+  try:
+    # We don't have to close, as the close happened already in AddAuthorizedKey
+    utils.AddAuthorizedKey(authorized_keys, filemap[pub_key][0])
+  finally:
+    authorized_keys.close()
+
+  _InvokeDaemonUtil(transport, "reload-ssh-keys")
+
+
+def SetupNodeDaemon(transport):
+  """Sets the node daemon up on the other side.
+
+  @param transport: The paramiko transport instance
+
+  """
+  noded_cert = utils.ReadFile(constants.NODED_CERT_FILE)
+
+  sftp = transport.open_sftp_client()
+  _WriteSftpFile(sftp, constants.NODED_CERT_FILE, 0400, noded_cert)
+
+  _InvokeDaemonUtil(transport, "start %s" % constants.NODED)
+
+
+def ParseOptions():
+  """Parses options passed to program.
+
+  """
+  program = os.path.basename(sys.argv[0])
+
+  parser = optparse.OptionParser(usage=("%prog [--debug|--verbose] <node>"
+                                        " <node...>"), prog=program)
+  parser.add_option(cli.DEBUG_OPT)
+  parser.add_option(cli.VERBOSE_OPT)
+
+  (options, args) = parser.parse_args()
+
+  return (options, args)
+
+
+def SetupLogging(options):
+  """Sets up the logging.
+
+  @param options: Parsed options
+
+  """
+  fmt = "%(asctime)s: %(threadName)s "
+  if options.debug or options.verbose:
+    fmt += "%(levelname)s "
+  fmt += "%(message)s"
+
+  formatter = logging.Formatter(fmt)
+
+  file_handler = logging.FileHandler(constants.LOG_SETUP_SSH)
+  stderr_handler = logging.StreamHandler()
+  stderr_handler.setFormatter(formatter)
+  file_handler.setFormatter(formatter)
+  file_handler.setLevel(logging.DEBUG)
+
+  if options.debug:
+    stderr_handler.setLevel(logging.NOTSET)
+  elif options.verbose:
+    stderr_handler.setLevel(logging.INFO)
+  else:
+    stderr_handler.setLevel(logging.ERROR)
+
+  # This is the paramiko logger instance
+  paramiko_logger = logging.getLogger("paramiko")
+  root_logger = logging.getLogger("")
+  root_logger.setLevel(logging.NOTSET)
+  root_logger.addHandler(stderr_handler)
+  root_logger.addHandler(file_handler)
+  paramiko_logger.addHandler(file_handler)
+
+
+def main():
+  """Main routine.
+
+  """
+  (options, args) = ParseOptions()
+
+  SetupLogging(options)
+
+  passwd = getpass.getpass(prompt="%s password:" % constants.GANETI_RUNAS)
+
+  for host in args:
+    transport = paramiko.Transport((host, netutils.GetDaemonPort("ssh")))
+    transport.connect(username=constants.GANETI_RUNAS, password=passwd)
+    try:
+      try:
+        SetupSSH(transport)
+        SetupNodeDaemon(transport)
+      except errors.GenericError, err:
+        logging.fatal("While doing setup on host %s an error occured: %s", host,
+                      err)
+    finally:
+      transport.close()
+
+
+if __name__ == "__main__":
+  main()
-- 
GitLab