ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())    #指定当对方主机没有本机公钥的情况时应该怎么办,AutoAddPolicy表示自动在对方主机保存下本机的秘钥
ssh.connect('ip',22,'user','passwd')    #SSH端口默认22,可改
stdin,stdout,stderr = ssh.exec_command("命令内容")    #这三个得到的都是类文件对象
outmsg,errmsg = stdout.read(),stderr.read()    #读一次之后,stdout和stderr里就没有内容了,所以一定要用变量把它们带的信息给保存下来,否则read一次之后就没有了
if errmsg == "":
    print outmsg
ssh.close()

②  文件交流

tra = paramiko.Transport(('ip',22))    #参数是一个tuple
tra.connect(username='...',password='...')    #一定要指明参数名的username和password。否则会报错str has no attribute 'get_name'
sftp = paramiko.SFTPClient.from_transport(tra)
sftp.put('本地路径','远程路径')    #上传文件
sftp.get('远程路径','本地路径')    #下载文件
tra.close()

需要注意的是在put和get方法中,两个路径都是需要完整的(要带文件名!)

sftp的put和get方法还有callback这个参数。这个参数指定一个函数对象,这个函数应该接受两个int型参数,分别代表了已上传/已下载的字节数;总的要上传/下载的字节数。利用callback的指定可以做出一个类似进度条的功能。另外,还需要注意的是callback会在传输完成之前不断地被调用但是具体是怎么样的时机下调用我不是很清楚,需要研究下paramiko的源码。但是可以确定的一点是这个callback函数也是在主线程中调用的,所以最好不要在里面写什么sleep,这样会导致整个传输过程变慢的。

■  Transport的更多扩展

今天想通过python做一个SSH模拟终端。想法非常简单,就是通过SSHClient类建立连接然后进行命令和返回的交互嘛。不过发生了很多问题,去网上一找才发现,原来SSHClient类的exec_command方法是个单session包装的方法。即调用这个方法只能执行一趟命令,执行完成之后就断开了连接,再次执行时又是新的session。比如:

ssh.exec_command('cd /tmp;pwd')的返回是/tmp,但如果把cd和pwd分成两个exec_command写的话,pwd最终返回的是HOME目录,这表明了exec_command的单会话特性。那么怎么样才能从更底层开始建立命令交互的通信? 网上小查了一种方法还是需要Transport这个我们之前在SFTP时候用的类。

tran = paramiko.Transport(sock=(ip,22))
tran.connect(username='xxx',password='xxx')
channel = tran.open_session()
channel.get_pty()
channel.invoke_shell()
channel.send('ls\n')
result = ''
while True:
    time.sleep(0.5)
    res = channel.recv(65535).decode('utf8')
    result += res
    if result:
        sys.stdout.write(result.strip('\n'))
    if res.endswith('# ') or res.endswith('$ '):
        break

通过这样的方式搭建出来的一个SSH命令通道是和Xshell这种软件建立出来的终端差不多的,比如有终端命令行提示符,也支持cd等命令。

在获取命令运行的返回(recv方法的返回)时,我们用了一个while True的逐次取数据的方式。这么做的一个好处就是当返回比较多比较大的时候可以顺利读取完全。其实这么写也是有其必要性在里面的。如果直接在这个代码的循环外面直接recv一下,返回得到的会是'\r\n'而不是ls返回的文件信息。什么原理不清楚但是既然while True这个方式有必要性又有优点的话就可以考虑用下。

跳出循环的方式是判断返回的结尾是不是终端命令提示符的结尾#+"空格"或者$+"空格",这种判断方法比较不健壮。网上也有用正则匹配或者其他的一些方法来识别返回结果读取到头了,可参考。这种方法建立的SSH通道的话,是自带命令行提示符的,而且每条命令的返回实质上是“真的返回”+"\n"+"命令提示符",所以可以做到每一个命令返回之后后面就有命令提示符。

另一方面,这个通道也并不是很万能的,比如对通过stdin进行交互要怎么做目前我还没有找到办法、因此也就也意味着对vim,crontab -e之类的对交互有需求的软件就不是支持很好了。

■  更多扩展

●  在connect方法里还有参数timeout = float设置连接超时时间

在connect方法中,还可用参数pkey指定本机私钥文件用于身份验证,内容可以是个PKey类对象。也可以用key_filename = '路径' 这个参数来指定一个文件。而在SSHClient类还有方法load_system_host_keys用于指定对方主机存放本机公钥的位置,默认不加参数的话是将这个位置设置为~/.ssh/known_hosts。SSHClient类对象connect之前,先用load_system_host_keys指定本机公钥存放位置,再在connect的时候指明本机私钥的话,就不用password,只要username就可以登录了。

●  关于set_missing_host_key_policy

这个方法的参数有三种选择,分别对应三种当对方主机没有在相关文件中找到本机的公钥时做的动作:

paramiko.AutoAddPolicy()  自动添加本机的公钥和主机名进相关文件

paramiko.RejectPolicy()  自动拒绝未知的主机名和密钥,“未知”指的是没有在相关文件中有记录的主机

paramiko.WarningPolicy()  和AutoAdd没差,只不过在出现没有记录的情况时警告一下

●  SFTPClient类除了上面提到的那些方法以外,还有:

sftp.mkdir('路径',mode)  #mode不用加引号,直接写755之类的即可,方法直接创建目录并按mode设置其权限

sftp.remove('路径')  #删除某目录

sftp.stat("文件路径")  #获取文件信息

sftp.listdir("路径")  #以列表方式返回目录下的内容

以上这些方法和exec_command执行一些特定的命令是差不多的,实践的时候也不必一棵树吊死在exec_command上,也可以适当考虑这些方法。

●  ssh.exec_command的异步原理

据我估计ssh.exec_command应该是单独开一条线程来远程执行命令,而当我们对命令的输出不感兴趣,在exec_command执行之后不调用read来收集远程返回的信息的话,那么这个线程是不阻塞的。这是个小坑的地方。比如下面两个例子,ssh是一个已经配置好的SSHClient对象:

stdin,stdout,stderr = ssh.exec_command("sleep 5s;echo foo")
stdin,stdout,stderr = ssh.exec_command("echo bar")
out,err = stdout.read(),stderr.read()
if err:
  print err
else:
  print out
#这种情况下的输出只有bar
######另一种情况###### stdin,stdout,stderr = ssh.exec_command("sleep 5s;echo foo") out,err = stdout.read(),stderr.read() stdin,stdout,stderr = ssh.exec_command("echo bar") out,err = stdout.read(),stderr.read() if err: print err else: print out #这种情况下先等5秒然后输出foo和bar

可以看到第一种情况中, 没有读取stdout等的信息,那么本地程序直接往下执行,不管之前的命令是否还未给出结果,而第二种情况,由于我要读取内容,所以必须等到上一条命令执行完成给出结果本地才继续跑下一条命令。

●  stdin的用法

之前由于基本上都是从服务器上面读数据,对于stdin这个变量一直感觉很鸡肋。直到那天要批量改服务器密码。。试了一下果然stdin是可以拿来进行write的。记得在write的时候要适当插入回车\n来模拟敲回车的过程,否则很可能会出现SSH被堵塞的情况。比如改密码这个过程:

##前面配置过程省略##
stdin,stdout,stderr = ssh.exec_command("passwd")
stdin.write("old_password\nnew_password\nnew_password\n")    #因为密码要确认,所以要输两遍
out,err = stdout.read(),stderr.read()
if err != '':
  print err
else:
  print out