X-Git-Url: https://code.grnet.gr/git/ganeti-local/blobdiff_plain/7688d0d348f012ebd63312fc976282477444c2a0..247ee81f3e3970506f082530f1eaf8f407bfa85f:/lib/ssh.py diff --git a/lib/ssh.py b/lib/ssh.py index c9cc180..1a3c101 100644 --- a/lib/ssh.py +++ b/lib/ssh.py @@ -1,7 +1,7 @@ # # -# Copyright (C) 2006, 2007 Google Inc. +# Copyright (C) 2006, 2007, 2010, 2011 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 @@ -25,11 +25,25 @@ import os +import logging +import re -from ganeti import logger from ganeti import utils from ganeti import errors from ganeti import constants +from ganeti import netutils + + +def FormatParamikoFingerprint(fingerprint): + """Format paramiko PKey fingerprint. + + @type fingerprint: str + @param fingerprint: PKey fingerprint + @return: The string hex representation of the fingerprint + + """ + assert len(fingerprint) % 2 == 0 + return ":".join(re.findall(r"..", fingerprint.lower())) def GetUserFiles(user, mkdir=False): @@ -52,18 +66,13 @@ def GetUserFiles(user, mkdir=False): 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))) + ssh_dir = utils.PathJoin(user_dir, ".ssh") + if mkdir: + utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)]) elif not os.path.isdir(ssh_dir): - raise errors.OpExecError("path ~%s/.ssh is not a directory" % user) + raise errors.OpExecError("Path %s is not a directory" % ssh_dir) - return [os.path.join(ssh_dir, base) + return [utils.PathJoin(ssh_dir, base) for base in ["id_dsa", "id_dsa.pub", "authorized_keys"]] @@ -71,20 +80,51 @@ class SshRunner: """Wrapper for SSH commands. """ - def __init__(self, cfg): - self.cfg = cfg + def __init__(self, cluster_name, ipv6=False): + """Initializes this class. + + @type cluster_name: str + @param cluster_name: name of the cluster + @type ipv6: bool + @param ipv6: If true, force ssh to use IPv6 addresses only + + """ + self.cluster_name = cluster_name + self.ipv6 = ipv6 def _BuildSshOptions(self, batch, ask_key, use_cluster_key, - strict_host_check): + strict_host_check, private_key=None, quiet=True): + """Builds a list with needed SSH options. + + @param batch: same as ssh's batch option + @param ask_key: allows ssh to ask for key confirmation; this + parameter conflicts with the batch one + @param use_cluster_key: if True, use the cluster name as the + HostKeyAlias name + @param strict_host_check: this makes the host key checking strict + @param private_key: use this private key instead of the default + @param quiet: whether to enable -q to ssh + + @rtype: list + @return: the list of options ready to use in L{utils.process.RunCmd} + + """ options = [ "-oEscapeChar=none", "-oHashKnownHosts=no", "-oGlobalKnownHostsFile=%s" % constants.SSH_KNOWN_HOSTS_FILE, "-oUserKnownHostsFile=/dev/null", + "-oCheckHostIp=no", ] if use_cluster_key: - options.append("-oHostKeyAlias=%s" % self.cfg.GetClusterName()) + options.append("-oHostKeyAlias=%s" % self.cluster_name) + + if quiet: + options.append("-q") + + if private_key: + options.append("-i%s" % private_key) # TODO: Too many boolean options, maybe convert them to more descriptive # constants. @@ -101,36 +141,48 @@ class SshRunner: else: options.append("-oStrictHostKeyChecking=no") - elif ask_key: - options.extend([ - "-oStrictHostKeyChecking=ask", - ]) + else: + # non-batch mode + + if ask_key: + options.append("-oStrictHostKeyChecking=ask") + elif strict_host_check: + options.append("-oStrictHostKeyChecking=yes") + else: + options.append("-oStrictHostKeyChecking=no") + + if self.ipv6: + options.append("-6") return options def BuildCmd(self, hostname, user, command, batch=True, ask_key=False, - tty=False, use_cluster_key=True, strict_host_check=True): + tty=False, use_cluster_key=True, strict_host_check=True, + private_key=None, quiet=True): """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) - use_cluster_key: Whether to expect and use the cluster-global SSH key - strict_host_check: Whether to check the host's SSH key at all - - Returns: - The ssh call to run 'command' on the remote host. + @param hostname: the target host, string + @param user: user to auth as + @param command: the command + @param batch: if true, ssh will run in batch mode with no prompting + @param ask_key: if true, ssh will run with + StrictHostKeyChecking=ask, so that we can connect to an + unknown host (not valid in batch mode) + @param use_cluster_key: whether to expect and use the + cluster-global SSH key + @param strict_host_check: whether to check the host's SSH key at all + @param private_key: use this private key instead of the default + @param quiet: whether to enable -q to ssh + + @return: the ssh call to run 'command' on the remote host. """ - argv = [constants.SSH, "-q"] + argv = [constants.SSH] argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key, - strict_host_check)) + strict_host_check, private_key, + quiet=quiet)) if tty: - argv.append("-t") + argv.extend(["-t", "-t"]) argv.extend(["%s@%s" % (user, hostname), command]) return argv @@ -140,11 +192,10 @@ class SshRunner: This method has the same return value as `utils.RunCmd()`, which it uses to launch ssh. - Args: - See SshRunner.BuildCmd. + Args: see SshRunner.BuildCmd. - Returns: - `utils.RunResult` like `utils.RunCmd()` + @rtype: L{utils.process.RunResult} + @return: the result as from L{utils.process.RunCmd()} """ return utils.RunCmd(self.BuildCmd(*args, **kwargs)) @@ -152,33 +203,35 @@ class SshRunner: 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 + @param node: node in the cluster + @param filename: absolute pathname of a local file - Returns: - success: True/False + @rtype: boolean + @return: the success of the operation """ if not os.path.isabs(filename): - logger.Error("file %s must be an absolute path" % (filename)) + logging.error("File %s must be an absolute path", filename) return False if not os.path.isfile(filename): - logger.Error("file %s does not exist" % (filename)) + logging.error("File %s does not exist", filename) return False - command = [constants.SCP, "-q", "-p"] + command = [constants.SCP, "-p"] command.extend(self._BuildSshOptions(True, False, True, True)) command.append(filename) + if netutils.IP6Address.IsValid(node): + node = netutils.FormatAddress((node, None)) + command.append("%s:%s" % (node, filename)) result = utils.RunCmd(command) if result.failed: - logger.Error("copy to node %s failed (%s) error %s," - " command was %s" % - (node, result.fail_reason, result.output, result.cmd)) + logging.error("Copy to node %s failed (%s) error %s," + " command was %s", + node, result.fail_reason, result.output, result.cmd) return not result.failed @@ -190,32 +243,38 @@ class SshRunner: connected to). This is used to detect problems in ssh known_hosts files - (conflicting known hosts) and incosistencies between dns/hosts + (conflicting known hosts) and inconsistencies between dns/hosts entries and local machine names - Args: - node: nodename of a host to check. can be short or full qualified hostname + @param 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 + @return: (success, detail), where: + - success: True/False + - detail: string with details """ - retval = self.Run(node, 'root', 'hostname') + retval = self.Run(node, "root", "hostname --fqdn", quiet=False) if retval.failed: msg = "ssh problem" output = retval.output if output: msg += ": %s" % output + else: + msg += ": %s (no output)" % retval.fail_reason + logging.error("Command %s failed: %s", retval.cmd, msg) return False, msg remotehostname = retval.stdout.strip() if not remotehostname or remotehostname != node: - return False, "hostname mismatch, got %s" % remotehostname + if node.startswith(remotehostname + "."): + msg = "hostname not FQDN" + else: + msg = "hostname mismatch" + return False, ("%s: expected %s but got %s" % + (msg, node, remotehostname)) return True, "host matches" @@ -224,6 +283,6 @@ def WriteKnownHostsFile(cfg, file_name): """Writes the cluster-wide equally known_hosts file. """ - utils.WriteFile(file_name, mode=0700, + utils.WriteFile(file_name, mode=0600, data="%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetHostKey()))