Search code examples
pythonsshpexpect

pexpect and ssh: how to format a string of commands after su - root -c


I am trying to iterate through a list of servers & passwords to change the sshd configs on a group of servers so that I can login/run commands via root using passwordless SSH keys.

I can do this easily in bash but I'm trying to learn Python and (obviously) would like to forego entering in passwords manually.

Here's the bash of what I want to do:

scp ~/.ssh/id_rsa.pub /etc/ssh/sshd_config USER@IP:/tmp/

ssh -o StrictHostKeyChecking=no -t USER@IP "su - root -c \"chown root:root /tmp/id_rsa.pub; chmod 600 /tmp/id_rsa.pub; chown root:root /tmp/sshd_config; mkdir /root/.ssh; chown root:root /root/.ssh; chmod 700 /root/.ssh; mv /tmp/id_rsa.pub /root/.ssh/authorized_keys; mv /tmp/sshd_config /etc/ssh/; service sshd reload\""

I've gotten close to doing this in Python with pexpect:

import pexpect

USER="user"
HOST="192.168.1.1"
USERPASS="userpass" 
ROOTPASS="rootpass"

COMMAND1="scp /Users/user/.ssh/id_rsa.pub /Users/user/github/ssh-pexpect/sshd_config %s@%s:/tmp/" % (USER, HOST)

COMMAND2="ssh -o StrictHostKeyChecking=no -t %s@%s \"su - root -c \"chown root:root /tmp/id_rsa.pub; chmod 600 /tmp/id_rsa.pub; chown root:root /tmp/sshd_config; mkdir /root/.ssh; chown root:root /root/.ssh; chmod 700 /root/.ssh; mv /tmp/id_rsa.pub /root/.ssh/authorized_keys; mv /tmp/sshd_config /etc/ssh/; service sshd reload\"\"" % (USER, HOST)

child = pexpect.spawn(COMMAND1)
child.expect('password:')
child.sendline(USERPASS)
child.expect(pexpect.EOF)
print child.before

child = pexpect.spawn(COMMAND2)
child.expect('password:')
child.sendline(USERPASS)
child.expect('Password:')
child.sendline(ROOTPASS)
child.expect(pexpect.EOF)
print child.before

