# emacs: -*- mode: python; py-indent-offset: 4; tab-width: 4; indent-tabs-mode: nil -*-
# ex: set sts=4 ts=4 sw=4 noet:
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
#
# See COPYING file distributed along with the datalad package for the
# copyright and license terms.
#
# ## ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ### ##
"""SSH command to expose datalad's connection management to 3rd-party tools
Primary use case is to be used with git as core.sshCommand
"""
__docformat__ = 'restructuredtext'
import logging
import os
import sys
import tempfile
from datalad.support.param import Parameter
from datalad.interface.base import Interface
from datalad.interface.base import build_doc
from datalad.utils import split_cmdline
from datalad import ssh_manager
lgr = logging.getLogger('datalad.sshrun')
@build_doc
class SSHRun(Interface):
"""Run command on remote machines via SSH.
This is a replacement for a small part of the functionality of SSH.
In addition to SSH alone, this command can make use of datalad's SSH
connection management. Its primary use case is to be used with Git
as 'core.sshCommand' or via "GIT_SSH_COMMAND".
Configure `datalad.ssh.identityfile` to pass a file to the ssh's -i option.
"""
# prevent common args from being added to the docstring
_no_eval_results = True
_params_ = dict(
login=Parameter(
args=("login",),
doc="[user@]hostname"),
cmd=Parameter(
args=("cmd",),
doc="command for remote execution"),
port=Parameter(
args=("-p", '--port'),
doc="port to connect to on the remote host"),
ipv4=Parameter(
args=("-4",),
dest="ipv4",
doc="use IPv4 addresses only",
action="store_true"),
ipv6=Parameter(
args=("-6",),
dest="ipv6",
doc="use IPv6 addresses only",
action="store_true"),
options=Parameter(
args=("-o",),
metavar="OPTION",
dest="options",
doc="configuration option passed to SSH",
action="append"),
no_stdin=Parameter(
args=("-n",),
action="store_true",
dest="no_stdin",
doc="Do not connect stdin to the process"),
)
@staticmethod
def __call__(login, cmd, port=None, ipv4=False, ipv6=False, options=None,
no_stdin=False):
lgr.debug("sshrun invoked: login=%r, cmd=%r, port=%r, options=%r, "
"ipv4=%r, ipv6=%r, no_stdin=%r",
login, cmd, port, options, ipv4, ipv6, no_stdin)
# Perspective workarounds for git-annex invocation, see
# https://github.com/datalad/datalad/issues/1456#issuecomment-292641319
if cmd.startswith("'") and cmd.endswith("'"):
lgr.debug(
"Detected additional level of quotations in %r so performing "
"command line splitting", cmd
)
# there is an additional layer of quotes
# Let's strip them off by splitting the command
cmd_ = split_cmdline(cmd)
if len(cmd_) != 1:
raise RuntimeError(
"Obtained more or less than a single argument after "
"command line splitting: %s" % repr(cmd_))
cmd = cmd_[0]
sshurl = 'ssh://{}{}'.format(
login,
':{}'.format(port) if port else '')
if ipv4 and ipv6:
raise ValueError("Cannot force both IPv4 and IPv6")
elif ipv4:
force_ip = 4
elif ipv6:
force_ip = 6
else:
force_ip = None
ssh = ssh_manager.get_connection(sshurl, force_ip=force_ip)
# use an empty temp file as stdin if none shall be connected
stdin_ = tempfile.TemporaryFile() if no_stdin else sys.stdin
try:
out, err = ssh(cmd, stdin=stdin_, log_output=False,
options=options)
finally:
if no_stdin:
stdin_.close()
os.write(1, out.encode('UTF-8'))
os.write(2, err.encode('UTF-8'))