2

Edit:

The original intent of this question was to find a way to launch an interactive ssh session via a Python script. I'd tried subprocess.call() before and had gotten a Killed response before anything was output onto the terminal. I just assumed this was an issue/limitation with the subprocess module instead of an issue somewhere else.This was found not to be the case when I ran the script on a non-resource limited machine and it worked fine.

This then turned the question into: How can I run an interactive ssh session with whatever resource limitations were preventing it from running?

Shoutout to Charles Duffy who was a huge help in trying to diagnose all of this .

Below is the original question:

Background:

So I have a script that is currently written in bash. It parses the output of a few console functions and then opens up an ssh session based on those parsed outputs.

It currently works fine, but I'd like to expand it's capabilities a bit by adding some flag arguments to it. I've worked with argparse before and thoroughly enjoyed it. I tried to do some flag work in bash, and let's just say it leaves much to be desired.

The Actual Question:

Is it possible to have python to do stuff in a console and then put the user in that console?

Something like using subprocess to run a series of commands onto the currently viewed console? This in contrast to how subprocess normally runs, where it runs commands and then shuts the intermediate console down

Specific Example because I'm not sure if what I'm describing makes sense:

So here's a basic run down of the functionality I was wanting:

  1. Run a python script
  2. Have that script run some console command and parse the output
  3. Run the following command:

    ssh -t $correctnode "cd /local_scratch/pbs.$jobid; bash -l"

This command will ssh to the $correctnode, change directory, and then leave a bash window in that node open.

I already know how to do parts 1 and 2. It's part three that I can't figure out. Any help would be appreciated.

Edit: Unlike this question, I am not simply trying to run a command. I'm trying to display a shell that is created by a command. Specifically, I want to display a bash shell created through an ssh command.

James Wright
  • 1,293
  • 2
  • 16
  • 32
  • Did you try this? https://stackoverflow.com/questions/89228/calling-an-external-command-in-python – hemnath mouli Aug 12 '18 at 01:07
  • Possible duplicate of [Calling an external command in Python](https://stackoverflow.com/questions/89228/calling-an-external-command-in-python) – Joel Aug 12 '18 at 01:30
  • @hemnathmouli I tried `subprocess.call(['ssh', '-t', nodename, '"bash -l"']) and it came up with the error `bash: bash -l: command not found`. However, I just type in `ssh -t [NODENAME] "bash -l"`, it works perfectly fine. – James Wright Aug 12 '18 at 01:42
  • @Joel It isn't, as I'm not just trying to run the commands specified. I'm trying to display a shell created by a command. – James Wright Aug 12 '18 at 01:43
  • it looks like this script should have been written in bash – Marat Aug 12 '18 at 02:06
  • @Marat ........ it is. Did you bother reading the question? I'm hoping it can be done in python (or wrapped in python somehow) so that I can use `argparse` to do options flags. – James Wright Aug 12 '18 at 02:08
  • I did read the question. I suspect you tend to overestimate how hard to parse command line arguments in bash, but the point is: if your main script is in bash, the task gets much simpler. You can reuse your Python script within that bash script, too. I will add an example to illustrate the idea – Marat Aug 12 '18 at 02:12
  • @Marat Sorry, I'm a bit frustrated at the moment. I've managed to implement 1 flag in the bash script, but couldn't get the actual flexibility that `argparse` has from it (like being able to have a random mixture of flags and arguments). – James Wright Aug 12 '18 at 02:14
  • It looks like what you're asking for is the `interact()` call in [`pexpect`](http://pexpect.readthedocs.io/en/stable/api/pexpect.html), handing control of a hitherto-scripted program over to the user. – Charles Duffy Aug 12 '18 at 02:35
  • @CharlesDuffy That looks very much like what I'm after. I'll try that out and see what happens. – James Wright Aug 12 '18 at 11:58
  • 1
    If you get it working and add your own answer demonstrating the approach, @-notify me; I'll gladly +1 it (and provide feedback on implementation details) – Charles Duffy Aug 12 '18 at 16:00
  • @CharlesDuffy will do. Just got back to my computer. I'll try out both (Marat's solution and `pexpect.interact()`) and report on what I ended up going with. Thanks for helping me out! – James Wright Aug 12 '18 at 18:49
  • Please note my comments on Marat's solution -- some work is needed to make it robust (against arguments with quotes, spaces, glob characters, etc). – Charles Duffy Aug 12 '18 at 19:36
  • Trying out `interact()` inside `pexpect` hasn't really lead to anything. It will successfully open up an ssh session and displays it (the `username@node###`) , but then it immediately says `Killed` and exits out of the script. I've posted the script in [this gist](https://gist.github.com/u2berggeist/e1ad29939915971e4b6c12f8f4fab422#file-pexpect_test1-py) as `pexpect_test1.py`. I'll try out Marat's solution next. – James Wright Aug 12 '18 at 19:41
  • I can't reproduce the behavior you describe (`Killed`/exit); the script in your gist works perfectly for me, when I provide an actually useful/correct ssh command. – Charles Duffy Aug 12 '18 at 20:12
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/177910/discussion-between-charles-duffy-and-u2berggeist). – Charles Duffy Aug 12 '18 at 20:14
  • 1
    For documentation's sake, the `Killed` message occurred due to the login node killing the task for some reason (probably some kind of resource limitations, but we couldn't figure it out). My (soon to be added) answer will work if the script is run on a separate node. – James Wright Aug 12 '18 at 20:45

