paramiko
fabric、pexpect及ansible
subprocess,shlex
sh
eval

paramiko


  • paramiko是基于Python实现的SSH2远程安全连接,支持认证及密钥方式
  • 可以实现远程命令执行、文件传输、中间SSH代理等功能
  • 相对于Pexpect(参看下面章节介绍),封装的层次更高,更贴近SSH协议的功能

官方文档

paramiko安装及使用

  • 安装
pip install paramiko

sshclient方式登陆服务器

  • 基于用户名和密码的 sshclient 方式
import paramiko

hostname = '10.211.55.3'
username = 'root'
password = 'root'

ssh = paramiko.SSHClient()
# print(ssh)
try:
    # ssh.load_system_host_keys()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    ssh.connect(hostname=hostname, username=username, password=password)
    _, stdout, stderr = ssh.exec_command('df -h')

    result = stdout.read().decode()
    # result = stdout.readlines()
    print(result)

except Exception as e:
    print(e)
finally:
    ssh.close()

如果有类似如下报错

Exception ignored in: <function BufferedFile.__del__ at 0x107557040>
Traceback (most recent call last):....
.....
TypeError: 'NoneType' object is not callable

可参看Why does paramiko sporadically raise an exception?

  • 基于公钥密钥的 SSHClient 方式
import paramiko

hostname = '10.211.55.3'
port = 22
username = 'root'

# 指定本地的RSA私钥文件
# 如果建立密钥对时设置的有密码,password为设定的密码,如无不用指定password参数
pkey = paramiko.RSAKey.from_private_key_file('~/.ssh/id_rsa', password='12345')

try:
    ssh = paramiko.SSHClient()
    ssh.connect(hostname=hostname,
                port=22,
                username=username,
                pkey=pkey)

    _, stdout, stderr = ssh.exec_command('ls -l')
    print(stdout.read().decode())

except:
    pass
finally:
    ssh.close()
  • 其他
import time
import paramiko

hostname = '10.211.55.3'
username = 'root'
password = 'root'

ssh = paramiko.SSHClient()
try:
    # ssh.load_system_host_keys()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())

    ssh.connect(hostname=hostname, username=username, password=password)
    chan = ssh.invoke_shell()
    chan.send('ls -l\n')
    output = chan.recv(1024)
    while True:
        time.sleep(0.1)
        if output.decode().endswith('# '):
            break
        else:
            output += chan.recv(1024)
    print(output.decode())

except Exception as e:
    print(e)
finally:
    ssh.close()

transport方式登陆服务器(推荐)

上面介绍的sshclient 方式是传统的连接服务器、执行命令、关闭的一个操作,多个操作需要连接多次,无法复用连接。有时候需要登录上服务器执行多个操作,比如执行命令、上传/下载文件,则可使用 transport 的方法

  • 基于用户名和密码的 transport 方式
import paramiko

hostname = '10.211.55.3'
port = 22
username = 'root'
password = 'root'
trans = paramiko.Transport((hostname, port))

try:
    trans.connect(username=username, password=password)

    # 将sshclient的对象的_transport指定为以上的trans
    ssh = paramiko.SSHClient()
    ssh._transport = trans

    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    a = ssh.exec_command("ls -l")
    print(a[1].read().decode())
    b = ssh.exec_command("ifconfig")
    print(b[1].read().decode())
except:
    pass
finally:
    trans.close()
  • 基于密钥的 Transport 方式
import paramiko

hostname = '10.211.55.3'
port = 22
username = 'root'

# 指定本地的RSA私钥文件
# 如果建立密钥对时设置的有密码,password为设定的密码,如无不用指定password参数
pkey = paramiko.RSAKey.from_private_key_file('~/.ssh/id_rsa', password='12345')
trans = paramiko.Transport((hostname, port))

try:
    trans.connect(username=username, pkey=pkey)

    # 将sshclient的对象的transport指定为以上的trans
    ssh = paramiko.SSHClient()
    ssh._transport = trans

    _, stdout, stderr = ssh.exec_command('ls -l')
    print(stdout.read().decode())

except:
    pass
finally:
    trans.close()

SSHClient类常用的方法

通常用于执行远程命令

  • connect:connect方法实现了远程SSH连接并校验
  • exec_command:远程命令执行方法,该命令的输入与输出流为标准输入(stdin)、输出(stdout)、错误(stderr)的Python文件对象
  • load_system_host_keys:加载本地公钥校验文件,默认为~/.ssh/known_host,非默认路径需要手工指定
  • set_missing_host_policy:设置连接的远程主机没有主机密钥或HostKeys对象时的策略,目前支持三种,分别是AutoAddPolicy、RejectPolicy(默认)、WarningPolicy,仅限用于SSHClient类

SFTPClient类常用方法

