#!/usr/bin/env python
#
# A Python port of the main RemoteCmd.pm functionality
#
import getpass
import logging as log
import os
import os.path
import re
import socket
import subprocess as sp
import sys
import schrodinger.utils.fileutils as fileutils
from schrodinger.utils import sshconfig
logger = log.getLogger('rcmd')
logger.setLevel(log.WARNING)
handler = log.StreamHandler(sys.stderr)
handler.setFormatter(log.Formatter("%(message)s"))
logger.addHandler(handler)
# Timeout period for ssh
rsh_timeout = int(os.environ.get('SCHRODINGER_RSH_TIMEOUT', '60'))
# Maximum number of times a remote command will be retried
rsh_max_retries = int(os.environ.get('SCHRODINGER_RSH_RETRIES', '1'))
# Debug level
debug = int(os.environ.get('SCHRODINGER_JOB_DEBUG', '0'))
# Should rsh/ssh from remote hosts be tested?
test_reverse_rsh = False
# Globals
hostname = socket.gethostname()
schrodinger_dir = os.environ.get('SCHRODINGER')
shell_exec = None
rsh_exec = None
rsh_src = None
rsh_errors_re = r'{}|{}|{}|{}|{}|{}'.format(
"Server refused our key", "Permission denied", "Connection abandoned",
"No route to host", "Host does not exist", "Name or service not known")
rsh_warning_issued = 0
if debug > 0:
logger.setLevel(log.DEBUG)
[docs]class CommandError(Exception):
"""
Used to report external commands that fail.
When this is caught by the main cmdline driver, the error message
will be printed and the usage message displayed.
"""
[docs] def __init__(self,
sp_error=None,
command="",
errmsg=None,
output="",
userhost="",
returncode=None):
"""
The constructor takes a subprocess.CalledProcessError for the failed
command, from which details of the command are extracted. A user-
friendly error message will be composed from that information
unless an explicit error message is provided.
"""
if sp_error:
self.command = str(sp_error.cmd)
self.output = sp_error.output
self.returncode = sp_error.returncode
else:
self.command = command
self.returncode = returncode
self.output = output
self.userhost = userhost
if errmsg is not None:
self.errmsg = errmsg
else:
errornum = self.returncode
what = "Error: Remote command (%s) " % self.command
if userhost:
what = " ".join((what, "to", userhost))
if errornum < 0:
self.errmsg = what + " exited due to signal " + str(-errornum)
else:
self.errmsg = what + " failed with exit code " + str(errornum)
if self.output:
self.errmsg += ". Output is - %s" % self.output
def __str__(self):
return self.errmsg
[docs]def which(program, search_path=None):
"""
Search for a file in the given list of directories. Use $PATH
if no search path is specified.
Returns the absolute pathname for the first executable found.
:type program: string
:param rogram: the executable to search for
:rtype: string
:return: the absolute pathname for the executable found, or None
if the program could not be found.
"""
if os.path.isabs(program):
if os.path.isfile(program):
return program
else:
return None
if search_path is None:
search_path = os.environ.get('PATH', os.defpath).split(os.pathsep)
for bindir in search_path:
pathname = os.path.join(bindir, program)
if os.path.isfile(pathname):
return pathname
if sys.platform == 'win32':
pathname += ".exe"
if os.path.isfile(pathname):
return pathname
[docs]def get_rsh_exec():
"""
Return the name of the rsh-compatible program to use for
executing remote commands.
"""
global rsh_exec, rsh_src, rsh_warning_issued
if rsh_exec:
return rsh_exec
if 'SCHRODINGER_SSH' in os.environ:
rsh_exec = os.environ.get('SCHRODINGER_SSH')
rsh_src = "SCHRODINGER_SSH"
elif 'SCHRODINGER_RSH' in os.environ:
rsh_exec = os.environ.get('SCHRODINGER_RSH')
rsh_src = "SCHRODINGER_RSH"
else:
if sys.platform == 'win32':
rsh_exec = which('plink')
if rsh_exec is None and 'MMSHARE_EXEC' in os.environ:
rsh_exec = which('plink', (os.environ.get('MMSHARE_EXEC')))
else:
rsh_exec = which('ssh')
if rsh_exec is None:
rsh_exec = which('remsh')
if rsh_exec is None:
rsh_exec = which('rsh')
rsh_src = "default"
if re.search(r"(r|rem)sh", rsh_exec):
if not rsh_warning_issued:
msg = ("Warning: You are using '%s' as your shell, " +
"which is deprecated. Consider setting up ssh instead"
) % rsh_exec
logger.warning(msg)
rsh_warning_issued = 1
return rsh_exec
[docs]def shell():
"""
Return the pathname to a Bourne-compatible shell that can be
used for running shell commands.
"""
global shell_exec
if shell_exec is None and sys.platform == 'win32':
unxutils_shell = os.path.join(schrodinger_dir, 'unxutils', 'sh.exe')
if os.path.isfile(unxutils_shell):
shell_exec = unxutils_shell
if shell_exec is None:
shell_exec = which("sh")
return shell_exec
def _filter_rsh_output(content):
"""
Filter the rsh output like 'Connection to <host> closed.' from
getting printed to the stream.
"""
if content:
content = re.sub(
r'Connection to.* closed.\s*', '', content, flags=re.IGNORECASE)
return content
[docs]def remote_command(command, host, user=None, capture_stderr=False):
"""
Execute a the given command on a particular host.
Returns a tuple containing the captured output and an error message.
:type host: string
:param host: the host on which to run the command
:type user: string
:param user: the user account under which to run the command
:type command: string
:param command: the command to be executed on the remote machine
:type capture_stderr: boolean
:param capture_stderr: should stderr be captured along with stdout?
:rtype: string
:return: the captured output from the command
:raise CommandError: if the remote command fails
"""
result = ""
errornum = 0
timed_out = 0
error = ""
rsh_command = _rsh_cmd(host, user, True)
rsh_command = ' '.join(['"%s"' % item for item in rsh_command])
userhost = "@".join((_get_remote_user(user), host))
full_command = []
# /bin/sh is wrapped with doublequotes if the exact command string is to be
# needed for interpretation by final shell. This is done in accordance with
# Unxutils of Windows and works for other non Windows platforms.
full_command.append("/bin/sh -c '%s'" % (command))
cmdline = "{} {}".format(rsh_command, sp.list2cmdline(full_command))
logger.debug(f">> {cmdline}")
if sys.platform == "win32":
cmdline = cmdline.replace("\\`", "`").replace("\\$", "$")
if not sshconfig.hostname_in_registry(host):
sshconfig.cache_hostname_plink(_get_remote_user(user), host)
if capture_stderr:
pobj = sp.Popen(
cmdline,
shell=True,
bufsize=4096,
stdout=sp.PIPE,
stderr=sp.STDOUT,
universal_newlines=True)
result = _filter_rsh_output(pobj.communicate()[0])
else:
pobj = sp.Popen(
cmdline,
shell=True,
bufsize=4096,
stdout=sp.PIPE,
stderr=sp.PIPE,
universal_newlines=True)
result, err = pobj.communicate()
err = _filter_rsh_output(err)
if err:
sys.stderr.write(err)
logger.debug("<< " + result)
errmsg = None
if sys.platform == 'win32' or pobj.returncode:
rsh_error = re.search(rsh_errors_re, result)
if rsh_error:
errmsg = ("Error: Remote command (%s) from %s to %s failed " +
"with '%s'.\n" +
"** Please check your passwordless SSH configuration " +
"on %s and on %s **") % \
(get_rsh_exec(), hostname, userhost, rsh_error.group(0),
hostname, host)
if rsh_error or pobj.returncode:
err = sp.CalledProcessError(pobj.returncode, cmdline, output=result)
raise \
CommandError(err, cmdline, errmsg=errmsg, userhost=userhost)
return result
#######################################################################
# Get the remote user to use with rsh command
#
def _get_remote_user(remoteuser=None):
"""
Return the remote user to use with rsh command
:type remoteuser: string
:param remoteuser: string
:rtype: string
:return: return the remote user for rsh command
"""
if remoteuser:
return remoteuser
else:
return getpass.getuser()
#######################################################################
# Form the basic rsh command for a given host and user.
#
def _rsh_cmd(remotehost, remoteuser=None, nostdin=True):
"""
Returns the 'ssh' command needed to execute a remote command on
the given host. The actual remote command needs to be appended to
the returned string.
:type host: string
:param host: string
:type user: string
:param user: string
:type nostdin: boolean
:param nostdin: should stdin be closed for this command?
:rtype: list
:return: the list of command args for the remote command, suitable
for use in subprocess functions
"""
ssh_auth = os.environ.get('SCHRODINGER_SSH_AUTH', 'rsa')
ssh_identity = os.environ.get('SCHRODINGER_SSH_IDENTITY', '')
rsh_exec = get_rsh_exec()
ssh_is_plink = 'plink' in rsh_exec
ssh_is_rsh = ('rsh' in rsh_exec) or ('remsh' in rsh_exec)
remoteuser = _get_remote_user(remoteuser)
command = []
if sys.platform.startswith("linux"):
command += [
'env',
'LD_LIBRARY_PATH=%s' % os.environ.get("ORIGINAL_LD_LIBRARY_PATH",
"")
]
command += [rsh_exec, remotehost]
if not ssh_is_rsh:
# set options to have ssh use 'batch mode', which
# inhibits password prompts.
if ssh_is_plink:
command.extend(("-ssh", "-batch"))
else:
command.extend(("-o", "BatchMode=yes", "-o",
"StrictHostKeyChecking=no"))
if nostdin:
command.append("-n")
if ssh_auth != 'rsa':
ssh_identity = ""
elif ssh_is_plink and ssh_identity == "":
home_dir = fileutils.get_directory_path(fileutils.HOME)
ssh_identity = os.path.join(home_dir, remoteuser + ".ppk")
if ssh_identity:
if os.path.isfile(ssh_identity):
command.extend(("-i", ssh_identity))
else:
logger.error("Cannot locate the private key file \"%s\"" %
(ssh_identity))
sys.exit(1)
if remoteuser:
command.extend(("-l", remoteuser))
return command
[docs]def rsh_put_cmd(remotehost, put_fn, remoteuser=None, from_remote=False):
"""
Returns the 'scp' command needed to execute to copy a file to a given remote
host. The actual remote command needs to be appended to the returned string.
:type remotehost: string
:param remotehost: string
:type put_fn: string
:param put_fn: Path to the file that will be copied over. If it is a
directory, it will be copied recursively
:type remoteuser: string
:param remoteuser: Remote username
:type from_remote: bool
:param from_remote: If True, put from remote to local, otherwise (default)
from local to remote
:rtype: list
:return: the list of command args for the command, suitable for use in
subprocess functions
"""
if not os.path.exists(put_fn):
logger.error('%s could not be found.' % put_fn)
sys.exit(1)
ssh_auth = os.environ.get('SCHRODINGER_SSH_AUTH', 'rsa')
ssh_identity = os.environ.get('SCHRODINGER_SSH_IDENTITY', '')
rsh_exec = get_rsh_exec()
if 'ssh' not in rsh_exec:
logger.error('rsh_put_cmd only supports ssh protocol.')
sys.exit(1)
# Replace ssh with scp
rsh_exec_split = rsh_exec.rsplit('ssh', 1)
scp_exec = 'scp'.join(rsh_exec_split)
remoteuser = _get_remote_user(remoteuser)
command = []
if sys.platform.startswith("linux"):
command += [
'env',
'LD_LIBRARY_PATH=%s' % os.environ.get("ORIGINAL_LD_LIBRARY_PATH",
"")
]
command += [scp_exec]
if os.path.isdir(put_fn):
command.append('-r')
put_dn = os.path.abspath(os.path.join(put_fn, os.pardir))
else:
put_dn = os.path.dirname(put_fn)
command.extend(("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=no"))
if ssh_auth != 'rsa':
ssh_identity = ''
if ssh_identity:
if os.path.isfile(ssh_identity):
command.extend(("-i", ssh_identity))
else:
logger.error('Cannot locate the private key file "%s"' %
(ssh_identity))
sys.exit(1)
if from_remote:
# First remote path then local
command.append(get_remote_path(remoteuser, remotehost, put_fn))
command.append(put_dn)
else:
# Local first
command.append(put_fn)
command.append(get_remote_path(remoteuser, remotehost, put_dn))
return command
[docs]def get_remote_path(remoteuser, remotehost, path):
"""
Assemble remote path from remote user, host and path.
:type remoteuser: str or None
:param remoteuser: Remote user, can be None
:type remotehost: str
:param remotehost: Remote host
:type path: str
:param path: Remote absolute path
:rtype: str
:return: Remote path with user and host
"""
if remoteuser:
return f'{remoteuser}@{remotehost}:"{path}"'
else:
return f'{remotehost}:"{path}"'
[docs]def rsh_test(hosts):
"""
Test remote commands to and from one or more hosts.
"""
return "** rsh_test() has not yet been ported **"
if __name__ == '__main__':
import argparse
parser = argparse.ArgumentParser(
add_help=False,
description="Run a remote command on the specified host.")
parser.add_argument("host", help="remote host name")
parser.add_argument(
"cmd", nargs='*', metavar='command', help="remote command to run")
parser.add_argument("-user", help="remote user name")
parser.add_argument(
"-debug", action='store_true', help="report debugging information")
parser.add_argument("-help", action='help', help="print this message")
opt = parser.parse_args()
if opt.debug:
logger.setLevel(log.DEBUG)
cmdline = sp.list2cmdline(opt.cmd)
try:
output = remote_command(
cmdline, opt.host, opt.user, capture_stderr=True)
print(output)
except CommandError as err:
print(err)
sys.exit(1)