python运维之paramiko

来源:互联网 发布:北洋打印机软件 编辑:程序博客网 时间:2024/04/30 01:15

paramiko是使用Python语言写的一个模块,遵循SSH2协议,支持以加密和认证的方式远程连接服务器。

1. 安装

在安装paramiko前,首先要安装PyCrypto模块。安装脚本为:
git clone https://github.com/dlitz/pycrypto.gitcd pycrypto && sudo python setup.py installcd ..git clone https://github.com/paramiko/paramiko.gitcd paramiko && sudo python setup.py installcd ..sudo rm -rf pycrypto paramiko


2. 使用paramiko

paramiko API 中最为基本的类是“paramiko.SSHClient". 它提供了与服务器连接和文件传输的最基本的接口。一个最简单的例子:
import paramikossh = paramiko.SSHClient()ssh.connect('127.0.0.1', username = 'ubuntu', password='123')
它将创建一个新的SSHClient实例,然后调用“connect()”来连接本地的SSH服务,没有比这更简单的了不是吗?

HOST Keys

SSH认证另一种方法是采用密钥的方式。无论何时你使用ssh来连接一个远程的服务器,host key的信息将被自动存储在家目录下的".ssh/known_hosts"文件中。如果你通过ssh新连接一个host,将会看到以下信息:

The authenticity of host 'localhost (::1)' can't beestablished.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)? 

敲入“yes”,key的信息将被保存到“known_hosts”文件中。这些密钥很重要,因为它是与主机之间的信任机制。如果key被破坏或更改,那么客户端会拒绝连接并不会通知你,而paramiko也采用相同的规则。如果在“hnown_hosts”中没有保存相关的信息,SSHClient 默认行为是拒绝连接。如果是工作在系统反反复复安装的实验环境中时,这将变得无比的烦人。设置host key的规则调用的方法叫ssh client 实例的"set_missing_host_key_policy()",它设定了你所期望的方法来管理host key.如果你像我一样懒惰,你可以用"paramiko.AutoAddPolicy()"方法来自动接收未知的key:
import paramikossh = paramiko.SSHClient()ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())ssh.connect('127.0.0.1', username = 'ubuntu', password='123')

connect方法

connect(hostnameport=22username=Nonepassword=Nonepkey=None,
 key_filename=None,timeout=Noneallow_agent=Truelook_for_keys=True
compress=Falsesock=Nonegss_auth=False,gss_kex=False
gss_deleg_creds=Truegss_host=None, banner_timeout=None)
Parametershostname (str) – the server to connect to
port (int) – the server port to connect to
username (str) – the username to authenticate as (defaults to the current local username)
password (str) – a password to use for authentication or for unlocking a private key
pkey (.PKey) – an optional private key to use for authentication
key_filename (str) – the filename, or list of filenames, of optional private key(s) to try for authentication
timeout (float) – an optional timeout (in seconds) for the TCP connect
allow_agent (bool) – set to False to disable connecting to the SSH agent
look_for_keys (bool) – set to False to disable searching for discoverable private key files in ~/.ssh/
compress (bool) – set to True to turn on compression
sock (socket) – an open socket or socket-like object (such as a Channel) to use for communication to the target host
gss_auth (bool) – True if you want to use GSS-API authentication
gss_kex (bool) – Perform GSS-API Key Exchange and user authentication
gss_deleg_creds (bool) – Delegate GSS-API client credentials or not
gss_host (str) – The targets name in the kerberos database. default: hostname
banner_timeout (float) – an optional timeout (in seconds) to wait for the SSH banner to be presented.RaisesBadHostKeyException – if the server’s host key could not be verified
AuthenticationException – if authentication failed
SSHException – if there was any other error connecting or establishing an SSH session
socket.error – if a socket error occurred while connecting

连接到SSH服务并身份认证。host key将根据系统host key(load_system_host_keys())和本地host key(load_host_keys())来进行检查。如果在系统host key和本地host key中都没有查到相关信息,则使用未知host key策略(set_missing_host_key_policy).认证失败抛出SSHException异常。认证优先级是:
  • The pkey or key_filename passed in (if any)
  • Any key we can find through an SSH agent
  • Any “id_rsa”, “id_dsa” or “id_ecdsa” key discoverable in ~/.ssh/
  • Plain username/password auth, if a password was given
key_filename和pkey只要一个就行。如果私钥需要密码来解锁,则要指明password参数,如:

