paramiko
*paramiko需要PyCrypto模塊的支持
paramiko支持通過SSH協議進行一些操作,比如遠程執行命令,上下傳文件等等
用法:
① 遠程命令
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