def run_cmd(command, stdin=None, stdin_str=None, capture_process=False, capture_status=False, manage=False): """Run a given cylc command on another account and/or host. Arguments: command (list): command inclusive of all opts and args required to run via ssh. stdin (file): If specified, it should be a readable file object. If None, DEVNULL is set if output is to be captured. stdin_str (str): A string to be passed to stdin. Implies `stdin=PIPE`. capture_process (boolean): If True, set stdout=PIPE and return the Popen object. capture_status (boolean): If True, and the remote command is unsuccessful, return the associated exit code instead of exiting with an error. manage (boolean): If True, watch ancestor processes and kill command if they change (e.g. kill tail-follow commands when parent ssh connection dies). Return: * If capture_process=True, the Popen object if created successfully. * Else True if the remote command is executed successfully, or if unsuccessful and capture_status=True the remote command exit code. * Otherwise exit with an error message. """ # CODACY ISSUE: # subprocess call - check for execution of untrusted input. # REASON IGNORED: # The command is read from the site/user global config file, but we check # above that it ends in 'cylc', and in any case the user could execute # any such command directly via ssh. stdout = None stderr = None if capture_process: stdout = PIPE stderr = PIPE if stdin is None: stdin = DEVNULL if stdin_str: read, write = os.pipe() os.write(write, stdin_str.encode()) os.close(write) stdin = read try: proc = Popen(command, stdin=stdin, stdout=stdout, stderr=stderr) except OSError as exc: sys.exit(r'ERROR: %s: %s' % (exc, ' '.join(quote(item) for item in command))) if capture_process: return proc else: if manage: watch_and_kill(proc) res = proc.wait() if WIFSIGNALED(res): sys.exit(r'ERROR: command terminated by signal %d: %s' % (res, ' '.join(quote(item) for item in command))) elif res and capture_status: return res elif res: sys.exit(r'ERROR: command returns %d: %s' % (res, ' '.join(quote(item) for item in command))) else: return True
def remote_cylc_cmd(cmd, user=None, host=None, capture=False, manage=False, ssh_login_shell=None, ssh_cylc=None, stdin=None): """Run a given cylc command on another account and/or host. Arguments: cmd (list): command to run remotely. user (string): user ID for the remote login. host (string): remote host name. Use 'localhost' if not specified. capture (boolean): If True, set stdout=PIPE and return the Popen object. manage (boolean): If True, watch ancestor processes and kill command if they change (e.g. kill tail-follow commands when parent ssh connection dies). ssh_login_shell (boolean): If True, launch remote command with `bash -l -c 'exec "$0" "$@"'`. ssh_cylc (string): Location of the remote cylc executable. stdin (file): If specified, it should be a readable file object. If None, it will be set to `open(os.devnull)` and the `-n` option will be added to the SSH command line. Return: If capture=True, return the Popen object if created successfully. Otherwise, return the exit code of the remote command. """ if host is None: host = "localhost" if user is None: user_at_host = host else: user_at_host = '%s@%s' % (user, host) # Build the remote command command = shlex.split( str(glbl_cfg().get_host_item('ssh command', host, user))) if stdin is None: command.append('-n') stdin = open(os.devnull) command.append(user_at_host) # Pass cylc version through. command += ['env', r'CYLC_VERSION=%s' % CYLC_VERSION] if ssh_login_shell is None: ssh_login_shell = glbl_cfg().get_host_item('use login shell', host, user) if ssh_login_shell: # A login shell will always source /etc/profile and the user's bash # profile file. To avoid having to quote the entire remote command # it is passed as arguments to bash. command += ['bash', '--login', '-c', quote(r'exec "$0" "$@"')] if ssh_cylc is None: ssh_cylc = glbl_cfg().get_host_item('cylc executable', host, user) if not ssh_cylc.endswith('cylc'): raise ValueError( r'ERROR: bad cylc executable in global config: %s' % ssh_cylc) command.append(ssh_cylc) command += cmd if cylc.flags.debug: sys.stderr.write('%s\n' % command) if capture: stdout = PIPE else: stdout = None # CODACY ISSUE: # subprocess call - check for execution of untrusted input. # REASON IGNORED: # The command is read from the site/user global config file, but we check # above that it ends in 'cylc', and in any case the user could execute # any such command directly via ssh. proc = Popen(command, stdout=stdout, stdin=stdin) if capture: return proc else: if manage: watch_and_kill(proc) res = proc.wait() if WIFSIGNALED(res): sys.stderr.write( 'ERROR: remote command terminated by signal %d\n' % res) elif res: sys.stderr.write('ERROR: remote command failed %d\n' % res) return res
def execute(self, force_required=False, env=None, path=None, dry_run=False): """Execute command on remote host. Returns False if remote re-invocation is not needed, True if it is needed and executes successfully otherwise aborts. """ if not self.is_remote: return False from cylc.cfgspec.globalcfg import GLOBAL_CFG from cylc.version import CYLC_VERSION name = os.path.basename(self.argv[0])[5:] # /path/to/cylc-foo => foo user_at_host = '' if self.owner: user_at_host = self.owner + '@' if self.host: user_at_host += self.host else: user_at_host += 'localhost' # Build the remote command # ssh command and options (X forwarding) ssh_tmpl = str(GLOBAL_CFG.get_host_item( "remote shell template", self.host, self.owner)).replace(" %s", "") command = shlex.split(ssh_tmpl) + ["-Y", user_at_host] # Use bash -l? ssh_login_shell = self.ssh_login_shell if ssh_login_shell is None: ssh_login_shell = GLOBAL_CFG.get_host_item( "use login shell", self.host, self.owner) # Pass cylc version through. command += ["env", "CYLC_VERSION=%s" % CYLC_VERSION] if ssh_login_shell: # A login shell will always source /etc/profile and the user's bash # profile file. To avoid having to quote the entire remote command # it is passed as arguments to the bash script. command += ["bash", "--login", "-c", "'exec $0 \"$@\"'"] # "cylc" on the remote host if path: command.append(os.sep.join(path + ["cylc"])) else: command.append(GLOBAL_CFG.get_host_item( "cylc executable", self.host, self.owner)) command.append(name) if env is None: env = {} for var, val in env.iteritems(): command.append("--env=%s=%s" % (var, val)) for arg in self.args: command.append("'" + arg + "'") # above: args quoted to avoid interpretation by the shell, # e.g. for match patterns such as '.*' on the command line. if cylc.flags.verbose: # Wordwrap the command, quoting arguments so they can be run # properly from the command line command_str = ' '.join([quote(arg) for arg in command]) print '\n'.join( TextWrapper(subsequent_indent='\t').wrap(command_str)) if dry_run: return command try: popen = subprocess.Popen(command) except OSError as exc: sys.exit("ERROR: remote command invocation failed %s" % str(exc)) res = popen.wait() if WIFSIGNALED(res): sys.exit("ERROR: remote command terminated by signal %d" % res) elif res: sys.exit("ERROR: remote command failed %d" % res) else: return True
def execute(self, dry_run=False, forward_x11=False, abort_if=None): """Execute command on remote host. Returns False if remote re-invocation is not needed, True if it is needed and executes successfully otherwise aborts. """ if not self.is_remote: return False if abort_if is not None and abort_if in sys.argv: sys.stderr.write( "ERROR: option '%s' not available for remote run\n" % abort_if) return True # Build the remote command command = shlex.split(glbl_cfg().get_host_item('ssh command', self.host, self.owner)) if forward_x11: command.append('-Y') user_at_host = '' if self.owner: user_at_host = self.owner + '@' if self.host: user_at_host += self.host else: user_at_host += 'localhost' command.append(user_at_host) # Pass cylc version through. command += ['env', quote(r'CYLC_VERSION=%s' % CYLC_VERSION)] if 'CYLC_UTC' in os.environ: command.append(quote(r'CYLC_UTC=True')) command.append(quote(r'TZ=UTC')) # Use bash -l? ssh_login_shell = self.ssh_login_shell if ssh_login_shell is None: ssh_login_shell = glbl_cfg().get_host_item('use login shell', self.host, self.owner) if ssh_login_shell: # A login shell will always source /etc/profile and the user's bash # profile file. To avoid having to quote the entire remote command # it is passed as arguments to the bash script. command += ['bash', '--login', '-c', quote(r'exec "$0" "$@"')] # 'cylc' on the remote host if self.ssh_cylc: command.append(self.ssh_cylc) else: command.append(glbl_cfg().get_host_item('cylc executable', self.host, self.owner)) # /path/to/cylc-foo => foo command.append(os.path.basename(self.argv[0])[5:]) if cylc.flags.verbose or os.getenv('CYLC_VERBOSE') in ["True", "true"]: command.append(r'--verbose') if cylc.flags.debug or os.getenv('CYLC_DEBUG') in ["True", "true"]: command.append(r'--debug') for arg in self.args: command.append(quote(arg)) # above: args quoted to avoid interpretation by the shell, # e.g. for match patterns such as '.*' on the command line. if cylc.flags.debug: sys.stderr.write(' '.join(quote(c) for c in command) + '\n') if dry_run: return command try: popen = Popen(command) except OSError as exc: sys.exit(r'ERROR: remote command invocation failed %s' % exc) res = popen.wait() if WIFSIGNALED(res): sys.exit(r'ERROR: remote command terminated by signal %d' % res) elif res: sys.exit(r'ERROR: remote command failed %d' % res) else: return True