寫部署指令碼時,難免涉及到一些遠端執行命令或者傳輸檔案。
之前一直使用sh庫,呼叫sh.ssh遠端執行一些命令,sh.scp傳輸檔案,但是實際使用中還是比較麻煩的,光是模擬使用者登陸這一點,還需要單獨定義方法模擬輸入。
感受一下:
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)複製程式碼
來自官方文件
後來發現paramiko庫更加優雅、便捷,所以準備用pramiko替換掉sh。
之前通過同事瞭解到,paramiko在遠端執行python指令碼時,指令碼中的輸出內容可能會通過stderr這個管道輸出出來,所以直接用paramiko的SSHClient類中的exec_command方法執行,通過讀stderr管道中有無輸出來判斷命令是否成功執行的方式是行不通的。所以用更底層一些的Channel類的recv_exit_status方法判斷執行退出碼更好一些。
安裝
可以通過使用pip install paramiko
安裝,細節這裡不再贅述。
封裝
首先定義幾個異常
# 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連線。
需要注意的是,遠端訪問的主機若是第一次連線,屬於未知裝置需要認證,通過set_missing_host_key_policy()
方法設定一種策略,這裡使用的是AutoAddPolicy()
。
這裡的_connect
支援兩種方式登入,一種是提供主機的使用者名稱密碼,另一種是通過金鑰檔案。在連線時檢查如果指定了金鑰檔案則使用這種方式登入,否則通過使用者名稱密碼登入。
_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
本身還有很多其他用法,歡迎大家積極討論。
以上只是本人的一點理解,如果有錯誤之處,歡迎指正。