寫部署腳本時,不免涉及到一些遠程執行命令或者傳輸文件。html
以前一直使用sh庫,調用sh.ssh遠程執行一些命令,sh.scp傳輸文件,可是實際使用中仍是比較麻煩的,光是模擬用戶登錄這一點,還須要單獨定義方法模擬輸入。
感覺一下:python
from sh import ssh PASS = 'xxxx' def ssh_interact(line, stdin): line = line.strip() print(line) if line.endswith('password:'): stdin.put(PASS) ssh('x.x.x.x', _out=ssh_interact)
來自官方文檔nginx
後來發現paramiko庫更加優雅、便捷,因此準備用pramiko替換掉sh。git
以前經過同事瞭解到,paramiko在遠程執行python腳本時,腳本中的輸出內容可能會經過stderr這個管道輸出出來,因此直接用paramiko的SSHClient類中的exec_command方法執行,經過讀stderr管道中有無輸出來判斷命令是否成功執行的方式是行不通的。因此用更底層一些的Channel類的recv_exit_status方法判斷執行退出碼更好一些。github
能夠經過使用pip install paramiko
安裝,細節這裏再也不贅述。session
首先定義幾個異常ssh
# coding: utf-8 import os.path from paramiko import SSHClient, AutoAddPolicy, AuthenticationException class ConnectError(Exception): """ 鏈接錯誤時拋出的異常 """ pass class RemoteExecError(Exception): """ 遠程執行命令,失敗時拋出的異常 """ pass class SCPError(Exception): """ 遠程下發文件時拋出的異常 """ pass
... class Remote(object): def __init__(self, host, username, password=None, port=22, key_filename=None): self.host = host self.username = username self.password = password self.port = port self.key_filename = key_filename self._ssh = None def _connect(self): self._ssh = SSHClient() self._ssh.set_missing_host_key_policy(AutoAddPolicy()) try: if self.key_filename: self._ssh.connect(self.host, username=self.username, port=self.port, key_filename=self.key_filename) else: self._ssh.connect(self.host, username=self.username, password=self.password, port=self.port) except AuthenticationException: self._ssh = None raise ConnectionError('鏈接失敗,請確認用戶名、密碼、端口或密鑰文件是否有效') except Exception as e: self._ssh = None raise ConnectionError('鏈接時出現意料外的錯誤:%s' % e) def get_ssh(self): if not self._ssh: self._connect() return self._ssh
實例化SSHClient
類,經過它的connect()
方法獲取SSH鏈接。ui
須要注意的是,遠程訪問的主機如果第一次鏈接,屬於未知設備須要認證,經過set_missing_host_key_policy()
方法設置一種策略,這裏使用的是AutoAddPolicy()
。code
這裏的_connect
支持兩種方式登陸,一種是提供主機的用戶名密碼,另外一種是經過密鑰文件。在鏈接時檢查若是指定了密鑰文件則使用這種方式登陸,不然經過用戶名密碼登陸。orm
_connect()
雖然是實際的創建鏈接的方法,但實際對外接口是get_ssh()
,若是已經有創建好的SSH鏈接直接返回,避免重複創建鏈接。
class Remote(object): ... def ssh(self, cmd, root_password=None, get_pty=False, super=False): cmd = self._prepare_cmd(cmd, root_password, super) stdout = self._exec(cmd, get_pty) return stdout def _prepare_cmd(self, cmd, root_password=None, super=False): if self.username != 'root' and super: if root_password: cmd = "echo '{}'|su - root -c '{}'".format(root_password, cmd) else: cmd = "echo '{}'|sudo -p '' -S su - root -c '{}'".format(self.password, cmd) return cmd def _exec(self, cmd, gty_pty=False): channel = self.get_ssh().get_transport().open_session() if get_pty: channel.get_pty() channel.exec_command(cmd) stdout = channel.makefile('r', -1).readlines() stderr = channel.makefile_stderr('r', -1).readlines() ret_code = channel.recv_exit_status() if ret_code: msg = ''.join(stderr) if stderr else ''.join(stdout) raise RemoteExecError(msg) return stdout
在遠程執行某些命令時,可能須要管理員權限,這種時候須要作一些判斷,首先判斷登陸提供的用戶名若是不是root,則須要對命令作一些修改。這裏的修改有兩種狀況,一是,該普通用戶自己就有sudo
權限,只須要把執行的命令加到sudo以後執行就能夠,還有一種是普通用戶沒有sudo
權限,須要經過su
先切換到root
身份以後再執行,這種狀況下須要提供root
密碼。
還有一點要注意的是get_pty
這個參數,實際在遠程執行sudo
命令時,通常主機都會須要經過tty
才能執行,經過把get_pty
值設置爲True
,能夠模擬tty
,可是隨之而來也會有一個問題,若是是遠程執行一個須要長期運行的進程,例如啓動nginx
服務,當遠程命令執行後SSH退出以後,這次運行的全部程序也會隨之結束,因此在須要經過遠程命令運行某些服務或程序時,是不能指定get_pty
參數的;但同時,若是是普通用戶遠程登陸,是沒有權限執行service
命令的。建議的一種方式是修改/etc/sudoers
配置文件,註釋掉Defaults requiretty
這行。
class Remote(object): ... def scp(self, local_file, remote_path): if not os.path.exists(local_file): raise SCPError("Local %s isn't exists" % local_file) if not os.path.isfile(local_file): raise SCPError("%s is not a File" % local_file) sftp = self.get_ssh().open_sftp() try: sftp.put(local_file, remote_path) except Exception as e: raise SCPError(e)
先確認要下發的文件存在,而且是文件不是目錄,若是不是則拋出異常。同時,remote_path
須要是遠程主機的文件絕對目錄,例如/tmp/xxx.xxx
,而不能是/tmp
。
# coding: utf-8 from remote_client import RemoteClient rc = RemoteClient('10.1.100.1', 'test', 'test_pass') rc.ssh('whoami') # [u'test\n'] rc.scp('/tmp/test.out', '/tmp/test.out')
相較於sh
,paramiko
好用的不是一星半點,這裏只是提供了一個簡單的封裝,paramiko
自己還有不少其餘用法,歡迎你們積極討論。
以上只是本人的一點理解,若是有錯誤之處,歡迎指正。