ssh.connect('10.227.129.234',username='ubuntu', compress = True, key_filename='/home/ubuntu/.ssh/test.pem',password='123')

如果想隐藏password,每次必须交互输入该怎么办呢?可以看看官方给的demo里面的实例中getpass模块来隐藏输入密码:
import getpasskey_filename = '/home/ubuntu/.ssh/test.pem'try:key = paramiko.RSAKey.from_private_key_file(key_filename)except paramiko.PasswordRequiredException:password = getpass.getpass('RSA key password: ')pkey = paramiko.RSAKey.from_private_key_file(path, password)ssh = paramiko.SSHClien()ssh.connect('10.227.129.234',username='ubuntu', compress = True, pkey= pkey)
这里明确指明pkey是RSA加密方式,如果是DSS加密,则将RSAKey改成DSSKey即可,可通过私钥文件第一行中的说明信息中自动判断是RSA加密还是DSS加密。
 if isinstance(key_filename,str):            key_file=open(key_filename,'r')            key_head=key_file.readline()            key_file.seek(0)            if 'DSA' in key_head:                keytype=paramiko.DSSKey            elif 'RSA' in key_head:                keytype=paramiko.RSAKey

执行命令

exec_command(commandbufsize=-1timeout=Noneget_pty=False)

Execute a command on the SSH server. A new Channel is opened and the requested command is executed. The command’s input and output streams are returned as Pythonfile-like objects representing stdin, stdout, and stderr.

Parameterscommand (str) – the command to execute
bufsize (int) – interpreted the same way as by the built-in file()function in Python
timeout (int) – set command’s channel timeout. SeeChannel.settimeout.settimeoutReturnsthe stdin, stdout, and stderr of the executing command, as a 3-tupleRaisesSSHException:if the server fails to execute the command

现在既然已经连接好了,那么可以执行一些命令并返回命令执行结果。与其他Unix-like应用程序一样,SSH使用input、output和error来进行输入、输出和错误输出。error将被输出到标准错误输出,output输出到标准输出,如果你要将数据传回应用程序,将数据写到标准输入即可。

...>>> ssh.connect('127.0.0.1', username='jesse',password='lol')>>> stdin, stdout, stderr = ssh.exec_command("uptime")>>> type(stdin)paramiko.channel.ChannelFile>>> stdout.readlines()['13:35  up 11 days,  3:13, 4 users, load averages: 0.14 0.18 0.16\n']

这个例子中,Paramiko打开了一个新的"paramiko.Channel"实例,它代表了与远端机器连接的安全通道,Channel实例表现与python socket实例是一样的。当我们调用"exec_command()"方法时,这个Channel实例便打开,”paramiko.ChannelFile“和”file-like“实例代表了发送到和来自远端机器的数据。

从远端机器传回的ChannelFile,在标准输出和标准错误输出中使用"read()"来不断地显示内容。如果返回过多的数据而填满缓冲区,则会挂起等待程序读取。这种情况下既不是调用”readlines()“也不是"read()".如果你想在内部暂存这些数据,你可以使用”readline“来迭代。

对于系统管理的任务,同样需要对执行命令的输出结果进行分析,得益于python的强大的字符串处理能力,这简直就是小菜一碟不是吗?那就来跑跑命名吧,不过这需要一个password:

stdin, stdout, stderr = ssh.exec_command("sudo dmesg")

哦....,这里使用了sudo 命令。远程主机将在交互模式下要求我输入一个password,不用担心:

ssh.connect('127.0.0.1', username='jesse', password='lol')stdin, stdout, stderr = ssh.exec_command("sudo fdisk -l")stdin.write('lol\n')stdin.flush()data = stdout.read().splitlines()for line in data:    if line:        print line

看到了吧?我登陆到远程机器,执行了sudo命令。这里关键一点是,当需要输入密码时,我把password写到了stdin中。也许你会感到困惑,而这样是创建你自己交互shell的简单的基本的做法。你可能想用传统的Python cmd模块来执行admin命令来管理机器,在paramiko中,这很简单。在下面的例子中例句了一个简单的方法来实现:我们封装了Paramiko 各个操作到 RunCommand方法中,允许用户随心所欲添加主机(hosts),调用connect并且执行命令。

