测试自动化:调用shell命令

说明:本文档中的示例代码都以Python3.7.3编写。

A. pexpect模块

pexpect模块是一个比较灵活的与命令行进行交互的模块。

pexpect 是 Don Libes 的 Expect 语言的一个 Python 实现,是一个用来启动子程序,并使用正则表达式对程序输出做出特定响应,以此实现与其自动交互的 Python 模块。 Pexpect 的使用范围很广,可以用来实现与 ssh、ftp 、telnet 等程序的自动交互;可以用来自动复制软件安装包并在不同机器自动安装;还可以用来实现软件测试中与命令行交互的自动化。

Pexpect4.0以上的版本可以在Windows平台和POSIX平台上使用了,但pexpect.spawn和pexpect.run()只能在POSIX平台上用,windows平台可以用pexpect.popen_spawn.PopenSpawn,操作文件时可用pexpect.fdpexpect.fdspawn。

child = pexpect.spawn()  #启动一个子进程;

child.send()  #发送命令

child.sendline()  #发送命令,且有一个回车符

child.sendcontrol()  #发送控制符

child.expect()  #从缓冲区匹配指定正则表达式的内容。

expect()方法中的正则表达式,使用的时候还是要注意:

  1. 不能用”$”匹配行尾(因为pexpect每次都只读取一个字符,每个字符看起来都是一行的最后一个字符),可以用“\r\n”匹配行尾;
  2. 在正则表达式的末尾,尽量少使用模糊匹配“+”“*”,建议使用精确匹配。因为pexpect中是最少匹配,而不是贪婪匹配。比如child.expect ('.+')只会匹配到一个字符,而child.expect ('.*')一个字符都匹配不到。
  3. 特殊模式EOF和pexpect.TIMEOUT。

   当没有匹配到任何内容时,pexpect会抛出pexpect.EOF的异常,可以把pexpect.EOF当成一个正则表达式传入expect(),这样就不会抛出异常,EOF之前所有的输出都会记录在before成员中;pexpect.TIMEOUT同理;pexpect的默认超时时间是30s,可以在expect方法中重新设置超时时间timeout=120.

child.before   #正则表达式匹配成功后前面的内容

child.after   #正则表达式匹配到的内容

child.interact()  #控制权交给用户

pexpect.run()   #直接运行shell命令,类似于os.system()功能,与 os.system() 不同的是,使用 run() 可以方便地同时获得命令的输出结果与命令的退出状态。 

代码示例:设置ssh免密

def ssh_authentication(server_ip, user, passwd):
    """进行ssh的免密码认证"""
    if not os.path.exists(os.path.expanduser("~/.ssh/id_rsa")) or not os.path.exists(
            os.path.expanduser("~/.ssh/id_rsa.pub")):
        if os.system("ssh-keygen -t rsa -P '' -f ~/.ssh/id_rsa"):
            return False
    #检查是否已经免密
    try:
        #在本机进行登录
        cmd="ssh {}@{}".format(user,server_ip)
        child = pexpect.spawn(cmd)
        index = child.expect(["(?i)Last login:", "(?i)yes/no", "(?i)password",pexpect.EOF, pexpect.TIMEOUT])
        if index == 0:
            logger.info("ssh key is installed:{}".format(server_ip))
            child.close()
            return True
        elif index == 1:
            child.sendline("yes")
            index1 = child.expect(["(?i)Last login:", pexpect.EOF, pexpect.TIMEOUT])
            if index1 == 0:
                logger.info("ssh key is installed:{}".format(server_ip))
                child.close()
                return True
        elif index ==2:
            logger.error("ssh key is not installed:{}".format(server_ip))
        else:
            output = child.before.decode()
            logger.error("error occured:{}".format(output))
        #设置免密
        logger.info("set ssh key for:{}".format(server_ip))
        cmd = "ssh-copy-id {}@{}".format(user, server_ip)
        child = pexpect.spawn(cmd)
        child.timeout=120
        index = child.expect(["(?i)password:", "(?i)yes/no", pexpect.EOF, pexpect.TIMEOUT])
        if index == 0:
            child.sendline(passwd)
            child.expect(pexpect.EOF)
            stdout = str(child.before)
            logger.info(stdout)
            if "you wanted were added." in stdout:
                logger.info("ssh key is installed:{}".format(server_ip))
                child.close()
                return True
        elif index == 1:
            child.sendline("yes")
            index1 = child.expect(["(?i)password:", pexpect.EOF, pexpect.TIMEOUT])
            if index1 == 0:
                child.sendline(passwd)
                child.expect(pexpect.EOF)
                stdout = str(child.before)
                logger.info(stdout)
                if "you wanted were added." in stdout:
                    logger.info("ssh key is installed:{}".format(server_ip))
                    child.close()
                    return True
        logger.error("ssh key install failed:{} {}".format(server_ip,child.before.decode()))
        child.close()
        except Exception as e:
            logger.error("{}:{}".format(server_ip, e))
    logger.info("ssh-copy-id failed:{}".format(server_ip))
    return False

 

B. pty.spawn()

pty是一个伪终端处理模块:启动另外一个进程并以程序的方式从其控制终端中进行读写。pty.spawn(argv[, master_read[, stdin_read]])方法生成一个进程,并将其控制终端连接到当前进程的标准io,master_read,stdin_read是两个函数,pty.spawn()调用时向这两个函数传入一个文件描述符,这两个函数会返回一个字符串,默认字符串长度是1024bytes, master_read的回调函数从子进程中读取输出,stdin_read从父进程的标准输入中读取数据,实现人机交互。

示例代码:打开bash,记录输入输出到文件中

该代码中,pty.spawn(shell,read)运行一个子进程,子进程中打开/bin/bash窗口,在该创建中的输入和屏幕输出,都会记录到temp.txt中,在窗口中输入exit,则退出子进程。

import pty,os,time

shell = os.environ.get('SHELL', 'sh')
filename = "./temp.txt"
mode = 'wb'

with open(filename, mode) as script:
    def read(fd):
        data = os.read(fd, 1024)
        script.write(data)
        return data

    print('Script started, file is', filename)
    script.write(('Script started on %s\n' % time.asctime()).encode())
    pty.spawn(shell,read)
    script.write(('Script done on %s\n' % time.asctime()).encode())
    print('Script done, file is', filename)

C. subprocess

subprocess 模块允许我们启动一个新进程,并连接到它们的输入/输出/错误管道,从而获取返回值。pty.spawn()也是生成子进程,但子进程的输入输出会连接到父进程的输入输出,subprocess的实现与此是不同的,subprocess提供了更丰富的子进程管理方法。

subprocess.Popen类是该模块的核心,对进程进行创建和管理。unix平台下用os.execvp()执行子程序,windows下调用CreateProcess()方法生成子进程。

subprocess.Popen的构造函数如下:

class subprocess.Popen(args, bufsize=0, executable=None, stdin=None, stdout=None, stderr=None, preexec_fn=None, close_fds=False, shell=False, cwd=None, env=None, universal_newlines=False, startupinfo=None, creationflags=0)

参数

描述

args

要执行的shell命令,可以是字符串或者列表,如果是字符串的话,是平台相关的,不同的平台处理方法不同,当shell=True时,建议将args参数作为字符串传递;

bufsize

指定缓存策略,0表示不缓冲,1表示行缓冲,其他大于1的数字表示缓冲区大小,负数 表示使用系统默认缓冲策略。

stdin, stdout, stderr

分别表示程序标准输入、输出、错误句柄。

preexec_fn

用于指定一个将在子进程运行之前被调用的可执行对象,只在Unix平台下有效。

close_fds

如果该参数的值为True,则除了0,1和2之外的所有文件描述符都将会在子进程执行之前被关闭。

shell

该参数用于标识是否使用shell作为要执行的程序,如果shell值为True,则建议将args参数作为一个字符串传递而不要作为一个序列传递。

cwd

如果该参数值不是None,则该函数将会在执行这个子进程之前改变当前工作目录。

env

用于指定子进程的环境变量,如果env=None,那么子进程的环境变量将从父进程中继承。如果env!=None,它的值必须是一个映射对象。

universal_newlines

如果该参数值为True,则该文件对象的stdin,stdout和stderr将会作为文本流被打开,否则他们将会被作为二进制流被打开。

startupinfo和creationflags

这两个参数只在Windows下有效,它们将被传递给底层的CreateProcess()函数,用于设置子进程的一些属性,如主窗口的外观,进程优先级等。

Popen抛出的异常:OSError、ValueError、CalledProcessError。

Popen实例的一些函数:

方法

描述

Popen.poll()

用于检查子进程(命令)是否已经执行结束,没结束返回None,结束后返回状态码。

Popen.wait(timeout=None)

等待子进程结束,并返回状态码;如果在timeout指定的秒数之后进程还没有结束,将会抛出一个TimeoutExpired异常。

Popen.communicate(input=None)

该方法可用来与进程进行交互,比如发送数据到stdin,从stdout和stderr读取数据,直到到达文件末尾。返回(stdoutdata, stderrdata)。如果要发送数据到子进程的stdin,并要子进程返回结果到stdoutdata、stderrdata中,在用Popen创建子进程时需要设置参数stdin=PIPE,stdout=PIPE及stderr=PIPE。如果返回的数据量过大,不建议使用该方法。

Popen.send_signal(signal)

发送指定的信号给这个子进程。

Popen.terminate()

停止该子进程。

Popen.kill()

杀死该子进程。

subprocess中的常用方法都是基于subprocess.Popen类实现的。

subprocess的常用方法:

方法

描述

subprocess.getstatusoutput(cmd)

#执行string类型的shell命令,返回执行状态和执行结果两个值:status,output;

subprocess.getoutput(cmd)  #执行string类型的shell命令,返回执行结果stdout or stderr;

subprocess.run()

#执行列表类型的shell命令(shell命令以列表形式提供,如’ls -l’ 要写成[‘ls’,’-l’]),返回CompletedProcess实例,如果传入参数同时传入shell=True,则传入一个字符串args,shell命令而不是待执行的shell命令序列。

subprocess.call()

执行列表类型的shell命令(shell命令以列表形式提供,如’ls -l’ 要写成[‘ls’,’-l’]),返回returncode ,如果传入参数同时传入shell=True,则传入一个字符串args,shell命令而不是待执行的shell命令序列。 这个函数不能在参数中使用stdout=PIPE或stderr=PIPE,这样会导致子进程死锁,如果要用pipe的的话可以用Popen的communicate()。

subprocess.check_call()

#参数同subprocess.call(),如果执行结果是0就返回,否则抛异常 CalledProcessError。

subprocess.check_output()

 #参数形式是列表,shell=True时可以是字符串, 执行shell命令,返回bytes类型的output,执行状态不为0时,抛异常 CalledProcessError;

subprocess.PIPE  #特殊常量,在Popen的stdin, stdout or stderr 参数中使用,表示打开标准输入输出流管道。

subprocess.STDOUT

Sepcial value,在Popen的stderr 参数中使用,表示将标准错误输出到标准输出中;

subprocess.CalledProcessError

subproecess的异常

subprocess.STARTUPINFO类

只在windows平台上使用的Popen

示例代码:执行shell命令,获取磁盘wwid

import subprocess

def execute_cmd(args,timeout=60):
    shell = True
    if isinstance(args,list):
        shell=False
    try:
        proc = subprocess.Popen(args, shell=shell, close_fds=True,
                                       stdout=subprocess.PIPE,
                                       stderr=subprocess.PIPE)
        stdout, stderr = proc.communicate(input=None)
        if proc.returncode == None:
            raise Exception("Timeout %s" % timeout)
        return proc.returncode, stdout.decode(), stderr.decode()
    except Exception as e:
        if proc:
            proc.kill()
            proc.wait()
        raise

cmd="lsscsi"
code,stdout,stderr = execute_cmd(cmd)
if code!=0:
    print(code,stdout,stderr)
else:
    stdout=stdout.split("\n")
    disks=[item.split()[-1] for item in stdout if item and "disk" in item]
    print(disks)
    for disk in disks:
        cmd=["/usr/lib/udev/scsi_id","-g", "-u", disk]
        code, stdout, stderr = execute_cmd(cmd)
        if code==0:
            print("{} wwid is :{}".format(disk,stdout.strip("\n")))
        else:
            print("get {} wwid failed :{}".format(disk, stderr))

D. paramiko

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

fabric和ansible内部的远程管理就是使用的paramiko来现实的。

paramiko包含两个核心组件,一个为SSHClient类,另一个为SFTPClient类。

SSHClient类封装了Transport, Channel, 和SFTPClient三个类,可以完成建立连接,鉴权,用于在远程服务器上执行shell命令。

示例代码一:基于用户名密码连接到远程服务器并执行shell

import paramiko

# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='20.20.6.138', port=22, username='root', password='sandstone')
# 执行命令
stdin, stdout, stderr = ssh.exec_command('df')
# 获取命令结果
rc = stdout.channel.recv_exit_status()
print(rc)
print(stdout.read().decode())
print(stderr.read().decode())
# 关闭连接
ssh.close()

示例代码二: 基于公钥私钥连接到远程服务器并执行shell

(先要用ssh-key-gen创建私钥和公钥,再用ssh-copy-id将公钥设置到远程服务器上)

import paramiko

private_key = paramiko.RSAKey.from_private_key_file('/root/.ssh/id_rsa')

# 创建SSH对象
ssh = paramiko.SSHClient()
# 允许连接不在know_hosts文件中的主机
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
# 连接服务器
ssh.connect(hostname='20.20.6.130', port=22, username='root', pkey=private_key)
# 执行命令
stdin, stdout, stderr = ssh.exec_command('ls -l')
# 获取命令结果
rc = stdout.channel.recv_exit_status()
print(rc)
print(stdout.read().decode())
print(stderr.read().decode())
# 关闭连接
ssh.close()

E. pxssh

pxssh 类是pexpect的派生类,用来建立ssh连接。pxssh不如paramiko功能强大,建议使用paramiko进行ssh连接。

该模块已提供了login()/logout()函数用于登陆登出ssh,sendline()等函数用于发送命令。

示例代码:登陆远程主机,执行shell命令

from pexpect import pxssh
#创建子进程
connect = pxssh.pxssh()
#登录
connect.login("20.20.6.130","root","sandstone")
#发送命令
connect.sendline("df")
#匹配下一个shell提示符
connect.prompt()
#获取结果
print(connect.before.decode())
#登出
connect.logout()

F. RedExpect

RedExpect模块也是提供ssh远程执行shell的模块,类似pexpect,模拟人机交互,通过关键字匹配输出。

使用这个模块的人不多,文档链接比较难打开,想学习的可以安装模块来练习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天读点书学堂

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值