SSH Programming with Paramiko | Completely Different

» SSH Programming with Paramiko | Completely Different jessenoller.com

SSH Programming with Paramiko | Completely Different

February 5th, 2009 § 20 comments

OpenSSH is the ubiq­ui­tous method of remote access for secure remote-machine login and file trans­fers. Many peo­ple — sys­tems admin­is­tra­tors, test automa­tion engi­neers, web devel­op­ers and oth­ers have to use and inter­act with it daily. Script­ing SSH access and file trans­fers with Python can be frus­trat­ing — but the Paramiko mod­ule solves that in a pow­er­ful way.

This is a reprint of an arti­cle I wrote for Python Mag­a­zine as a Com­pletely Dif­fer­ent col­umn that was pub­lished in the Octo­ber 2008 issue. I have repub­lished this in its orig­i­nal form, bugs and all

 

SSH is every­where. OS X, Linux, Solaris, and even Win­dows offer OpenSSH servers for remote access and file trans­fers. It long ago dis­placed other meth­ods of remote access like tel­net and rlogin. While those other sys­tems may still exist, their wide­spread usage has faded with the rapid adop­tion of the OpenSSH suite of tools.

OpenSSH itself is actu­ally a suite of tools based on the ssh2 pro­to­col. The suite pro­vides secure remote login tools (ssh), secure file trans­fer (scp and sftp), and key man­age­ment tools.

On most oper­at­ing sys­tems the client-side tools (ssh, scp, sftp) are already installed for users to lever­age. Users can also eas­ily install and con­fig­ure the server-side util­i­ties on sys­tems they want to remotely access.

Many, many peo­ple use OpenSSH daily, and many of them spend a lot of time try­ing to script its usage. Most of these tools and scripts try to wrap the com­mand line exe­cuta­bles (ssh, scp, etc) directly. They use things like Pex­pect to pro­vide pass­words, and try to ratio­nal­ize and parse the out­put of the bina­ries directly.

Hav­ing spent a lot of time script­ing around the bina­ries and try­ing to man­age time­outs, stan­dard out/in/error pipes, authen­ti­ca­tion, argu­ments and options all through ”sub­process”, ”popen2”, etc., I’m here to tell you wrap­ping com­mand line bina­ries is prone to error, dif­fi­cult to test, and painful to maintain.

When you’re in the busi­ness of pars­ing out­put from com­mand line util­i­ties, watch­ing for exit codes and jug­gling time­outs, you’re not on a good path. That’s where some­thing like Paramiko comes in.

I dis­cov­ered Paramiko some time ago. It builds on PyCrypto to pro­vide a Python inter­face to the SSH2 pro­to­col. The mod­ule pro­vides all of the fac­ul­ties you could ask for, includ­ing: ssh-key authen­ti­ca­tion, ssh shell access, and sftp.

Since dis­cov­er­ing Paramiko, my entire par­a­digm and usage of SSH has changed. Instead of the frus­trat­ing expe­ri­ence of shelling-out and hack­ing around the var­i­ous kinks with that, I can pro­gram­mat­i­cally access all of the pro­to­cols and tools I need in a clean, Pythonic way.

About Paramiko

Paramiko is a pure-Python mod­ule and can be easy_install’ed as other typ­i­cal python mod­ules can. How­ever, PyCrypto is writ­ten largely in C, so you may need a com­piler to install both depend­ing on your platform.

Paramiko itself has exten­sive API doc­u­men­ta­tion and an active mail­ing list. As an added bonus, there’s a Java port of it as well (don’t get me started on con­trol­ling SSH within Java) if you need some­thing to achieve the same thing in Java.

Paramiko also offers an imple­men­ta­tion of the SSH and SFTP server pro­to­cols. It really is feature-rich and com­plete. I’ve used it in heav­ily threaded appli­ca­tions as well as in day-to-day main­te­nance scripts. There’s even an instal­la­tion and deploy­ment sys­tem, named Fab­ric, that fur­ther builds on Paramiko to pro­vide appli­ca­tion deploy­ment util­i­ties via SSH.

Get­ting started

The pri­mary class of the Paramiko API is ”paramiko.SSHClient”. It pro­vides the basic inter­face you are going to want to use to instan­ti­ate server con­nec­tions and file transfers.

