使用paramiko远程执行命令、下发文件

写部署脚本时,不免涉及到一些远程执行命令或者传输文件。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')

总结

相较于shparamiko好用的不是一星半点,这里只是提供了一个简单的封装,paramiko自己还有不少其余用法,欢迎你们积极讨论。

以上只是本人的一点理解,若是有错误之处,欢迎指正。

相关文章
相关标签/搜索