When I run that COMMAND1 (scp'ing) works fine. But COMMAND2 fails:

server1:ssh-pexpect user$ python test4.py 

id_rsa.pub                                    100%  410     0.4KB/s   00:00    
sshd_config                                   100% 3498     3.4KB/s   00:00    

Traceback (most recent call last):
  File "test4.py", line 25, in <module>
    child.expect(pexpect.EOF)
  File "/Library/Python/2.7/site-packages/pexpect.py", line 1316, in expect
    return self.expect_list(compiled_pattern_list, timeout, searchwindowsize)
  File "/Library/Python/2.7/site-packages/pexpect.py", line 1330, in expect_list
    return self.expect_loop(searcher_re(pattern_list), timeout, searchwindowsize)
  File "/Library/Python/2.7/site-packages/pexpect.py", line 1414, in expect_loop
    raise TIMEOUT (str(e) + '\n' + str(self))
pexpect.TIMEOUT: Timeout exceeded in read_nonblocking().
<pexpect.spawn object at 0x102b796d0>
version: 2.4 ($Revision: 516 $)
command: /usr/bin/ssh
args: ['/usr/bin/ssh', '-o', 'StrictHostKeyChecking=no', '-t', '[email protected]', 'su - root -c chown', 'root:root', '/tmp/id_rsa.pub;', 'chmod', '600', '/tmp/id_rsa.pub;', 'chown', 'root:root', '/tmp/sshd_config;', 'mkdir', '/root/.ssh;', 'chown', 'root:root', '/root/.ssh;', 'chmod', '700', '/root/.ssh;', 'mv', '/tmp/id_rsa.pub', '/root/.ssh/authorized_keys;', 'mv', '/tmp/sshd_config', '/etc/ssh/;', 'service', 'sshd', 'reload']
searcher: searcher_re:
    0: EOF
buffer (last 100 chars): : Permission denied
mv: try to overwrite `/etc/ssh/sshd_config', overriding mode 0600 (rw-------)? 
before (last 100 chars): : Permission denied
mv: try to overwrite `/etc/ssh/sshd_config', overriding mode 0600 (rw-------)? 
after: <class 'pexpect.TIMEOUT'>
match: None
match_index: None
exitstatus: None
flag_eof: False
pid: 3612
child_fd: 4
closed: False
timeout: 30
delimiter: <class 'pexpect.EOF'>
logfile: None
logfile_read: None
logfile_send: None
maxread: 2000
ignorecase: False
searchwindowsize: None
delaybeforesend: 0.05
delayafterclose: 0.1
delayafterterminate: 0.1

If I remove the /etc/ssh/sshd_config file on the remote server before running the script I get:

server1:ssh-pexpect user$ python test4.py  
id_rsa.pub                                    100%  410     0.4KB/s   00:00    
sshd_config                                   100% 3498     3.4KB/s   00:00    


chown: missing operand
Try `chown --help' for more information.
chown: changing ownership of `/tmp/sshd_config': Operation not permitted
mkdir: cannot create directory `/root/.ssh': Permission denied
chown: cannot access `/root/.ssh': Permission denied
chmod: cannot access `/root/.ssh': Permission denied
mv: accessing `/root/.ssh/authorized_keys': Permission denied
mv: cannot move `/tmp/sshd_config' to `/etc/ssh/sshd_config': Permission denied
bash: service: command not found
Connection to 192.168.1.1 closed.

I'm not even sure how to debug this to see where it's messing up. I don't think it's parsing COMMAND2 properly, though. Pretty new to Python so any advice is appreciated. Thanks.


Solution

  • You have COMMAND2 in double quotes and correctly escape any embedded double quotes, but you also need to double-escape any already-escaped double quotes. In other words, not really a Python problem. You could switch to Python triple quotes for the outermost quotes, though. It would be easier to read, too.

    Edit: Actually, any proper disambiguation of the quoting will do. Since the shell offers single quotes as well, your solution with single quotes is fine. Python would allow you to use single quotes or a number of other quoting facilities, which I would recommend if you hadn't already solved the problem (because then you can choose quotes which don't require any changes to the string itself; less room for error).

    So, any of these should be fine:

    COMMAND2='ssh -o StrictHostKeyChecking=no -t %s@%s "su - root -c \"chown root:root /tmp/id_rsa.pub; chmod 600 /tmp/id_rsa.pub; chown root:root /tmp/sshd_config; mkdir /root/.ssh; chown root:root /root/.ssh; chmod 700 /root/.ssh; mv /tmp/id_rsa.pub /root/.ssh/authorized_keys; mv /tmp/sshd_config /etc/ssh/; service sshd reload\""' % (USER, HOST)
    
    COMMAND2="""ssh -o StrictHostKeyChecking=no -t %s@%s "su - root -c \"chown root:root /tmp/id_rsa.pub; chmod 600 /tmp/id_rsa.pub; chown root:root /tmp/sshd_config; mkdir /root/.ssh; chown root:root /root/.ssh; chmod 700 /root/.ssh; mv /tmp/id_rsa.pub /root/.ssh/authorized_keys; mv /tmp/sshd_config /etc/ssh/; service sshd reload\"" """ % (USER, HOST)
    
    COMMAND2="ssh -o StrictHostKeyChecking=no -t %s@%s 'su - root -c \"chown root:root /tmp/id_rsa.pub; chmod 600 /tmp/id_rsa.pub; chown root:root /tmp/sshd_config; mkdir /root/.ssh; chown root:root /root/.ssh; chmod 700 /root/.ssh; mv /tmp/id_rsa.pub /root/.ssh/authorized_keys; mv /tmp/sshd_config /etc/ssh/; service sshd reload\"'" % (USER, HOST)
    

    I needed to add a space to the triple-quotes in order to disambiguate the adjacent double quote. But you could use triple single quotes instead. Also, triple quotes (single or double) allow you to embed newlines, which improves legibility significantly:

    COMMAND2='''ssh -o StrictHostKeyChecking=no -t %s@%s "su - root -c '
        chown root:root /tmp/id_rsa.pub
        chmod 600 /tmp/id_rsa.pub
        chown root:root /tmp/sshd_config
        mkdir /root/.ssh
        chown root:root /root/.ssh
        chmod 700 /root/.ssh
        mv /tmp/id_rsa.pub /root/.ssh/authorized_keys
        mv /tmp/sshd_config /etc/ssh/
        service sshd reload'"''' % (USER, HOST)