当前位置 主页 > 服务器问题 > win服务器问题汇总 >

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

    栏目:win服务器问题汇总 时间:2019-10-14 12:20

    写部署脚本时,难免涉及到一些远程执行命令或者传输文件。

    之前一直使用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