Here’s a sim­ple example:

? View Code PYTHON
1
2
3
4
import paramiko
ssh = paramiko.SSHClient()
ssh.connect('127.0.0.1', username='jesse', 
    password='lol')

This cre­ates a new SSH­Client object, and then calls ”con­nect()” to con­nect us to the local SSH server. It can’t get much eas­ier than that!

Host Keys

One of the com­pli­cat­ing aspects of SSH authen­ti­ca­tion is host keys. When­ever you make an ssh con­nec­tion to a remote machine, that host’s key is stored auto­mat­i­cally in a file in your home direc­tory called ”.ssh/known_hosts”. If you’ve ever con­nected to a new host via SSH and seen a mes­sage like this:

The authenticity of host 'localhost (::1)' can't be
established.
RSA key fingerprint is
22:fb:16:3c:24:7f:60:99:4f:f4:57:d6:d1:09:9e:28.
Are you sure you want to continue connecting
(yes/no)?

and typed “yes” — you’ve added an entry to the ”known_hosts” file. These keys are impor­tant because accept­ing them implies a level of trust of the host. If the key ever changes or is com­pro­mised in some way, your client will refuse to con­nect with­out noti­fy­ing you.

Paramiko enforces this same rule. You must accept and autho­rize the use and stor­age of these keys on a per-host basis. Luck­ily, rather then hav­ing to be prompted for each one, or man­age each one indi­vid­u­ally, you can set a magic policy.

The default behav­ior with an SSH­Client object is to refuse to con­nect to a host (”paramiko.RejectPolicy”) who does not have a key stored in your local ”known_hosts” file. This can become annoy­ing when work­ing in a lab envi­ron­ment where machines come and go and have the oper­at­ing sys­tem rein­stalled constantly.

Set­ting the host key pol­icy takes one method call to the ssh client object (”set_missing_host_key_policy()”), which sets the way you want to man­age inbound host keys. If you’re lazy like me, you pass in the ”paramiko.AutoAddPolicy()” which will auto-accept unknown keys.

? View Code PYTHON
1
2
3
4
5
6
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(
    paramiko.AutoAddPolicy())
ssh.connect('127.0.0.1', username='jesse', 
    password='lol')

Of course, don’t do this if you’re work­ing with machines you don’t know or trust! Tools built on Paramiko should make this overly lib­eral pol­icy a con­fig­u­ra­tion option.

Run­ning Sim­ple Commands

So, now that we’re con­nected, we should try run­ning a com­mand and get­ting some output.

SSH uses the same type of input, out­put, and error han­dles you should be famil­iar with from other Unix-like appli­ca­tions. Errors are sent to stan­dard error, out­put goes to stan­dard out, and if you want to send data back to the appli­ca­tion, you write it to stan­dard in.

So, the response data from client com­mands are going to come back in a tuple — (stdin, std­out, stderr) — which are file-like objects you can read from (or write to, in the case of stdin). For example:

? View Code PYTHON
1
2
3
4
5
6
7
8
9
...
>>> ssh.connect('127.0.0.1', username='jesse', 
...    password='lol')
>>> stdin, stdout, stderr = \
...    ssh.exec_command("uptime")
>>> type(stdin)
<class 'paramiko.ChannelFile'>
>>> stdout.readlines()
['13:35  up 11 days,  3:13, 4 users, load averages: 0.14 0.18 0.16\n']

Under the cov­ers, Paramiko has opened a new ”paramiko.Channel” object which rep­re­sents the secure tun­nel to the remote host. The Chan­nel object acts like a nor­mal python socket object. When we call ”exec_command()”, the Chan­nel to the host is opened, and we are handed back ”paramiko.ChannelFile” “file-like” objects which rep­re­sents the data sent to and from the remote host.

One of the doc­u­mented nits with the Chan­nelFile objects paramiko passes back to you is that you need to con­stantly ”read()” off of the stderr and std­out han­dles given back to you. If the remote host sends back enough data to fill the buffer, the host will hang wait­ing for your pro­gram to read more. A way around this is to either call ”read­lines()” as we did above, or ”read()”. If you need to inter­nally buffer the data, you can also iter­ate over the object with ”readline()”.

This is the sim­plest form of con­nect­ing and run­ning a com­mand to get the out­put back. For many sysad­min tasks, this will be invalu­able as you need to parse the out­put of a returned com­mand to find exactly what you need. With Python’s rich string manip­u­la­tion, this is an easy task. Let’s run some­thing with a lot of out­put, that also requires a password:

? View Code PYTHON
1
2
3
4
ssh.connect('127.0.0.1', username='jesse', 
   password='lol')
stdin, stdout, stderr = ssh.exec_command(
   "sudo dmesg")

Uh oh. I just called the sudo com­mand. It is going to require me to pro­vide a pass­word inter­ac­tively with the remote host. No worries:

? View Code PYTHON
1
2
3
4
5
6
7
8
9
10
ssh.connect('127.0.0.1', username='jesse', 
    password='lol')
stdin, stdout, stderr = ssh.exec_command(
    "sudo dmesg")
stdin.write('lol\n')
stdin.flush()
data = stdout.read.splitlines()
for line in data:
    if line.split(':')[0] == 'AirPort':
        print line

There! I logged in remotely and found all mes­sages for my Air­port card. The key thing to note here is that I wrote my pass­word to the stdin “file” so that sudo allowed me in.

If you’re won­der­ing, yes, this pro­vides an easy base to cre­ate your own inter­ac­tive shell. You might want to do some­thing like this to make a lit­tle cus­tom admin shell using the Python cmd mod­ule to admin­is­ter machines inside of your lab.

Using Paramiko, this is easy. In List­ing 1, I out­line a basic way to approach this — we wrap the Paramiko manip­u­la­tion up in the Run­Com­mand meth­ods, allow­ing the user to add as many hosts as they want, call con­nect and then run a command.

List­ing 1:

? View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#!/usr/bin/python
 
import paramiko
import cmd
 
class RunCommand(cmd.Cmd):
    """ Simple shell to run a command on the host """
 
    prompt = 'ssh > '
 
    def __init__(self):
        cmd.Cmd.__init__(self)
        self.hosts = []
        self.connections = []
 
    def do_add_host(self, args):
        """add_host <host,user,password>
        Add the host to the host list"""
        if args:
            self.hosts.append(args.split(','))
        else:
            print "usage: host <hostip,user,password>"
 
    def do_connect(self, args):
        """Connect to all hosts in the hosts list"""
        for host in self.hosts:
            client = paramiko.SSHClient()
            client.set_missing_host_key_policy(
                paramiko.AutoAddPolicy())
            client.connect(host[0], 
                username=host[1], 
                password=host[2])
            self.connections.append(client)
 
    def do_run(self, command):
        """run <command>
        Execute this command on all hosts in the list"""
        if command:
            for host, conn in zip(self.hosts, self.connections):
                stdin, stdout, stderr = conn.exec_command(command)
                stdin.close()
                for line in stdout.read().splitlines():
                    print 'host: %s: %s' % (host[0], line)
        else:
            print "usage: run <command>"
 
    def do_close(self, args):
        for conn in self.connections:
            conn.close()
 
if __name__ == '__main__':
    RunCommand().cmdloop()

Exam­ple output:

ssh > add_host 127.0.0.1,jesse,lol
ssh > connect
ssh > run uptime
host: 127.0.0.1: 14:49  up 11 days,  4:27, 8 users,
load averages: 0.36 0.25 0.19
ssh > close

This is just designed to be a proof-of con­cept of a pseudo-interactive shell. There are a few improve­ments you could make should you use it:

- Bet­ter print­ing for multi-line std­out out­put.
– Han­dle stan­dard error
– Add in a quit method
– Thread the com­mand execution/data returned.

Like all shells, the sky is the limit when it comes to data visu­al­iza­tion. Tools like pssh, OSH, Fab­ric, etc., all man­age the return data dif­fer­ently, and they all have dif­fer­ent ways of aggre­gat­ing the out­put from dif­fer­ent hosts.

File put and get

File manip­u­la­tion within Paramiko is han­dled via the SFTP imple­men­ta­tion, and, like the ssh client com­mand exe­cu­tion, it’s easy as pie.

We start by instan­ti­at­ing a new paramiko.SSHClient just as before:

? View Code PYTHON
1
2
3
4
5
6
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(
    paramiko.AutoAddPolicy())
ssh.connect('127.0.0.1', username='jesse', 
    password='lol')

This time, we make a call into ”open_sftp()” after we per­form the con­nect to the host. ”open_sftp()” returns a ”paramiko.SFTPClient” client object that sup­ports all of the nor­mal sftp oper­a­tions (stat, put, get, etc.). In this exam­ple, we per­form a “get” oper­a­tion to down­load the file ”remotefile.py” from the remote sys­tem and write it to to the local file, ”localfile.py”.


ftp = ssh.open_sftp()
ftp.get('remotefile.py', 'localfile.py')
ftp.close()

Writ­ing a file to the remote host (a “put” oper­a­tion) works the exact same way. We just trans­pose the local and remote arguments:

? View Code PYTHON
1
2
3
ftp = ssh.open_sftp()
ftp.get('localfile.py', 'remotefile.py')
ftp.close()

The nice thing about the sftp client imple­men­ta­tion that Paramiko pro­vides is that it sup­port things like stat, chmod, chown, etc. Obvi­ously these might act dif­fer­ently depend­ing on the remote server because some servers do not imple­ment all of the pro­to­col, but even so they’re incred­i­bly useful.

You could eas­ily write func­tions like ”glob.glob()” to trans­verse a remote direc­tory tree look­ing for a par­tic­u­lar file­name pat­tern. You could also search based on per­mis­sions, size, etc.

One thing to note, how­ever, and this bit me a few times: sftp as a pro­to­col is slightly more restric­tive than some­thing like nor­mal secure copy (scp). SCP allows you to use Unix wild cards in the file name when grab­bing a file from the remote machine. SFTP, on the other hand, expects the full explicit path to the file you want to down­load. An exam­ple of this is:

? View Code PYTHON
1
ftp.get('*.py', '.')

In most cases, this would mean “down­load all files with .py” to the local direc­tory on my machine. SFTP is unhappy with this for­mu­la­tion, though (see List­ing 2). I learned this the hard way, after I spent sev­eral hours pulling apart the sftp client imple­men­ta­tion out of frustration.

List­ing 2:

? View Code PYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
>>> ftp.get("./*.py", '.')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Library/Python/2.5/site-packages/paramiko/sftp_client.py", 
    line 567, in get
    fr = self.file(remotepath, 'rb')
  File "/Library/Python/2.5/site-packages/paramiko/sftp_client.py", 
    line 238, in open
    t, msg = self._request(CMD_OPEN, filename, imode, attrblock)
  File "/Library/Python/2.5/site-packages/paramiko/sftp_client.py", 
    line 589, in _request
    return self._read_response(num)
  File "/Library/Python/2.5/site-packages/paramiko/sftp_client.py", 
    line 636, in _read_response
    self._convert_status(msg)
  File "/Library/Python/2.5/site-packages/paramiko/sftp_client.py", 
    line 662, in _convert_status
    raise IOError(errno.ENOENT, text)
IOError: [Errno 2] No such file

In Clos­ing

I hope I’ve shown you enough to really dig into Paramiko. It’s one of the gems from the Python com­mu­nity that helps me on a daily basis. I can do remote admin­is­tra­tion pro­gram­mat­i­cally, write test plu­g­ins that per­form remote oper­a­tions eas­ily, and a lot more, all with­out need­ing to install extra dae­mons on the remote machines.

SSH is every­where, and sooner or later you’re going to need to write a pro­gram that inter­acts with it. Why not save your­self the trou­ble now and give Paramiko a look?

Related Links

  • OpenSSH —
http://www.openssh.com/
Pex­pect — http://www.noah.org/wiki/Pexpect
Fab­ric — http://www.nongnu.org/fab/
Paramiko Docs — http://www.lag.net/paramiko/docs/
Paramiko Mail­ing List — http://www.lag.net/mailman/listinfo/paramiko
OSHhttp://geophile.com/osh/
PSSHhttp://www.theether.org/pssh/
posted on 2012-02-16 23:39  lexus 阅读( ...) 评论( ...) 编辑 收藏

转载于:https://www.cnblogs.com/lexus/archive/2012/02/16/2355124.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值