#!/usr/bin/pythonimport paramikoimport cmdclass RunCommand(cmd.Cmd):    """add_host 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 the host to the host list"""        if args:            self.hosts.append(args.split(','))        else:            print "usage: host "    def do_connect(self, args):        """runConnect 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):        """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 "    def do_close(self, args):        for conn in self.connections:            conn.close()if __name__ == '__main__':    RunCommand().cmdloop()
输出:
ssh > add_host 127.0.0.1,jesse,lolssh > connectssh > run uptimehost: 127.0.0.1: 14:49  up 11 days,  4:27, 8 users,load averages: 0.36 0.25 0.19ssh > close
这个例子仅仅是演示了伪交互式shell的概念,如果要使用它还有很多地方有待完善:更好的打印多行stdout输出,处理标准错误,添加一个结束方法,线程地处理返回的命令/数据等等。
和所有的shell一样,当需要可视化数据时,处理上限是是限制。如pssh、OSH、Fabric等工具,管理返回的数据方法都不尽相同,他们都有不同的方法来聚合来自不同主机的输出。

交互式

invoke_shell(term='vt100'width=80height=24width_pixels=0height_pixels=0)

Start an interactive shell session on the SSH server. A new Channel is opened and connected to a pseudo-terminal using the requested terminal type and size.

Parametersterm (str) – the terminal type to emulate (for example, "vt100")
width (int) – the width (in characters) of the terminal window
height (int) – the height (in characters) of the terminal window
width_pixels (int) – the width (in pixels) of the terminal window
height_pixels (int) – the height (in pixels) of the terminal windowReuturnsa new Channel connected to the remote shellRaisesSSHException:if the server fails to invoke a shell

以上都是通过paramiko模块来远程操作服务器,如果想通过paramiko模块直接用ssh协议登录到远端机器上怎么办?同样:
import paramikoimport interactive # logparamiko.util.log_to_file('/tmp/test') #create ssh connectionssh=paramiko.SSHClient()ssh.load_system_host_keys()ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())ssh.connect('10.1.6.190',port=22,username='root',password='xxxxxx',compress=True) #create interactive shell connectionchannel=ssh.invoke_shell() #creat interactive pipinteractive.interactive_shell(channel) #close connectionchannel.close()ssh.close()

文件传输

Paramiko的文件操作时通过SFTP来实现,就像ssh客户端命令执行一样。首先就像先前一样,创建一个paramiko.SSHClient实例:
import paramikossh = paramiko.SSHClient()ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())ssh.connect('127.0.0.1', username='jesse', password='lol')
这次,我们创建了与远端机器的连接之后调用"open_sftp()"命令,"open_sftp()"返回一个"paramiko.SFTPClient"的客户端实例,支持所有sftp操作(stat, put, get, mkdir, listdir, remove, rename, symlink, unlink etc.可以用dir(ftp)来查看)。 在这个例子中,我们使用"get"操作来下载远端文件”remotefile.py“到本地系统中,并重命名为"localfaile.py"。
ftp = ssh.open_sftp()ftp.get('remotefile.py', 'localfile.py')ftp.close()
写入一个文件到远端的主机中也采用同样的方法,只需将local和remote参数对调即可:
ftp = ssh.open_sftp()ftp.put( 'localfile.py','remotefile.py')ftp.close()
使用paramiko提供的sftp客户端的一个优点是对诸如 stat/chmod/chown等文件属性操作的支持。你可能会很轻松地写出如"glob.glob()"这样的函数来传输某个远端文件夹下的特定文件名模式的文件,你也可以基于文件的权限、大小等搜索文件。

然而,有一点必须要注意:相比scp(secure copy)来说,sftp作为一个协议是有一些限制的。当从远端机器上下载文件时,scp可以使用Unix通配符,而sftp需要完整的文件路径,例如:
ftp.get("*.py",".")
我们原本期望的是:下载所有名称为.py结束的文件到本地机器,但是sftp却不识别此方案:
>>> ftp.get("./*.py", '.')Traceback (most recent call last):  File "", line 1, in   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

需要注意的是,sftp的put和get函数都只支持单个文件,如果你需要传输整个目录的话,需要手动使用sftp.mkdir建立一个目录,然后自己遍历整个文件夹,并对每个文件使用put或get函数,pysftp则实现了上传下载文件夹。下面是来自stackoverflow中对paramiko上传下载文件夹问题的一个回复,他用一个SSHSession类做了一个封装:
import paramikoimport socketimport osfrom stat import S_ISDIRclass SSHSession(object):    # Usage:    # Detects DSA or RSA from key_file, either as a string filename or a    # file object.  Password auth is possible, but I will judge you for     # using it. So:    # ssh=SSHSession('targetserver.com','root',key_file=open('mykey.pem','r'))    # ssh=SSHSession('targetserver.com','root',key_file='/home/me/mykey.pem')    # ssh=SSHSession('targetserver.com','root','mypassword')    # ssh.put('filename','/remote/file/destination/path')    # ssh.put_all('/path/to/local/source/dir','/path/to/remote/destination')    # ssh.get_all('/path/to/remote/source/dir','/path/to/local/destination')    # ssh.command('echo "Command to execute"')    def __init__(self,hostname,username='root',key_file=None,password=None):        #        #  Accepts a file-like object (anything with a readlines() function)          #  in either dss_key or rsa_key with a private key.  Since I don't         #  ever intend to leave a server open to a password auth.        #        self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)        self.sock.connect((hostname,22))        self.t = paramiko.Transport(self.sock)        self.t.start_client()        keys = paramiko.util.load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))        key = self.t.get_remote_server_key()        # supposed to check for key in keys, but I don't much care right now to find the right notation        if key_file is not None:            if isinstance(key,str):                key_file=open(key,'r')            key_head=key_file.readline()            key_file.seek(0)            if 'DSA' in key_head:                keytype=paramiko.DSSKey            elif 'RSA' in key_head:                keytype=paramiko.RSAKey            else:                raise Exception("Can't identify key type")            pkey=keytype.from_private_key(key_file)            self.t.auth_publickey(username, pkey)        else:            if password is not None:                self.t.auth_password(username,password,fallback=False)            else: raise Exception('Must supply either key_file or password')        self.sftp=paramiko.SFTPClient.from_transport(self.t)    def command(self,cmd):        #  Breaks the command by lines, sends and receives         #  each line and its output separately        #        #  Returns the server response text as a string        chan = self.t.open_session()        chan.get_pty()        chan.invoke_shell()        chan.settimeout(20.0)        ret=''        try:            ret+=chan.recv(1024)        except:            chan.send('\n')            ret+=chan.recv(1024)        for line in cmd.split('\n'):            chan.send(line.strip() + '\n')            ret+=chan.recv(1024)        return ret    def put(self,localfile,remotefile):        #  Copy localfile to remotefile, overwriting or creating as needed.        self.sftp.put(localfile,remotefile)    def put_all(self,localpath,remotepath):        #  recursively upload a full directory        os.chdir(os.path.split(localpath)[0])        parent=os.path.split(localpath)[1]        for walker in os.walk(parent):            try:                self.sftp.mkdir(os.path.join(remotepath,walker[0]))            except:                pass            for file in walker[2]:                self.put(os.path.join(walker[0],file),os.path.join(remotepath,walker[0],file))    def get(self,remotefile,localfile):        #  Copy remotefile to localfile, overwriting or creating as needed.        self.sftp.get(remotefile,localfile)    def sftp_walk(self,remotepath):        # Kindof a stripped down  version of os.walk, implemented for         # sftp.  Tried running it flat without the yields, but it really        # chokes on big directories.        path=remotepath        files=[]        folders=[]        for f in self.sftp.listdir_attr(remotepath):            if S_ISDIR(f.st_mode):                folders.append(f.filename)            else:                files.append(f.filename)        print (path,folders,files)        yield path,folders,files        for folder in folders:            new_path=os.path.join(remotepath,folder)            for x in self.sftp_walk(new_path):                yield x    def get_all(self,remotepath,localpath):        #  recursively download a full directory        #  Harder than it sounded at first, since paramiko won't walk        #        # For the record, something like this would gennerally be faster:        # ssh user@host 'tar -cz /source/folder' | tar -xz        self.sftp.chdir(os.path.split(remotepath)[0])        parent=os.path.split(remotepath)[1]        try:            os.mkdir(localpath)        except:            pass        for walker in self.sftp_walk(parent):            try:                os.mkdir(os.path.join(localpath,walker[0]))            except:                pass            for file in walker[2]:                self.get(os.path.join(walker[0],file),os.path.join(localpath,walker[0],file))    def write_command(self,text,remotefile):        #  Writes text to remotefile, and makes remotefile executable.        #  This is perhaps a bit niche, but I was thinking I needed it.        #  For the record, I was incorrect.        self.sftp.open(remotefile,'w').write(text)        self.sftp.chmod(remotefile,755)

1 0