SFTPClient作为一个SFTP客户端对象,根据SSH传输协议的sftp会话,实现远程文件操作,比如文件上传、下载、权限、状态等操作。这里不做详细介绍,可查看本篇最后的参考及扩展阅读文章

fabric、pexpect及ansible


  • fabric模块是在paramiko基础上又做了一层封装,操作起来更方便,主要用于多台主机批量执行任务
  • pexpect是一个用来启动子程序,并使用正则表达式对程序输出做出特定响应,以此实现与其自动交互的Python模块。暂不支持Windows下的Python环境执行
  • ansible是一款自动化运维工具,ansible是一个配置管理和应用部署工具,基于python开发,集合了众多运维工具(pupet,cfengine,chef,func,fabric,saltstack)的优点。实现了批量系统配置,批量程序部署,批量运行命令等功能。ansible是基于模块工作的,本身没有批量部署的能力。真正具有批量部署的是ansible所运行的模块,ansible只是提供了一种框架

官方文档

这里不做详细介绍,可进一步查看官方文档及其他博客文章

subprocess


subprocess 模块允许你生成新的进程,连接它们的输入、输出、错误管道,并且获取它们的返回码。此模块打算代替一些老旧的模块与功能:os.systemos.spawn*
因此这里就不再介绍os.system和os.spawn*等的使用

官方文档

subprocess.run()

subprocess.run(args, *, stdin=None, input=None, stdout=None, stderr=None, capture_output=False, shell=False, cwd=None, timeout=None, check=False, encoding=None, errors=None, text=None, env=None, universal_newlines=None, **other_popen_kwargs)

等待命令执行完成后返回一个包含执行结果的CompletedProcess类的实例。除了timeout,input以及check参数外,其他的参数都会传递给Popen接口。也就是说Popen接口是核心。run方法只是外部的一个包装。
这个方法默认不捕获标准输出和标准错误

  1. run的参数
    • args:可能是一个序列(列表或元组)或字符串,如果传递一个简单的字符串,则 shell 参数必须为 True 或者该字符串中将被运行的程序名必须用简单的命名而不指定任何参数
    • stdin、stdout 、stderr :分别指定了执行的程序的标准输入、输出和标准错误文件句柄。合法的值有 PIPE 、 DEVNULL 、 一个现存的文件描述符(一个正整数)、一个现存的文件对象以及 None
    • capture_output:如果 capture_output 设为 true,stdout 和 stderr 将会被捕获。在使用时,内置的 Popen 对象将自动用 stdout=PIPE 和 stderr=PIPE 创建。stdout 和 stderr 参数不应当与 capture_output 同时提供
    • shell:如果 shell 设为 True,,则使用 shell 执行指定的指令。args可为简单字符串
    • timeout:设置命令超时时间,传递给 Popen.communicate()方法。如果发生超时,子进程将被杀死并等待。 TimeoutExpired 异常将在子进程中断后被抛出
    • input:传递给 Popen.communicate() 以及子进程的标准输入。 如果使用此参数,它必须是一个字节序列。 如果指定了 encoding 或 errors 或者将 text 设置为 True,那么也可以是一个字符串。 当使用此参数时,在创建内部 Popen 对象时将自动带上 stdin=PIPE,并且不能再手动指定 stdin 参数
    • check:如果 check 设为 True, 并且进程以非零状态码退出, 一个 CalledProcessError 异常将被抛出
    • encoding:指出了标准流的编码格式,不指定的话默认是二进制的
  2. run返回值subprocess.CompletedProcess类实例,返回后则代表一个进程结束
  • args:启动进程的参数,可是一个列表或者字符串
  • returncode:子进程的退出状态码. 通常来说, 一个为 0 的退出码表示进程运行正常
  • stdout:从子进程捕获到的标准输出
    • 如果 run() 是设置了encoding, errors 或者 text=True 来运行的,则stdout是一个字节序列, 或一个字符串
    • 如果未有捕获, 则为 None
    • 通过stderr=subprocess.STDOUT 运行,标准输入和标准错误将被组合在一起,并且 stderr将为 None
  • stderr:捕获到的子进程的标准错误
  • check_returncode:如果 returncode 非零,抛出 CalledProcessError
import subprocess

lst_file = subprocess.run(['ls', '-l'])  # 未捕获输出
print(1, lst_file)  # CompletedProcess(args=['ls', '-l'], returncode=0) 返回CompletedProcess实例
print(2, lst_file.args)
print(3, lst_file.returncode)

lst_file = subprocess.run('ls -l', shell=True)  # shell为True时,args可为字符串

lst_file_out_1 = subprocess.run(['ls', '-l'], capture_output=True)  # 捕获输出方式1
print(4, lst_file_out_1)
print(5, lst_file_out_1.stdout, lst_file_out_1.stderr, sep='\n')

