目录
- 本文目的说明
- 编者声明
- Hydropper简介
- test_microvm_api代码分析
- · test_api_lifecycle(testcase/test_microvm_api.py)
- · 实例化microvm对象(conftest.py)
- · init_microvm(conftest.py)
- · test_session_root_path(conftest.py)
- · MonitorThread(monitor/monitor_thread.py)
- · testvm.kill(virt/microvm.py)
- · shutdown(virt/basevm.py)
- · cmd(virt/basevm.py)
- · event_wait(virt/basevm.py)
- · wait_pid_exit(virt/basevm.py)
- · _post_shutdown(virt/basevm.py)
- · basic_config(virt/basevm.py)
- · launch(virt/basevm.py)
- · _pre_launch(virt/basevm.py)
- · _post_launch(virt/basevm.py)
- · create_ssh_session(virt/basevm.py)
- · serial_cmd(virt/basevm.py)
- · config_network(virt/basevm.py)
- · post_launch_serial(virt/basevm.py)
- · create_serial_control(virt/basevm.py)
- · _wait_for_active(virt/basevm.py)
- · post_launch_qmp(virt/basevm.py)
- · connect(virt/basevm.py)
- · query_cpus(virt/basevm.py)
- · qmp_command(virt/basevm.py)
本文目的说明
通过对于测试用例的纵向分析,来达到对于Hydropper测试框架源码一定程度的熟悉程度。截止截稿日(2021.8.10),openEuler社区开放的Hydropper源码中提供了11个测试用例。本文将test_microvm_api.py作为分析案例。
编者声明
本文分析的hydropper源码来源于gitee的openEuler社区上,链接如下:
https://gitee.com/openeuler/stratovirt/tree/master/tests/hydropper
本人第一次尝试分析企业级的代码,并且对于硬件方面的知识也是十分有限。本文的分析或多或少会出现表达不清晰,用词不准确等各种错误,如发现问题,恳求在评论区留下您的意见,我会持续关注并保持错误的修正。请各位读者不吝赐教!
Hydropper简介
hydropper是一个基于pytest的轻量级测试框架,在其基础上封装了虚拟化的相关测试原子,用于stratovirt的黑盒测试。当前hydropper已经提供了一些测试用例,可以帮助开发人员发现和定位stratovirt的问题。
test_microvm_api代码分析
· test_api_lifecycle(testcase/test_microvm_api.py)
test_api_lifecycle函数作为test_microvm_api测试用例的入口,并打上了acceptance的标签 。该函数通过对于虚拟机中虚拟处理器的设置来测试程序接口的生命周期。
如图可见,microvm对象作为固件传入,依次调用了basic_config和launch方法进行了虚拟机处理器数量设置和虚拟机的启动。再调用query_cups方法查看cpu信息,断言虚拟处理器数量为4。最后将虚拟机对象关闭。
· 实例化microvm对象(conftest.py)
microvm实例化函数在conftest.py文件内,默认为每个函数调用执行一次。将test_session_root_path固件作为init_microvm的参数。
该函数先执行init_microvm方法,然后执行调用microvm固件的函数,最后执行teardown部分代码。
· init_microvm(conftest.py)
首先为虚拟机分配了一个通用唯一识别码(UUID),将stratovirt的二进制文件路径等作为参数放入Microvm函数(import from virt.microvm.py),最后将testvm类返回
microvm类包含大量方法,以下附上其_init_函数:
def __init__(self, root_path, name, uuid, bin_path=CONFIG.stratovirt_microvm_bin,
vmconfig=CONFIG.get_default_microvm_vmconfig(),
vmlinux=CONFIG.stratovirt_vmlinux, rootfs=CONFIG.stratovirt_rootfs, initrd=CONFIG.stratovirt_initrd,
vcpus=4, max_vcpus=8, memslots=0, maxmem=None,
memsize=2524971008, socktype="unix", loglevel="info"):
self.name = name
self.vmid = uuid
if "unix" in socktype:
sock_path = os.path.join(root_path, self.name + "_" + self.vmid + ".sock")
else:
sock_path = ("127.0.0.1", 32542)
self.vmconfig_template_file = vmconfig
self.vm_json_file = None
self.vmlinux = vmlinux
self.rootfs = rootfs
self.initrd = initrd
self.vcpus = vcpus
self.max_vcpus = max_vcpus
self.memslots = memslots
self.memsize = memsize
self.maxmem = maxmem
self.inited = False
self.init_vmjson(root_path)
self._args = list()
super(MicroVM, self).__init__(root_path=root_path,
name=name,
uuid=uuid,
args=self._args,
bin_path=bin_path,
mon_sock=sock_path,
daemon=True,
config=self.vm_json_file)
self.add_env("RUST_BACKTRACE", "1")
if CONFIG.rust_san_check:
self.add_env("RUSTFLAGS", "-Zsanitizer=address")
self.add_env("STRATOVIRT_LOG_LEVEL", loglevel)
if CONFIG.memory_usage_check:
self.memory_check = MemoryUsageExceededInfo(0)
self.memory_check.disable()
self.memory_check.start()
参数解析:
- self :对象自身
- root_path :根路径
- name :虚拟机名称
- uuid :系统分配的通用唯一识别码
- bin_path :从config.py文件里生成的CONFIG对象内获取的stratovirt二进制文件路径
- vmconfig :从CONFIG对象内获取的虚拟机配置信息
- vmlinux :从CONFIG对象内获取的内核文件
- rootfs : 从CONFIG对象内获取的文件系统
- initrd :从CONFIG对象内获取的Linux初始RAM磁盘
- vcpus :虚拟处理器总数,默认为4
- max_vcpus :最大虚拟处理器数量,默认为8
- memslots :内存槽,默认为0
- maxmem :最大内存,默认为空,即不设置上限
- memsize :内存大小,默认为2GB
- socktype :主板型号,默认为unix
- loglevel :日志级别,默认为info
· test_session_root_path(conftest.py)
fixture标签的意思是该固件为会话级,开启自动调用;也就是说每次测试会话都不需要手动调用参数就能开启。
- setup
- 从CONFIG对象内获取删除测试会话属性
- 调用MonitorThread函数,开启线程监控
- 如果虚拟机类型是stratovirt,则用shell命令复制一份bak格式的文件系统文件
- teardown
- 如果虚拟机类型是stratovirt,则用shell命令将bak格式的文件系统文件复制回原文件
- 关闭并断开线程监控
- 如果需要删除当前会话并且没有新的测试会话生成,则递归删除CONFIG.test_session_root_path所代表的根路径文件
· MonitorThread(monitor/monitor_thread.py)
可以看到MonitorThread继承于threading库的thread类
stop函数调佣自定义的set_state函数。用with语句判断是否线程上锁,若没有上锁即改变线程状态。从而完成对于线程的stop操作。
· testvm.kill(virt/microvm.py)
杀死虚拟机
先进行对于CONFIG.memory_usage_check的判断(内存使用是否合法),然后对内存状态进行设置。最后再调用从父类basevm继承来的kill方法。
- CONFIG.memory_usage_check是从CONFIG对象中获取的内存使用检查属性。
- self.memory_check是MemoryUsageExceededInfo类的实例。
- MemoryUsageExceededInfo继承于monitor_info.MonitorInfo,因为本文中涉及到的相关方法都是继承父类的,所以不加以分析。
- basevm的kill方法如下:(virt/basevm.py)
- 将虚拟机shutdown,如果报错则将警告写入日志并destroy虚拟机。
- 通过遍历taps,将网络配置NETWORK中所有与该虚拟机有关的tap(分路器)清除。
· shutdown(virt/basevm.py)
关闭虚拟机并清理环境
- 没有启动则返回
- 调用_pre_shutdown,该函数暂时为空
- 如果daemon(用以加载镜像文件的物理光驱)可用且虚拟机正在运行,则判断如果qmp协议可使用,则执行:(cmd和event_wait函数以下会有分析),否则杀死进程
self.cmd('quit')
self.event_wait(name='SHUTDOWN', timeout=10, match={'data': {'guest': False, 'reason': 'host-qmp-quit'}})
- 如果出现Exception则在日志写入,然后调用_popen.kill杀死对应进程
- 如果daemon不可用,则让进程等待
- 否则(daemon可用,虚拟机没有运行),调用wait_pid_exit函数
- 整个函数的最后调用_post_shutdown函数并将launched置为false
· cmd(virt/basevm.py)
用qmp命令与QEMU通信
- cmd函数将name、args、cmd_id字符串拼接成qmp命令用以与QEMU通信
- 向日志里写入输入信息
- 将qum_cmd以utf-8协议转换为json格式,并通过_qmp[‘sock’]送向虚拟机monitor
- 如果报OSError,只有当错误序号是EPIPE时才消除报错
- 接收反馈信息,并向日志里写入反馈信息
- 返回反馈信息
· event_wait(virt/basevm.py)
等待反馈事件与qmp命令匹配
- 在一个等待时间内等待从主板向__qmp[‘events’]推送一个event
- 如果事件名匹配,且返回的格式内容与match相同,则返回该事件
- 如果有TypeError,如果事件名匹配,返回事件
- 向事件序列里加入新事件
- 循环至步骤1继续等待
· wait_pid_exit(virt/basevm.py)
在pid出现前一直等待
- 该函数被装饰为retry,两次的间隔为1s,最大尝试次数为30
- 将检测信息写入日志
- 如果对应pid的文件依旧存在说明关闭虚拟机失败,对应信息报错
· _post_shutdown(virt/basevm.py)
执行关闭虚拟机后需要的操作
- 获取进程退出码
- 如果退出码不为空且小于0,则说明退出异常,向日志输出对应警告
- 如果_qmp不为空则关闭socket及其文件
- 关闭串口会话、ssh会话
- 移除所有运行文件
- 通过pid移除进程文件
· basic_config(virt/basevm.py)
基本配置信息设置
- 将参数内存在的属性的values赋给configdict内对应的属性,然后删除参数列表的对应属性
- 在参数列表中剩余的属性若存在于self对象中,则进行属性赋值
· launch(virt/basevm.py)
启动虚拟机并建立qmp连接
- 重置参数列表
- 调用_pre_launch,预启动
- full_command初始化,并写入日志
- 如果虚拟机的环境属性里没有键,则创建一个子进程对象
- 否则,将系统环境信息覆盖更新,赋值给_tempenv,将_tempenv作为参数之一,创建子进程对象
对于官方的解释,environ是一个字符串所对应环境的映像对象。这是什么意思呢?举个例子来说,environ[‘HOME’]就代表了当前这个用户的主目录。
- 如果加载镜像文件的物理光驱daemon可用,当前进程等待,并获取pid
- 否则,将进程pid赋给虚拟机pid
- 如果当前不是异常处理,则调用启动后函数
· _pre_launch(virt/basevm.py)
- 如果__qmp_set为真,即qmp命令可用,则:
- 如果控制台地址存在,则进行属性赋值,若地址不是元组,将控制台地址加入待移除文件列表
- 否则通过参数拼接出控制台地址,将控制台地址加入待移除文件列表
- 通过parser_config_to_args函数,将json格式的配置信息转换到参数列表args中
· _post_launch(virt/basevm.py)
- 将虚拟机状态启动置为真
- 如果incoming属性为真,则直接返回
- 依次调用post_launch_serial、post_launch_qmp对串口和qmp进行启动
- 如果虚拟网络数量大于0,则调用post_launch_vnet和config_network
- 再如果ssh会话已开启,调用ssh_session.close将其关闭。
- 调用create_ssh_session创建新的ssh会话
· create_ssh_session(virt/basevm.py)
- 创建ssh会话*
- 设置相关参数,尝试ping向ip地址,并将结果写入日志
- 创建ssh会话对象
- 将ssh绘画对象作为参数,向虚拟机发送创建会话命令
- 如果报错,将会话关闭
- 返回ssh会话
· serial_cmd(virt/basevm.py)
- 写入对应日志
- 返回向控制台输入命令后得到的输出
· config_network(virt/basevm.py)
配置虚拟机网络
- 默认以DHCP协议,配置网络
- 获取内部接口
- 如果虚拟机类型有stratovirt,则通过命令关闭网络管理和防火墙,开启ssh,重启sshd
- 如果模型有dhcp,通过命令获取客户机ip地址
- 如果模型有static,通过命令将网络连接设置为静态
· post_launch_serial(virt/basevm.py)
启动虚拟机后的串口创建
- 如果控制台启动,则调用create_serial_control,并等待虚拟机激活
- 否则,睡眠2秒
· create_serial_control(virt/basevm.py)
创建串口控制台
- 调用_wait_console_create,这是个间隔0.2s,最大尝试数为50的retry函数:返回控制台信息
- 生成串口控制台对象
- 将控制台对象作为参数在虚拟机中配置控制台
· _wait_for_active(virt/basevm.py)
等待虚拟机激活
- 等待串口登录后,将返回的会话赋值给serial_session属性
- wait_for_serial_login函数就是在timeout时间内不断尝试通过串口登录生成会话,其中包含了对应的日志记录与报错处理
· post_launch_qmp(virt/basevm.py)
启动虚拟机后的qmp监管协议
- 如果mon_sock是元组,则将mon_sock作为qmp_monitor_protocol的协议
- 否则,将_vm_monitor作为协议
- 如果_qmp命令不为空,调用connect函数来连接到qmp监视器
· connect(virt/basevm.py)
连接到QMP监视器并执行功能协商
- 将qmp地址作为参数进行socket连接,接收反馈信息
- 如果没有反馈信息或者QMP没有出现在反馈信息中,报错
- 发送命令检测qmp功能,如果返回内容里出现return,则返回socket反馈信息
- 否则报错
· query_cpus(virt/basevm.py)
查询cpu数量
- 调用qmp命令,参数为“query_cpus”
· qmp_command(virt/basevm.py)
运行qmp命令
- 遍历解析参数列表,将对应内容转换为字典格式
- 调用cmd函数输入命令字典
- 如果返回值为空,则报错
- 否则,返回返回值