X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/00003458e2096287409ba98df66b6385d9d2caa7..68676a00a95239fcdd935758b7f6144cbec8b0fc:/lib/ssh.py diff --git a/lib/ssh.py b/lib/ssh.py index 55a3292..6f32e56 100644 --- a/lib/ssh.py +++ b/lib/ssh.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +# # # Copyright (C) 2006, 2007 Google Inc. @@ -30,10 +30,7 @@ from ganeti import logger from ganeti import utils from ganeti import errors from ganeti import constants - - -__all__ = ["SSHCall", "CopyFileToNode", "VerifyNodeHostname", - "KNOWN_HOSTS_OPTS", "BATCH_MODE_OPTS", "ASK_KEY_OPTS"] +from ganeti import ssconf KNOWN_HOSTS_OPTS = [ @@ -43,135 +40,195 @@ KNOWN_HOSTS_OPTS = [ # Note: BATCH_MODE conflicts with ASK_KEY BATCH_MODE_OPTS = [ - "-oEscapeChar=none", "-oBatchMode=yes", + "-oEscapeChar=none", "-oStrictHostKeyChecking=yes", ] ASK_KEY_OPTS = [ - "-oStrictHostKeyChecking=ask", "-oEscapeChar=none", "-oHashKnownHosts=no", + "-oStrictHostKeyChecking=ask", ] -def BuildSSHCmd(hostname, user, command, batch=True, ask_key=False): - """Build an ssh string to execute a command on a remote node. - Args: - hostname: the target host, string - user: user to auth as - command: the command - batch: if true, ssh will run in batch mode with no prompting - ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that - we can connect to an unknown host (not valid in batch mode) +def GetUserFiles(user, mkdir=False): + """Return the paths of a user's ssh files. - Returns: - The ssh call to run 'command' on the remote host. + The function will return a triplet (priv_key_path, pub_key_path, + auth_key_path) that are used for ssh authentication. Currently, the + keys used are DSA keys, so this function will return: + (~user/.ssh/id_dsa, ~user/.ssh/id_dsa.pub, + ~user/.ssh/authorized_keys). - """ - argv = ["ssh", "-q"] - argv.extend(KNOWN_HOSTS_OPTS) - if batch: - # if we are in batch mode, we can't ask the key - if ask_key: - raise errors.ProgrammerError("SSH call requested conflicting options") - argv.extend(BATCH_MODE_OPTS) - elif ask_key: - argv.extend(ASK_KEY_OPTS) - argv.extend(["%s@%s" % (user, hostname), "'%s'" % command]) - return argv - - -def SSHCall(hostname, user, command, batch=True, ask_key=False): - """Execute a command on a remote node. - - This method has the same return value as `utils.RunCmd()`, which it - uses to launch ssh. - - Args: - hostname: the target host, string - user: user to auth as - command: the command - batch: if true, ssh will run in batch mode with no prompting - ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that - we can connect to an unknown host (not valid in batch mode) - - Returns: - `utils.RunResult` as for `utils.RunCmd()` + If the optional parameter mkdir is True, the ssh directory will be + created if it doesn't exist. + + Regardless of the mkdir parameters, the script will raise an error + if ~user/.ssh is not a directory. """ - return utils.RunCmd(BuildSSHCmd(hostname, user, command, batch=batch, ask_key=ask_key)) + user_dir = utils.GetHomeDir(user) + if not user_dir: + raise errors.OpExecError("Cannot resolve home of user %s" % user) + ssh_dir = os.path.join(user_dir, ".ssh") + if not os.path.lexists(ssh_dir): + if mkdir: + try: + os.mkdir(ssh_dir, 0700) + except EnvironmentError, err: + raise errors.OpExecError("Can't create .ssh dir for user %s: %s" % + (user, str(err))) + elif not os.path.isdir(ssh_dir): + raise errors.OpExecError("path ~%s/.ssh is not a directory" % user) -def CopyFileToNode(node, filename): - """Copy a file to another node with scp. + return [os.path.join(ssh_dir, base) + for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]] - Args: - node: node in the cluster - filename: absolute pathname of a local file - Returns: - success: True/False +class SshRunner: + """Wrapper for SSH commands. """ - if not os.path.isfile(filename): - logger.Error("file %s does not exist" % (filename)) - return False + def __init__(self, sstore=None): + if sstore is None: + self.sstore = ssconf.SimpleStore() + else: + self.sstore = sstore + + def _GetHostKeyAliasOption(self): + return "-oHostKeyAlias=%s" % self.sstore.GetClusterName() + + def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, + tty=False): + """Build an ssh command to execute a command on a remote node. + + Args: + hostname: the target host, string + user: user to auth as + command: the command + batch: if true, ssh will run in batch mode with no prompting + ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that + we can connect to an unknown host (not valid in batch mode) + + Returns: + The ssh call to run 'command' on the remote host. + + """ + argv = [constants.SSH, "-q"] + argv.extend(KNOWN_HOSTS_OPTS) + argv.append(self._GetHostKeyAliasOption()) + if batch: + # if we are in batch mode, we can't ask the key + if ask_key: + raise errors.ProgrammerError("SSH call requested conflicting options") + argv.extend(BATCH_MODE_OPTS) + elif ask_key: + argv.extend(ASK_KEY_OPTS) + if tty: + argv.append("-t") + argv.extend(["%s@%s" % (user, hostname), command]) + return argv + + def Run(self, hostname, user, command, batch=True, ask_key=False): + """Runs a command on a remote node. + + This method has the same return value as `utils.RunCmd()`, which it + uses to launch ssh. + + Args: + hostname: the target host, string + user: user to auth as + command: the command + batch: if true, ssh will run in batch mode with no prompting + ask_key: if true, ssh will run with StrictHostKeyChecking=ask, so that + we can connect to an unknown host (not valid in batch mode) + + Returns: + `utils.RunResult` like `utils.RunCmd()` + + """ + return utils.RunCmd(self.BuildCmd(hostname, user, command, batch=batch, + ask_key=ask_key)) + + def CopyFileToNode(self, node, filename): + """Copy a file to another node with scp. + + Args: + node: node in the cluster + filename: absolute pathname of a local file + + Returns: + success: True/False - if not os.path.isabs(filename): - logger.Error("file %s must be an absolute path" % (filename)) - return False + """ + if not os.path.isabs(filename): + logger.Error("file %s must be an absolute path" % (filename)) + return False - command = ["scp", "-q", "-p"] - command.extend(KNOWN_HOSTS_OPTS) - command.extend(BATCH_MODE_OPTS) - command.append(filename) - command.append("%s:%s" % (node, filename)) + if not os.path.isfile(filename): + logger.Error("file %s does not exist" % (filename)) + return False - result = utils.RunCmd(command) + command = [constants.SCP, "-q", "-p"] + command.extend(KNOWN_HOSTS_OPTS) + command.extend(BATCH_MODE_OPTS) + command.append(self._GetHostKeyAliasOption()) + command.append(filename) + command.append("%s:%s" % (node, filename)) - if result.failed: - logger.Error("copy to node %s failed (%s) error %s," - " command was %s" % - (node, result.fail_reason, result.output, result.cmd)) + result = utils.RunCmd(command) - return not result.failed + if result.failed: + logger.Error("copy to node %s failed (%s) error %s," + " command was %s" % + (node, result.fail_reason, result.output, result.cmd)) + return not result.failed -def VerifyNodeHostname(node): - """Verify hostname consistency via SSH. + def VerifyNodeHostname(self, node): + """Verify hostname consistency via SSH. + This functions connects via ssh to a node and compares the hostname + reported by the node to the name with have (the one that we + connected to). - This functions connects via ssh to a node and compares the hostname - reported by the node to the name with have (the one that we - connected to). + This is used to detect problems in ssh known_hosts files + (conflicting known hosts) and incosistencies between dns/hosts + entries and local machine names - This is used to detect problems in ssh known_hosts files - (conflicting known hosts) and incosistencies between dns/hosts - entries and local machine names + Args: + node: nodename of a host to check. can be short or full qualified hostname - Args: - node: nodename of a host to check. can be short or full qualified hostname + Returns: + (success, detail) + where + success: True/False + detail: String with details - Returns: - (success, detail) - where - success: True/False - detail: String with details + """ + retval = self.Run(node, 'root', 'hostname') - """ - retval = SSHCall(node, 'root', 'hostname') + if retval.failed: + msg = "ssh problem" + output = retval.output + if output: + msg += ": %s" % output + return False, msg - if retval.failed: - msg = "ssh problem" - output = retval.output - if output: - msg += ": %s" % output - return False, msg + remotehostname = retval.stdout.strip() - remotehostname = retval.stdout.strip() + if not remotehostname or remotehostname != node: + return False, "hostname mismatch, got %s" % remotehostname - if not remotehostname or remotehostname != node: - return False, "hostname mismatch, got %s" % remotehostname + return True, "host matches" - return True, "host matches" + +def WriteKnownHostsFile(cfg, sstore, file_name): + """Writes the cluster-wide equally known_hosts file. + + """ + utils.WriteFile(file_name, mode=0700, + data="%s ssh-rsa %s\n" % (sstore.GetClusterName(), + cfg.GetHostKey()))