lst_file_out_2 = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE, encoding='utf-8', timeout=1)  # 捕获输出方式2
print(6, lst_file_out_2.returncode, lst_file_out_2.stdout, lst_file_out_2.stderr)
try:
    lst_file_out_3 = subprocess.run(['ls', '-l'], stdout=subprocess.PIPE, text=True, timeout=0.0000001)  # 捕获输出方式3
    print(7, lst_file_out_3.stdout)
except subprocess.TimeoutExpired as e:
    print(8, e)
  1. 其他说明
    • subprocess.DEVNULL
    • subprocess.PIPE:可被 Popen 的 stdin,stdout 或者 stderr 参数使用的特殊值,表示打开标准流的管道。常用于 Popen.communicate()
    • subprocess.STDOUT:可被 Popen 的 stdin , stdout 或者 stderr 参数使用的特殊值, 表示标准错误与标准输出使用同一句柄
    • 其他请查看官方文档

Poen构造函数

此模块的底层的进程创建与管理由 Popen 类处理,更加灵活

class subprocess.Popen(args, bufsize=-1, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None, env=None, universal_newlines=None, startupinfo=None, creationflags=0, restore_signals=True, start_new_session=False, pass_fds=(), *, group=None, extra_groups=None, user=None, umask=-1, encoding=None, errors=None, text=None)

  • args:是一个程序参数的序列或者是一个单独的字符串或 path-like object。推荐用序列
  • bufsize:缓冲区大小。当创建标准流的管道对象时使用,默认-1。
    • 0:不使用缓冲区(读取与写入是一个系统调用并且可以返回短内容)
    • 1:表示行缓冲,仅当universal_newlines=True时可用,如文本模式
    • 正数:表示缓冲区大小
    • 负数:表示使用系统默认的缓冲区大小(io.DEFAULT_BUFFER_SIZE)
  • 其他:查看官方文档
import shlex, subprocess

command_line = 'ls -a /usr/local'
args = shlex.split(command_line)
print(1, args)
lst_file = subprocess.Popen(args, stdout=subprocess.PIPE, encoding='utf-8')
print(2, lst_file.args, lst_file.pid, lst_file.returncode)
print(3, lst_file.stdout)
# print(4, lst_file.stdout.read())
print(5, lst_file.stdout.readlines())

Popen对象的方法和属性

  • Popen.poll():检查子进程是否已被终止,设置并返回 returncode 属性。否则返回 None
  • Popen.wait(timeout=None):等待子进程被终止。设置并返回 returncode 属性
  • Popen.communicate(input=None, timeout=None):与进程交互:将数据发送到 stdin
  • Popen.send_signal(signal):将信号 signal 发送给子进程
  • Popen.terminate():停止子进程
  • Popen.kill():杀死子进程
  • Popen.stdin、Popen.stdout、Popen.stderr、Popen.pid、Popen.returncode
import subprocess

def cmd(command):
    subp = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, encoding="utf-8")
    subp.wait(2)
    if subp.poll() == 0:
        print(subp.communicate()[1])
    else:
        print("失败")

cmd("java -version")
cmd("exit 1")
  • 补充示例:adb devices获取设备uuid
import subprocess
import re


def get_device_uuid():
    stdout, stderr = subprocess.Popen("adb devices", shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                      text=True).communicate()

    pattern = r'([a-z-A-Z0-9]*)\s*device$'
    uuid_lst = re.findall(pattern, stdout, re.M)
    return uuid_lst


print(get_device_uuid())

可进一步参看多进程

with上下文管理Popen对象

Popen 对象支持通过 with 语句作为上下文管理器,在退出时关闭文件描述符并等待进程

with Popen(["ifconfig"], stdout=PIPE) as proc:
    log.write(proc.stdout.read())

sh


sh is a full-fledged subprocess replacement for Python 2.6 - 3.8
sh将系统的命令动态映射到python函数,通过python的方式去写shell脚本

from sh import ifconfig
print(ifconfig('en0'))

# 或者采用如下方式
import sh
print(sh.ifconfig('en0'))

官方文档

安装

pip install sh

使用简介

不支持windows,以下以mac为例

  1. 传参
    如执行如下命令 curl https://www.baidu.com -o page.html --silient
    from sh import curl
    curl("https://www.baidu.com/", "-o", "page.html", "--silent")
    # 或采用keyword传参
    curl("http:s//www.baidu.com/", o="page.html", silent=True)
    
  2. Baking

    “baking” arguments into commands

    from sh import ls
    
    ls = ls.bake("-la")
    print(ls)  # "/usr/bin/ls -la"
    # resolves to "ls -la /"
    print(ls("/"))
    
  3. 其他

eval


eval() 函数用来执行一个字符串表达式,并返回表达式的值

s = '[1, 2]'
print(s, type(s))
s = eval(s)
print(s, type(s))
s = eval('abs(-10)')
print(s)
s = eval("__import__('os').system('ls /usr/local')")
print(s)

参考及扩展阅读