blob: c694341c67e34dc8d36778d21d34bc384a540496 [file] [log] [blame]
#
#
# 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
# 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.
"""Module encapsulating ssh functionality.
"""
import os
import logging
from ganeti import utils
from ganeti import errors
from ganeti import constants
from ganeti import netutils
from ganeti import pathutils
from ganeti import vcluster
from ganeti import compat
def GetUserFiles(user, mkdir=False, dircheck=True, kind=constants.SSHK_DSA,
_homedir_fn=None):
"""Return the paths of a user's SSH files.
@type user: string
@param user: Username
@type mkdir: bool
@param mkdir: Whether to create ".ssh" directory if it doesn't exist
@type dircheck: bool
@param dircheck: Whether to check if ".ssh" directory exists
@type kind: string
@param kind: One of L{constants.SSHK_ALL}
@rtype: tuple; (string, string, string)
@return: Tuple containing three file system paths; the private SSH key file,
the public SSH key file and the user's C{authorized_keys} file
@raise errors.OpExecError: When home directory of the user can not be
determined
@raise errors.OpExecError: Regardless of the C{mkdir} parameters, this
exception is raised if C{~$user/.ssh} is not a directory and C{dircheck}
is set to C{True}
"""
if _homedir_fn is None:
_homedir_fn = utils.GetHomeDir
user_dir = _homedir_fn(user)
if not user_dir:
raise errors.OpExecError("Cannot resolve home of user '%s'" % user)
if kind == constants.SSHK_DSA:
suffix = "dsa"
elif kind == constants.SSHK_RSA:
suffix = "rsa"
else:
raise errors.ProgrammerError("Unknown SSH key kind '%s'" % kind)
ssh_dir = utils.PathJoin(user_dir, ".ssh")
if mkdir:
utils.EnsureDirs([(ssh_dir, constants.SECURE_DIR_MODE)])
elif dircheck and not os.path.isdir(ssh_dir):
raise errors.OpExecError("Path %s is not a directory" % ssh_dir)
return [utils.PathJoin(ssh_dir, base)
for base in ["id_%s" % suffix, "id_%s.pub" % suffix,
"authorized_keys"]]
def GetAllUserFiles(user, mkdir=False, dircheck=True, _homedir_fn=None):
"""Wrapper over L{GetUserFiles} to retrieve files for all SSH key types.
See L{GetUserFiles} for details.
@rtype: tuple; (string, dict with string as key, tuple of (string, string) as
value)
"""
helper = compat.partial(GetUserFiles, user, mkdir=mkdir, dircheck=dircheck,
_homedir_fn=_homedir_fn)
result = [(kind, helper(kind=kind)) for kind in constants.SSHK_ALL]
authorized_keys = [i for (_, (_, _, i)) in result]
assert len(frozenset(authorized_keys)) == 1, \
"Different paths for authorized_keys were returned"
return (authorized_keys[0],
dict((kind, (privkey, pubkey))
for (kind, (privkey, pubkey, _)) in result))
class SshRunner:
"""Wrapper for SSH commands.
"""
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, 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" % pathutils.SSH_KNOWN_HOSTS_FILE,
"-oUserKnownHostsFile=/dev/null",
"-oCheckHostIp=no",
]
if use_cluster_key:
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.
# Note: ask_key conflicts with batch mode
if batch:
if ask_key:
raise errors.ProgrammerError("SSH call requested conflicting options")
options.append("-oBatchMode=yes")
if strict_host_check:
options.append("-oStrictHostKeyChecking=yes")
else:
options.append("-oStrictHostKeyChecking=no")
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")
else:
options.append("-4")
return options
def BuildCmd(self, hostname, user, command, batch=True, ask_key=False,
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.
@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]
argv.extend(self._BuildSshOptions(batch, ask_key, use_cluster_key,
strict_host_check, private_key,
quiet=quiet))
if tty:
argv.extend(["-t", "-t"])
argv.append("%s@%s" % (user, hostname))
# Insert variables for virtual nodes
argv.extend("export %s=%s;" %
(utils.ShellQuote(name), utils.ShellQuote(value))
for (name, value) in
vcluster.EnvironmentForHost(hostname).items())
argv.append(command)
return argv
def Run(self, *args, **kwargs):
"""Runs a command on a remote node.
This method has the same return value as `utils.RunCmd()`, which it
uses to launch ssh.
Args: see SshRunner.BuildCmd.
@rtype: L{utils.process.RunResult}
@return: the result as from L{utils.process.RunCmd()}
"""
return utils.RunCmd(self.BuildCmd(*args, **kwargs))
def CopyFileToNode(self, node, filename):
"""Copy a file to another node with scp.
@param node: node in the cluster
@param filename: absolute pathname of a local file
@rtype: boolean
@return: the success of the operation
"""
if not os.path.isabs(filename):
logging.error("File %s must be an absolute path", filename)
return False
if not os.path.isfile(filename):
logging.error("File %s does not exist", filename)
return False
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, vcluster.ExchangeNodeRoot(node, filename)))
result = utils.RunCmd(command)
if result.failed:
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
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 is used to detect problems in ssh known_hosts files
(conflicting known hosts) and inconsistencies between dns/hosts
entries and local machine names
@param node: nodename of a host to check; can be short or
full qualified hostname
@return: (success, detail), where:
- success: True/False
- detail: string with details
"""
cmd = ("if test -z \"$GANETI_HOSTNAME\"; then"
" hostname --fqdn;"
"else"
" echo \"$GANETI_HOSTNAME\";"
"fi")
retval = self.Run(node, constants.SSH_LOGIN_USER, cmd, 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:
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"
def WriteKnownHostsFile(cfg, file_name):
"""Writes the cluster-wide equally known_hosts file.
"""
data = ""
if cfg.GetRsaHostKey():
data += "%s ssh-rsa %s\n" % (cfg.GetClusterName(), cfg.GetRsaHostKey())
if cfg.GetDsaHostKey():
data += "%s ssh-dss %s\n" % (cfg.GetClusterName(), cfg.GetDsaHostKey())
utils.WriteFile(file_name, mode=0600, data=data)