2 Answers2

1

This is in no way a real answer, just an illustration to my comment.

Let's say you have a Python script, test.py:

import argparse


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('myarg', nargs="*")
    args = parser.parse_args()

    print("echo Hello world! My arguments are: " + " ".join(args.myarg))

So, you create a bash wrapper around it, test.sh

set -e
$(python test.py $*)

and this is what you get:

$ bash test.sh
Hello world! My arguments are:
$ bash test.sh one two
Hello world! My arguments are: one two

What is going on here:

  • python script does not execute commands. Instead, it outputs the commands bash script will run (echo in this example). In your case, the last command will be ssh blabla
  • bash executes the output of the python script (the $(...) part), passing on all its arguments (the $* part)
  • you can use argparse inside the python script; if anything is wrong with the arguments, the message will be put to stderr and will not be executed by bash; bash script will stop because of set -e flag
Marat
  • 15,215
  • 2
  • 39
  • 48
  • 1
    This is problematic in several ways. `$(python test.py $*)` changes `./yourscript 'hello world'` to `./yourscript 'hello' 'world'`. And `$(...anything...)` takes the output of `...anything...`, string-splits on whitespace, glob-expands each resulting word, and runs that as another command, notably, it **doesn't** honor quotes, escaping, redirections or other shell syntax, so it mangles commands quite badly compared to how a shell would run them were they parsed as code; the bugs created are akin to those described in [BashFAQ #50](http://mywiki.wooledge.org/BashFAQ/050). – Charles Duffy Aug 12 '18 at 02:33
  • If you **really** want to generate a shell command from Python, (1) use `pipes.quote()` it Python 2 or `shlex.quote()` in Python 3 to escape the words to be `eval`-safe from the Python side; and (2) use `eval` to actually evaluate them from the shell side. – Charles Duffy Aug 12 '18 at 02:37
  • ...so, the former might look something like: `cmd=['printf', r' - %q\n'] + args.myarg; print("echo 'Arguments follow:';" + ' '.join(pipes.quote(arg) for arg in cmd))` – Charles Duffy Aug 12 '18 at 02:40
  • @CharlesDuffy thanks, I learned something today. With these fixes applied and assuming python program does everything through subprocess and only outputs the final ssh command, does this approach still make sense? – Marat Aug 12 '18 at 02:43
  • It's a workable approach, yes. BTW, I also advise against using `set -e` without very deliberate consideration of the (substantial) drawbacks. See the [exercises in BashFAQ #105](http://mywiki.wooledge.org/BashFAQ/105#Exercises) (and the allegory above them if you have some time to read), or the list of incompatibilities at https://www.in-ulm.de/~mascheck/various/set-e/. – Charles Duffy Aug 12 '18 at 02:46
1

Context For Readers

The OP is operating on a very resource-constrained (particularly, it appears, process-constrained) jumphost box, where starting an ssh process as a subprocess of python goes over a relevant limit (on number of processes, perhaps?)


Approach A: Replacing The Python Interpreter With Your Interactive Process

Using the exec*() family of system calls causes your original process to no longer be in memory (unlike the fork()+exec*() combination used to start a subprocess while leaving the parent process running), so it doesn't count against the account's limits.

import argparse
import os

try:
    from shlex import quote
except ImportError:
    from pipes import quote

parser = argparse.ArgumentParser()
parser.add_argument('node')
parser.add_argument('jobid')
args = parser.parse_args()

remote_cmd_str = 'cd /local_scratch/pbs.%s && exec bash -i' % (quote(args.jobid))
local_cmd = [
  '/usr/bin/env', 'ssh', '-tt', node, remote_cmd_str
]
os.execv("/usr/bin/env", local_cmd)

Approach B: Generating Shell Commands From Python

If we use Python to generate a shell command, the shell can invoke that command only after the Python process exited, such that we stay under our externally-enforced process limit.

First, a slightly more robust approach at generating eval-able output:

import argparse

try:
    from shlex import quote
except ImportError:
    from pipes import quote

parser = argparse.ArgumentParser()
parser.add_argument('node')
parser.add_argument('jobid')
args = parser.parse_args()

remoteCmd = ['cd', '/local_scratch/pbs.%s' % (args.jobid)]
remoteCmdStr = ' '.join(quote(x) for x in remoteCmd) + ' && bash -l'
cmd = ['ssh', '-t', args.correctnode, remoteCmdStr]
print(' '.join(pipes.quote(x) for x in cmd)

To run this from a shell, if the above is named as genSshCmd:

#!/bin/sh
eval "$(genSshCmd "$@")"

Note that there are two separate layers of quoting here: One for the local shell running eval, and the second for the remote shell started by SSH. This is critical -- you don't want a jobid of $(rm -rf ~) to actually invoke rm.

Charles Duffy
  • 280,126
  • 43
  • 390
  • 441