Source code for schrodinger.job.remote_command

#!/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)