小插曲python
前幾節講了paramiko模塊,可是pexpect模塊的功能幾乎跟paramiko同樣,先來分析一下:linux
1.各自介紹nginx
pexpect是一個經過啓動子程序,使用正則表達式對程序輸出作出特定響應,以此實現與其自動交互的python模塊。正則表達式
paramiko是一個基於python實現的ssh遠程安全鏈接,用於ssh遠程執行命令,文件傳輸等功能的ssh客戶端模塊。shell
2.功能區別windows
pexpect須要藉助linux下的ssh命令方式登錄,實現相似scp的拷貝文件功能。緩存
paramiko沒法實現相似pexpect的interact方法進行交互式命令輸入,而且自身不支持多線程。安全
3.使用場景bash
使用python來做自動化登錄,並執行命令服務器
pexpect模塊的使用偏向於偏交互式的使用,如:管理員須要登錄100臺機器,每次都會手動執行不一樣的命令,若是經過ssh的命令進行登錄,這樣每次輸入不一樣的密碼,用戶,主機地址等信息會是一個不小的工做量,用pexpect這個模塊,就能夠解決問題。
paramiko模塊實現ssh的登錄探測,由於其更加通用而且兼容各類ssh協議的操做系統和相關的環境,而且能夠實現相似sftp登錄驗證。
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
pexpect能夠理解成Linux下的expect的python封裝,經過expect咱們能夠實現ssh,ftp,telnet等命令行進行自動交互,包括輸入主機地址,用戶名,密碼,上傳文件等,待出現異常咱們還能夠進行嘗試自動處理。
pexpect安裝:pip install pexcept 便可
pexpect的核心組件:
1.spawn類
spawn在windows沒法運行,在linux上能夠運行
spawn是pexpect的主要類接口,功能是啓動和控制子應用程序,如下是它的構造函數:
class pexpect.spawn(command,args=[],timeout=30,maxread=2000,searchwindowsize=None,logfile=None,cwd=None,env=None,ignore_sighup=True)
參數說明:
child = pexpect.spawn('/usr/bin/ftp') #啓動ftp客戶端命令 child = pexpect.spawn('/usr/bin/ssh user@example.com') #啓動ssh遠程鏈接命令 child = pexpect.spawn('ls -latr /tmp') #運行ls顯示/tmp目錄內容命令
當子程序須要參數時:
child = pexpect.spawn('/usr/bin/ftp',[]) child = pexpect.spawn('/usr/bin/ssh',['user@example.com']) child = pexpect.spawn('ls',['-latr','/tmp'])
須要注意的是,pexpect不會解析shell命令當中的元字符,包括重定向 ">",管道 "|",通配符 "*",固然能夠經過將存在這三個特殊元字符命令做爲/bin/bash的參數進行調用,像這樣:
child = pexpect.spawn('/bin/bash -c "ls -l | grep LOG > logs.txt"')
child.expect(pexpect.EOF)
也能夠經過將命令的參數以python列表的形式進行替換:
shell_cmd = 'ls -l | grep LOG > logs.txt'
child = pexpect.spawn('/bin/bash',['-c',shell_cmd]) child.expect(pexpect.EOF)
有時候調試代碼,須要獲取pexpect的輸入與輸出信息,以便了解匹配狀況。pexpect提供了兩種途徑,一種爲寫到日誌文件,另外一種爲輸出到標準輸出。
寫到日誌文件的實現方法以下:
child = pexpect.spawn(command) file = open('mylog.txt','w') child.logfile = file
獲取標準輸出的方法以下:
child = pexpect.spawn(command) child.logfile_read = sys.stdout
獲取發送的內容:
child.logfile_send = sys.stdout
上面的語句僅僅在屏幕上打印向程序發送的內容。
下面代碼實現ssh登錄,登錄成功後顯示/home目錄文件清單,並經過日誌文件記錄全部的輸入與輸出:
import pexpect import sys child = pexpect.spawn('ssh root@192.168.0.132') file = open('mylog.txt','w') child.logfile = file #child.logfile = sys.stdout
child.expect("password: ") child.sendline("12580") child.expect('#') child.sendline('ls /home') child.expect('#')
下面是mylog.txt日誌的內容,不過默認是二進制的方式存儲:
上面代碼反覆使用expect,read方法,這裏來介紹一下:
(1)expect方法
expect定義了一個子程序輸出的匹配規則。
expect(pattern_list,timeout=-1,searchwindowsize=-1)
參數解釋:
process.expect('[Nn]ame')
上面的代碼表示:匹配 process 這個句柄(表明 spawn 方法的例子中咱們啓動的 ftp 鏈接)中的 name 關鍵字,其中 n 不分大小寫。
上面的關鍵字一旦匹配,就會返回0表示匹配成功,可是若是一直匹配不到呢?默認是會一直等下去,可是若是設置了 timeout 的話就會超時。
2.匹配一系列輸出
實際上, expect() 能夠匹配一系列輸出,經過檢查匹配到的輸出,咱們能夠作不一樣的事情。好比以前 spawn 的 ftp 鏈接,若是咱們輸入用戶名以後有不一樣的狀況,就能夠經過監控這些不一樣狀況來作不一樣的動做,好比:
index = process.expect([ 'Permission Denied', 'Terminal type', 'ftp>', ]) if index == 0: print "Permission denied at host, can't login." process.kill(0) elif index == 1: print "Login ok, set up terminal type…" process.sendline('vty100') process.expect("ftp>") elif index == 2: print "Login Ok, please send your command" process.interact()
上面的代碼中,expect 方法中的是一個列表,列表中的每一個元素都是一個關鍵字的正則表達式,也就是說咱們期待這 3 種狀況之一,而 expect 返回一個順序值來表明我匹配到了哪個元素(也就是發生了哪一種狀況了),這個順序值是從 0 開始計算的。
當expect以後,下面的 if 語句就開始處理這 3 種狀況了:
另外有一種特殊狀況,若是同時有2個被匹配到,那麼怎麼辦?簡單來講就是這樣:
給個例子:
import pexpect child = pexpect.spawn("echo 'foobar'") print(child.expect(['bar','foo','foobar'])) 輸出:1,即 'foo' 被匹配
使用技巧
若是要檢查或者匹配 expect.EOF 和 expect.TIMEOUT 這兩種情形,那麼必須將它們放進匹配列表裏面去,這樣能夠經過檢查返回的數字來處理它們。若是沒放進列表的話,就會發生 EOF 或者 TIMEOUT 錯誤,程序就會中途中止了
匹配規則中有些特殊語法,好比下面的規則中前 2 個匹配都是大小寫無關的,關鍵就是這個 (?i) 匹配規則,它至關於 re.IGNORE 或者 re.I 這個關鍵字,由於畢竟不是真正的正則表達式引擎,因此 pexpect 使用這樣特殊語法:
child.expect(['(?i)etc', '(?i)readme', pexpect.EOF, pexpect.TIMEOUT])
(2)send方法
下面這些輸出的方法的做用都是向子程序發送響應命令,能夠理解成代替了咱們的標準輸入:
1.send() 用來向程序發送指定的字符串
2.sendline() 用來發送帶回車符的字符串
sendline() 和 send() 惟一的區別就是在發送的字符串後面加上了回車換行符,這也使它們用在了不一樣的地方:
它也會返回發送的字符數量
3.sendcontrol() 發送控制信號
sendcontrol()向子程序發送控制字符,好比 "ctrl+c","ctrl+d"...能夠這樣寫
process.sendcontrol('c')
4.sendeof() 發送EOF信號
5.interact() 將控制權交給用戶
interact() 表示將控制權限交給用戶(或者說標準輸入)。通常狀況下 pexpect 會接管全部的輸入和輸出,但有的時候仍是但願用戶介入,或者僅僅是爲了完成一部分工做的時候, interact() 就頗有用了。
好比:
使用方法:
# escape_character 就是當用戶輸出這裏指定的字符之後表示本身的操做完成了,將控制權從新交給 pexpect
process.interact(escape_character='\x1d', input_filter=None, output_filter= None)
詳細來講,這個方法將控制權交給用戶(或者說用戶操做的鍵盤),而後簡單的將標準輸出、標準錯誤輸出和標準輸入綁定到系統上來。
經過設置 escape_character 的值,能夠定義返回碼,默認是 <kbd>ctrl+]</kbd> 或者說 <kbd>^]</kbd>,當輸入了返回碼之後,腳本會將控制權從用戶那裏從新拿回來,而後繼續向下執行。
若是想中途關閉子程序,那麼能夠用 close 來完成,調用這個方法後會返回這個程序的返回值。
若是設置 force=True 會強行關閉這個程序,大概的過程就是先發送 SIGHUP 和 SIGINT 信號,若是都無效的話就發 SIGKILL 信號,反正無論怎麼樣都會保證這個程序被關閉掉。
屢次調用這個方法是容許的,可是不保證每次都能返回正確的返回值。儘可能不要這麼作,若是想保證程序被關閉的話只要設置force的值就能夠了。
下面是實例:
process.close(force=True)
7.cwd 指定命令執行的目錄
默認值: None 或者說 ./
cwd 用來指定命令發送的命令在哪一個路徑下執行,它通常是用在 send() 系列命令中,好比在 Linux 中,你想在 /etc 目錄下執行 ls –l 命令,那麼徹底不須要用sendline("cd /etc &&ls -l") 這樣的方式,而是用 sendline("ls -l",cwd="/etc") 就能夠了。
8.env 指定環境變量
默認值: None
指定環境變量的值,這個值是一個字典,若是你發送的命令要使用一些環境變量,那麼能夠在這裏提供
(3)特殊變量
1.pexpect.EOF 匹配終止信號
EOF 變量使用範圍很普遍,好比檢查 ssh/ftp/telnet 鏈接是否終止啊,文件是否已經到達末尾啊。 pexpect 大部分腳本的最後都會檢查 EOF 變量來判斷是否是正常終止和退出,好比下面的代碼:process.expect("ftp>") process.sendline("by") process.expect(pexpect.EOF) print("ftp connect terminated.")
2.pexpect.TIMEOUT 匹配超時信號
TIMEOUT 變量用來匹配超時的狀況,默認狀況下 expect 的超時時間是 60 秒,若是超過 60 秒尚未發現期待的關鍵字,就會觸發這個行爲,好比:# 匹配pexpect.TIMEOUT的動做,只有超時事件發生的時候纔會有效 index = process.expect(['ftp>', pexpect.TIMEOUT],) if index == 1: process.interactive() ; # 將控制權交給用戶 elif index == 2: print "Time is out." process.kill(0) ; # 殺掉進程 # 那麼怎麼改變超時時間呢?其實能夠修改spawn對象裏的timeout參數: # 下面的例子僅僅加了一行,這樣就改變了超時的時間了 process.timeout = 300 ; # 注意這一行 index = process.expect(['ftp>', pexpect.TIMEOUT],) if index == 1: process.interactive() ; # 將控制權交給用戶 elif index == 2: print "Time is out." process.kill(0) ; # 殺掉進程
3.process.before/after/match 獲取程序運行輸出
當 expect() 過程匹配到關鍵字(或者說正則表達式)以後,系統會自動給3個變量賦值,分別是 before, after 和 match
若是 expect() 過程當中發生錯誤,那麼 before 保存到目前位置緩存裏的全部數據, after 和 match 都是 None
pexpect.run(command,timeout=-1,withexitstatus=False,events=None,extra_args=None,logfile=None,cwd=None,env=None)
參數說明:
spawn方式的例子以下:
from pexpect import * child = spawn('scp foo root@192.168.0.132:.') child.expect('(?!)password') child.sendline(mypassword)
使用run函數的話:
from pexpect import * run('scp foo root@192.168.0.132:.',events={'(?!)password':mypassword})
(5)pxssh類
pxssh是pexpect的派生類,針對在ssh會話操做上再作一層封裝,提供與基類更加直接的操做方法。
class pexpect.pxssh.pxssh(timeout=30,maxread=2000,searchwindowsize=None,logfile=None,cwd=None,env=None)
pxssh經常使用的三個方法以下:
下面使用pxssh類實現一個ssh鏈接遠程主機並執行命令的示例。首先使用login()方法與遠程主機創建鏈接,再經過sendline()方法發送執行的命令,prompt()方法等待命令執行結束且出現系統提示符,最後使用logout()方法端口鏈接。
from pexpect import pxssh
import getpass
try:
#調用構造函數,建立一個pxssh類的對象
s = pxssh.pxssh()
hostname = input('hostname: ')
username = input('username: ')
password = getpass.getpass('password: ')
#利用pxssh類的login方法進行ssh登錄,原始prompt爲'$','#'或'>'
s.login(hostname, username, password,original_prompt='[$#>]')
s.sendline('uptime') #運行uptime命令
s.prompt() #匹配系統提示符
print(s.before) #打印出現系統提示符前的命令輸出
s.sendline('ls -l')
s.prompt()
print(s.before)
s.sendline('df')
s.prompt()
print(s.before)
s.logout() #斷開ssh鏈接
except pxssh.ExceptionPxssh as e:
print("pxssh failed on login.")
print(e)
Pexpect應用示例
1.實現一個自動化FTP操做
使用pexpect模塊的spawn()方法執行FTP命令,經過expect()方法定義匹配的輸出規則,sendline()方法執行相關FTP交互命令。
from __future__ import unicode_literals #使用unicode編碼
import pexpect
import sys
#運行ftp命令
child = pexpect.spawn('ftp 192.168.0.132')
#(?i)標識後面的字符串正則匹配忽略大小寫
child.expect('(?i)name .*: ')
#輸入ftp帳號信息
child.sendline('帳號')
#匹配密碼輸入提示
child.expect('(?i)password')
#輸入ftp密碼
child.sendline('密碼')
child.expect('ftp> ')
#啓用二進制傳輸模式
child.sendline('bin')
child.expect('ftp> ')
#下載robots.txt
child.sendline('get robot.txt')
child.expect('ftp> ')
#輸出匹配的ftp > 以前的輸入與輸出
sys.stdout.write(child.before.decode('utf-8'))
print("Escape character is '^]'. \n")
sys.stdout.write(child.after.decode('utf-8'))
sys.stdout.flush()
#調用interact讓出控制權,用戶能夠繼續當前的會話手工控制子程序,默認輸入「^]」字符跳出
child.interact()
child.sendline('bye')
child.close()
運行結果以下:
import pexpect import sys ip="192.168.0.132" #定義目標主機 user="root" #目標主機用戶 passwd="123456" target_file="/data/logs/nginx.log" #目標主機nginx日誌文件 child=pexpect.spawn('/usr/bin/ssh', [user+'@'+ip]) #運行ssh命令 fout=open('mylog.txt','wb') #輸入,輸出日誌寫入mylog.txt文件 child.logfile=fout try: child.expect('(?i)password') #匹配password字符串,(?!)表示不區分大小寫 child.sendline(passwd) child.expect('#') child.sendline('tar -zcf /data/logs/nginx.tar.gz ' +target_file) #打包nginx日誌文件 child.expect('#') print(child.before) child.sendline('exit') fout.close() except EOF: #定義EOF異常處理 print("expect EOF") except TIMEOUT: #定義TIMEOUT異常處理 print("expect TIMEOUT") child=pexpect.spawn('/usr/bin/scp', [user+'@'+ip+':/data/logs/nginx.tar.gz','/home']) #啓動scp遠程拷貝命令,實現將打包好的nginx日誌複製到/home目錄下 fout=file('mylog.txt','a') child.logfile=fout try: child.expect('(?i)password') child.sendline(passwd) child.expect(pexpect.EOF) #匹配緩衝區EOF(結尾),保證文件複製正常完成
except EOF: print("expect EOF") except TIMEOUT: print("expect TIMEOUT")
3.完整實現ftp登錄,下載文件到本地
# coding=UTF-8 import pexpect # 即將 ftp 所要登陸的遠程主機的域名 ipAddress = '192.168.0.132' # 登陸用戶名 loginName = 'root' # 用戶名密碼 loginPassword = '12580' # 拼湊 ftp 命令 cmd = 'ftp ' + ipAddress # 利用 ftp 命令做爲 spawn 類構造函數的參數,生成一個 spawn 類的對象 child = pexpect.spawn(cmd) # 指望具備提示輸入用戶名的字符出現 index = child.expect(["(?i)name", "(?i)Unknown host", pexpect.EOF, pexpect.TIMEOUT]) # 匹配到了 "(?i)name",代表接下來要輸入用戶名 if index == 0: # 發送登陸用戶名 + 換行符給子程序. child.sendline(loginName)
# 指望 "(?i)password" 具備提示輸入密碼的字符出現. index = child.expect(["(?i)password", pexpect.EOF, pexpect.TIMEOUT]) # 匹配到了 pexpect.EOF 或 pexpect.TIMEOUT,表示超時或者 EOF,程序打印提示信息並退出. if (index != 0): print("ftp login failed") child.close(force=True) # 匹配到了密碼提示符,發送密碼 + 換行符給子程序. child.sendline(loginPassword) # 指望登陸成功後,提示符 "ftp>" 字符出現. index = child.expect( ['ftp>', 'Login incorrect', 'Service not available', pexpect.EOF, pexpect.TIMEOUT]) # 匹配到了 'ftp>',登陸成功. if (index == 0): print('Congratulations! ftp login correct!') # 發送 'bin'+ 換行符給子程序,表示接下來使用二進制模式來傳輸文件. child.sendline("bin") print('getting a file...') # 向子程序發送下載文件 rmall 的命令. child.sendline("get ftptest.txt") # 指望下載成功後,出現 'Transfer complete.*ftp>',其實下載成功後, # 會出現如下相似於如下的提示信息: # 200 PORT command successful. # 150 Opening data connection for rmall (548 bytes). # 226 Transfer complete. # 548 bytes received in 0.00019 seconds (2.8e+03 Kbytes/s) # 因此直接用正則表達式 '.*' 將 'Transfer complete' 和提示符 'ftp>' 之間的字符全省去. index = child.expect( ['Transfer complete.*ftp>', pexpect.EOF, pexpect.TIMEOUT] ) # 匹配到了 pexpect.EOF 或 pexpect.TIMEOUT,表示超時或者 EOF,程序打印提示信息並退出. if (index != 0): print("failed to get the file") child.close(force=True) # 匹配到了 'Transfer complete.*ftp>',代表下載文件成功,打印成功信息,並輸入 'bye',結束 ftp session. print('successfully received the file') child.sendline("bye") # 用戶名或密碼不對,會先出現 'Login incorrect',而後仍會出現 'ftp>',可是 pexpect 是最小匹配,不是貪婪匹配, # 因此若是用戶名或密碼不對,會匹配到 'Login incorrect',而不是 'ftp>',而後程序打印提示信息並退出. elif (index == 1): print("You entered an invalid login name or password. Program quits!") child.close(force=True) # 匹配到了 'Service not available',通常代表 421 Service not available, remote server has # closed connection,程序打印提示信息並退出. # 匹配到了 pexpect.EOF 或 pexpect.TIMEOUT,表示超時或者 EOF,程序打印提示信息並退出. else: print("ftp login failed! index = " + index) child.close(force=True) # 匹配到了 "(?i)Unknown host",表示 server 地址不對,程序打印提示信息並退出 elif index == 1 : print("ftp login failed, due to unknown host") child.close(force=True) # 匹配到了 pexpect.EOF 或 pexpect.TIMEOUT,表示超時或者 EOF,程序打印提示信息並退出 else: print("ftp login failed, due to TIMEOUT or EOF") child.close(force=True)
輸出結果:
4.ssh登錄並執行命令,獲取命令返回結果
#coding=utf-8
""" This runs a command on a remote host using SSH. At the prompts enter hostname, user, password and the command. """ import pexpect import getpass import os import traceback #user: ssh 主機的用戶名 #host:ssh 主機的域名 #password:ssh 主機的密碼 #command:即將在遠端 ssh 主機上運行的命令 def ssh_command (user, host, password, command): """ This runs a command on the remote host. This could also be done with the pxssh class, but this demonstrates what that class does at a simpler level. This returns a pexpect.spawn object. This handles the case when you try to connect to a new host and ssh asks you if you want to accept the public key fingerprint and continue connecting. """ ssh_newkey = 'Are you sure you want to continue connecting' # 爲 ssh 命令生成一個 spawn 類的子程序對象. child = pexpect.spawn('ssh -l %s %s %s'%(user, host, command)) i = child.expect([pexpect.TIMEOUT, ssh_newkey, 'password: ']) # 若是登陸超時,打印出錯信息,並退出. if i == 0: # Timeout print('ERROR!') print('SSH could not login. Here is what SSH said:') print(child.before, child.after) return None # 若是 ssh 沒有 public key,接受它. if i == 1: # SSH does not have the public key. Just accept it. child.sendline('yes') child.expect('password: ') i = child.expect([pexpect.TIMEOUT, 'password: ']) if i == 0: # Timeout print('ERROR!') print('SSH could not login. Here is what SSH said:') print(child.before, child.after) return None # 輸入密碼. child.sendline(password) return child if i == 2: # 輸入密碼. child.sendline(password) return child def main (): # 得到用戶指定 ssh 主機域名. host = input('Hostname: ') # 得到用戶指定 ssh 主機用戶名. user = input('User: ') # 得到用戶指定 ssh 主機密碼. password = getpass.getpass() # 得到用戶指定 ssh 主機上即將運行的命令. command = input('Enter the command: ') child = ssh_command(user, host, password, command) # 匹配 pexpect.EOF child.expect(pexpect.EOF,timeout=180) # 輸出命令結果. print(child.before.strip().decode('utf-8')) if __name__ == '__main__': try: main() except Exception: print("ERROR!") traceback.print_exc() os._exit(1)
執行結果: