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 re
import socket
import subprocess as sp
import sys

import schrodinger.utils.fileutils as fileutils
from schrodinger.utils import env
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) subprocess_env = os.environ.copy() if sys.platform == "linux": subprocess_env[ env.LD_LIBRARY_PATH] = env.get_ld_library_path_for_system_ssh() if capture_stderr: pobj = sp.Popen(cmdline, shell=True, bufsize=4096, stdout=sp.PIPE, stderr=sp.STDOUT, env=subprocess_env, 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, env=subprocess_env, 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)