From 70f785a0dbf244da50c4aeded23398bf9d880595 Mon Sep 17 00:00:00 2001 From: zjl_long <154542010@qq.com> Date: Fri, 20 May 2022 15:17:39 +0800 Subject: [PATCH] update mugen --- mugen/License/LICENSE | 127 ++++++ mugen/README.en.md | 36 ++ mugen/README.md | 428 ++++++++++++------ mugen/dep_install.sh | 20 + ...00\350\247\206\350\247\204\350\214\203.md" | 77 ++++ .../env.conf => libs/locallibs/__init__.py} | 0 mugen/libs/locallibs/common_lib.sh | 273 +++++------ mugen/libs/locallibs/free_port.py | 131 ++++++ mugen/libs/locallibs/get_test_device.py | 133 ++++++ mugen/libs/locallibs/mugen_log.py | 54 +++ mugen/libs/locallibs/read_conf.py | 129 ++++++ mugen/libs/locallibs/remote_reboot.py | 133 ++++++ mugen/libs/locallibs/rpm_manage.py | 180 ++++++++ mugen/libs/locallibs/sftp.py | 228 ++++++++++ mugen/libs/locallibs/sleep_wait.py | 92 ++++ mugen/libs/locallibs/ssh_cmd.py | 138 ++++++ mugen/libs/locallibs/sshcmd.sh | 2 +- mugen/libs/locallibs/suite_case.py | 125 +++++ mugen/libs/locallibs/write_conf.py | 160 +++++++ mugen/libs/results_listener.sh | 79 ++++ mugen/mugen.sh | 375 +++++++-------- mugen/runoet.sh | 263 ++++++++++- mugen/suite2cases/testsuite | 3 - mugen/suite2cases/testsuite.json | 25 + .../oe_test_casename_01.sh | 21 +- .../oe_test_casename_02.py | 24 +- .../testsuite/oe_test_casename_03.sh | 3 +- 27 files changed, 2736 insertions(+), 523 deletions(-) create mode 100644 mugen/License/LICENSE create mode 100644 mugen/README.en.md create mode 100644 mugen/dep_install.sh create mode 100644 "mugen/doc/\346\265\213\350\257\225\347\224\250\344\276\213\346\243\200\350\247\206\350\247\204\350\214\203.md" rename mugen/{conf/env.conf => libs/locallibs/__init__.py} (100%) create mode 100644 mugen/libs/locallibs/free_port.py create mode 100644 mugen/libs/locallibs/get_test_device.py create mode 100644 mugen/libs/locallibs/mugen_log.py create mode 100644 mugen/libs/locallibs/read_conf.py create mode 100644 mugen/libs/locallibs/remote_reboot.py create mode 100644 mugen/libs/locallibs/rpm_manage.py create mode 100644 mugen/libs/locallibs/sftp.py create mode 100644 mugen/libs/locallibs/sleep_wait.py create mode 100644 mugen/libs/locallibs/ssh_cmd.py create mode 100644 mugen/libs/locallibs/suite_case.py create mode 100644 mugen/libs/locallibs/write_conf.py create mode 100644 mugen/libs/results_listener.sh mode change 120000 => 100644 mugen/runoet.sh delete mode 100644 mugen/suite2cases/testsuite create mode 100644 mugen/suite2cases/testsuite.json diff --git a/mugen/License/LICENSE b/mugen/License/LICENSE new file mode 100644 index 00000000..9e32cdef --- /dev/null +++ b/mugen/License/LICENSE @@ -0,0 +1,127 @@ + 木兰宽松许可证, 第2版 + + 木兰宽松许可证, 第2版 + 2020年1月 http://license.coscl.org.cn/MulanPSL2 + + + 您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + + 0. 定义 + + “软件”是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + + “贡献”是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + + “贡献者”是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + + “法人实体”是指提交贡献的机构及其“关联实体”。 + + “关联实体”是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + + 1. 授予版权许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可以复制、使用、修改、分发其“贡献”,不论修改与否。 + + 2. 授予专利许可 + + 每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权行动之日终止。 + + 3. 无商标许可 + + “本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定的声明义务而必须使用除外。 + + 4. 分发限制 + + 您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + + 5. 免责声明与责任限制 + + “软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于何种法律理论,即使其曾被建议有此种损失的可能性。 + + 6. 语言 + “本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文版为准。 + + 条款结束 + + 如何将木兰宽松许可证,第2版,应用到您的软件 + + 如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + + 1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + + 2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + + 3, 请将如下声明文本放入每个源文件的头部注释中。 + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + + Mulan Permissive Software License,Version 2 + + Mulan Permissive Software License,Version 2 (Mulan PSL v2) + January 2020 http://license.coscl.org.cn/MulanPSL2 + + Your reproduction, use, modification and distribution of the Software shall be subject to Mulan PSL v2 (this License) with the following terms and conditions: + + 0. Definition + + Software means the program and related documents which are licensed under this License and comprise all Contribution(s). + + Contribution means the copyrightable work licensed by a particular Contributor under this License. + + Contributor means the Individual or Legal Entity who licenses its copyrightable work under this License. + + Legal Entity means the entity making a Contribution and all its Affiliates. + + Affiliates means entities that control, are controlled by, or are under common control with the acting entity under this License, ‘control’ means direct or indirect ownership of at least fifty percent (50%) of the voting power, capital or other securities of controlled or commonly controlled entity. + + 1. Grant of Copyright License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable copyright license to reproduce, use, modify, or distribute its Contribution, with modification or not. + + 2. Grant of Patent License + + Subject to the terms and conditions of this License, each Contributor hereby grants to you a perpetual, worldwide, royalty-free, non-exclusive, irrevocable (except for revocation under this Section) patent license to make, have made, use, offer for sale, sell, import or otherwise transfer its Contribution, where such patent license is only limited to the patent claims owned or controlled by such Contributor now or in future which will be necessarily infringed by its Contribution alone, or by combination of the Contribution with the Software to which the Contribution was contributed. The patent license shall not apply to any modification of the Contribution, and any other combination which includes the Contribution. If you or your Affiliates directly or indirectly institute patent litigation (including a cross claim or counterclaim in a litigation) or other patent enforcement activities against any individual or entity by alleging that the Software or any Contribution in it infringes patents, then any patent license granted to you under this License for the Software shall terminate as of the date such litigation or activity is filed or taken. + + 3. No Trademark License + + No trademark license is granted to use the trade names, trademarks, service marks, or product names of Contributor, except as required to fulfill notice requirements in Section 4. + + 4. Distribution Restriction + + You may distribute the Software in any medium with or without modification, whether in source or executable forms, provided that you provide recipients with a copy of this License and retain copyright, patent, trademark and disclaimer statements in the Software. + + 5. Disclaimer of Warranty and Limitation of Liability + + THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + + 6. Language + + THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION SHALL PREVAIL. + + END OF THE TERMS AND CONDITIONS + + How to Apply the Mulan Permissive Software License,Version 2 (Mulan PSL v2) to Your Software + + To apply the Mulan PSL v2 to your work, for easy identification by recipients, you are suggested to complete following three steps: + + i Fill in the blanks in following statement, including insert your software name, the year of the first publication of your software, and your name identified as the copyright owner; + + ii Create a file named “LICENSE” which contains the whole context of this License in the first directory of your software package; + + iii Attach the statement to the appropriate annotated syntax at the beginning of each source file. + + + Copyright (c) [Year] [name of copyright holder] + [Software Name] is licensed under Mulan PSL v2. + You can use this software according to the terms and conditions of the Mulan PSL v2. + You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 + THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. diff --git a/mugen/README.en.md b/mugen/README.en.md new file mode 100644 index 00000000..5d96a35f --- /dev/null +++ b/mugen/README.en.md @@ -0,0 +1,36 @@ +# mugen + +#### Description +Test framework and test suites + +#### Software Architecture +Software architecture description + +#### Installation + +1. xxxx +2. xxxx +3. xxxx + +#### Instructions + +1. xxxx +2. xxxx +3. xxxx + +#### Contribution + +1. Fork the repository +2. Create Feat_xxx branch +3. Commit your code +4. Create Pull Request + + +#### Gitee Feature + +1. You can use Readme\_XXX.md to support different languages, such as Readme\_en.md, Readme\_zh.md +2. Gitee blog [blog.gitee.com](https://blog.gitee.com) +3. Explore open source project [https://gitee.com/explore](https://gitee.com/explore) +4. The most valuable open source project [GVP](https://gitee.com/gvp) +5. The manual of Gitee [https://gitee.com/help](https://gitee.com/help) +6. The most popular members [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/) diff --git a/mugen/README.md b/mugen/README.md index 4e8b79f8..b4c13f21 100644 --- a/mugen/README.md +++ b/mugen/README.md @@ -1,151 +1,289 @@ # mugen ## mugen介绍 - mugen是openEuler社区开放的测试框架,提供公共配置和方法以便社区开发者进行测试代码的编写和执行 -## mugen使用说明 - -用户对mugen框架的使用是通过执行mugen.sh接口来实现(兼容原有的runoet.sh,并保留)。接口详细说明如下: - -- 参数说明: -\-c: 测试环境配置 -\-d: 下载openEuler社区开源的测试脚本 -\-a:执行所有的测试用例 -\-f:指定测试套执行 -\-r:指定测试套下测试用例的执行,当前支持单用例 -\-C: 不去检测用例名和测试套映射文件的对应关系 -\-x:进入调试模式下执行测试用例 - -- 使用示例 - - 配置测试环境 - `bash mugen.sh -c $ip $user $password` - - 下载openEuler社区开源测试脚本 - `bash mugen.sh -d` - - 执行所有用例 - - 正常模式 - `bash mugen.sh -a` - - 调试模式 - `bash mugen.sh -xa` - - 执行指定测试套下所有用例 - - 正常模式 - `bash mugen.sh -f "xxx"` - - 调试模式 - `bash mugen.sh -xf "xxx"` - - 执行指定的用例 - - 正常模式 - `bash mugen.sh -f "xxx" -Cr "yyy"` - - 正常模式且不检测映射文件 - - 调试模式 - `bash mugen.sh -xf "xxx" -r "yyy"` - - 调试模式且不检测映射文件 - `bash mugen.sh -xf "xxx" -Cr "yyy"` - -- 使用说明 - - 测试用例执行之前必须先配置环境变量 - - 所有的测试用例存放在testcases目录下 - - 测试套和测试用例的对应关系需要在suite2cases目录中进行定义 - - 大家可以参考现有的测试模板进行用例开发 - -- 框架目录 -  ![mugen_tree](data:image/png;base64,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) - -- mugen框架的生成的环境变量说明 - - 配置文件路径 - - 如果/etc/mugen/不存在,则路径为${OET_PATH}/conf - - 配置文件内容 - - NODE: 测试环境节点 - - LOCALTION: 测试环境的本地 OR 远端 - - USER: 测试环境节点的用户 - - PASSWORD: 测试环境节点的用户密码 - - MACHINE: 虚拟机 OR 物理机 - - FRAME: 系统架构 - - NICS: 网卡名 (变量为数组) - - MAC: 网卡对应的mac地址(变量为数组) - - IPV4: ip v4 地址 (变量为数组) - - IPV6: ip v6 地址 (变量为数组) - - 环境变量名 - - NODE1_LOCATION、NODE2_LOCATION - - NODE1_USER、NODE2_USER - - NODE1_PASSWORD、NODE2_PASSWORD - - NODE1_MACHINE、NODE2_MACHINE - - NODE1_FRAME、NODE2_FRAME - - NODE1_NICS、NODE2_NICS - - NODE1_MAC、NODE2_MAC - - NODE1_IPV4、NODE2_IPV4 - - NODE1_IPV6、NODE2_IPV6 - - PS: 使用者可以根据实际情况在env.conf中定义全局变量 - -## mugen框架中shell公共方法 - -- SSH_CMD - - 对ssh进行封装,远程执行命令时无需进入交互模式 - - 使用方法 - `SSH_CMD "$cmd" $REMOTEIP $REMOTEPASSWD $REMOTEUSER` - -- SSH_SCP - - 对scp进行封装,执行scp命令时无需进入交互模式 - - 使用方法 - - 本地文件传输到远端 - `SSH_SCP $local_path/$file $REMOTE_USER@$REMOTE_IP:$remote_path "$REMOTE_PASSWD"` - - 远端文件传输到本地 - `SSH_SCP $REMOTE_USER@$REMOTE_IP:$remote_path/$file $local_path "$REMOTE_PASSWD"` - -- LOG_INFO - - 输出INFO级日志 - `LOG_INFO $log` +## mugen更新说明 +- 优化了mugen框架的入口函数 +- 当前版本添加了对测试套路径和测试用例环境的添加 +- 新增python公共函数 + +## mugen使用教程 + +#### 安装依赖软件 +`bash dep_install.sh` + +#### 配置测试套环境变量 +- 命令执行:`bash mugen.sh -c --ip $ip --password $passwd --user $user --port $port` +- 参数说明: + - ip:测试机的ip地址 + - user:测试机的登录用户,默认为root + - password: 测试机的登录密码 + - port:测试机ssh登陆端口,默认为22 +- 环境变量文件:./conf/env.json +``` +{ + "NODE": [ + { + "ID": 1, + "LOCALTION": "local", + "MACHINE": "physical", + "FRAME": "aarch64", + "NIC": "eth0", + "MAC": "55:54:00:c8:a9:21", + "IPV4": "192.168.0.10", + "USER": "root", + "PASSWORD": "openEuler12#$", + "SSH_PORT": 22, + "BMC_IP": "", + "BMC_USER": "", + "BMC_PASSWORD": "" + }, + { + "ID": 2, + "LOCALTION": "remote", + "MACHINE": "kvm", + "FRAME": "aarch64", + "NIC": "eth0", + "MAC": "55:54:00:c8:a9:22", + "IPV4": "192.168.0.11", + "USER": "root", + "PASSWORD": "openEuler12#$", + "SSH_PORT": 22, + "HOST_IP": "", + "HOST_USER": "", + "HOST_PASSWORD": "" + "HOST_SSH_PORT": "" + } + ] +} +``` +- 在用例中的使用:NODE${id}_${LOCALTION} + +#### 用例执行 +- 执行所有用例 +`bash mugen.sh -a` +- 执行指定测试套 +`bash mugen.sh -f testsuite` +- 执行单条用例 +`bash mugen.sh -f testsuite -r testcase` +- 日志输出shell脚本的执行过程 +``` +bash mugen.sh -a -x +bash mugen.sh -f testsuite -x +bash mugen.sh -f testsuite -r testcase -x +``` + +#### 用例添加 +- 根据模板编写用例脚本(推荐) +``` +source ${OET_PATH}/libs/locallibs/common_lib.sh + +# 需要预加载的数据、参数配置 +function config_params() { + LOG_INFO "Start to config params of the case." + + LOG_INFO "No params need to config." + + LOG_INFO "End to config params of the case." +} + +# 测试对象、测试需要的工具等安装准备 +function pre_test() { + LOG_INFO "Start to prepare the test environment." + + LOG_INFO "No pkgs need to install." + + LOG_INFO "End to prepare the test environment." +} + +# 测试点的执行 +function run_test() { + LOG_INFO "Start to run test." + + # 测试命令:ls + ls -CZl -all + CHECK_RESULT 0 + + # 测试/目录下是否存在proc|usr|roor|var|sys|etc|boot|dev目录 + CHECK_RESULT "$(ls / | grep -cE 'proc|usr|roor|var|sys|etc|boot|dev')" 7 + + LOG_INFO "End to run test." +} + +# 后置处理,恢复测试环境 +function post_test() { + LOG_INFO "Start to restore the test environment." + + LOG_INFO "Nothing to do." + + LOG_INFO "End to restore the test environment." +} + +main "$@" +``` + +- 单纯的shell脚本或python脚本,通过脚本执行的返回码判断用例是否成功。 + - 可参考样例:testsuite测试套下用例oe_test_casename_02和oe_test_casename_03 + + +#### suite2cases中json文件的写法 +- 文件名为测试套名,文件后缀.json +- 文件内容说明: + + +#### 框架的公共函数 +- 如何加载到用例中,以供调用 +``` +# bash +source ${OET_PATH}/libs/locallibs/common_lib.sh + +# python +import os, sys, subprocess + +LIBS_PATH = os.environ.get("OET_PATH") + "/libs/locallibs" +sys.path.append(LIBS_PATH) +import xxx +``` -- LOG_WARN - - 输出WARN级日志 - `LOG_WARN $log` - -- LOG_ERROR - - 输出ERROR级日志 - `LOG_WARN $log` - -- DNF_INSTALL - - 用于安装软件包 - `DNF_INSTALL "vim bc nettools"` - - 特别说明:建议需要安装的软件包,在前置处理中一次性安装完成 - -- DNF_REMOVE - - 用于卸载软件包 - - 特别说明:依赖于 DNF_INSTALL,为了保证环境恢复的,会将DNF_INSTALL安装的所有包都进行卸载 - `DNF_REMOVE` - - 如果不想依赖DNF_INSTALL,单纯想要卸载某个包,需要在方法最后添加参数“1” - `DNF_REMOVE "tree" 1` - -- REMOTE_REBOOT_WAIT - - 多节点环境中,当对端进行重启操作,将用于等待对端完全重启 - `REMOTE_REBOOT_WAIT $REMOTEPASSWD $REMOTEUSER $REMOTEIP` - -- SLEEP_WAIT - - 需要睡眠等待大于1秒的操作,不要直接使用sleep,建议使用SLEEP_WAIT - `SLEEP_WAIT 3` - -- CHECK_RESULT - - 对测试点进行检查,mugen框架将会对执行结果进行统计,所以务必使用此方法进行判断 - - 参数说明: - - 参数1:实际结果 - - 参数2:预期结果,默认为“0” - - 参数3:判断模式,默认为“0”:需要实际结果和预期结果一致;选择“1”:需要实际结果和预期结果不一致 - `CHECK_RESULT 0 0` - -- GET_RANDOM_PORT - - 随机获取未使用的端口号 - - 参数说明: - - 参数1:随机获取端口的范围起始,默认为1 - - 参数2:随机获取端口的范围终点,默认为10000 - `GET_RANDOM_PORT 1 10000` - -## mugen中python公共方法 - -- 尽情期待。。。 - -## mugen的日志说明 - -所有用例执行结束之后 - -- 日志将存储到和runoet.sh同层的logs目录下面 -- 执行结果将会存放到和runoet.sh同层的results目录下面 -- logs和results目录会在用例执行之后自动生成 +- 公共函数 + - 日志打印 + ``` + # bash + LOG_INFO "$message" + LOG_WARN "$message" + LOG_DEBUG "$message" + LOG_ERROR "$message" + # python + import mugen_log + mugen_log.logging(level, message) # level:INFO,WARN,DEBUG,ERROR;message:日志输出 + ``` + - 结果检查 + ``` + # bash + CHECK_RESULT $1 $2 $3 $4 + # $1:测试点的返回结果 + # $2:预期结果 + # $3:对比模式,0:返回结果和预期结果相对;1:返回结果和预期结果不等 + # $4:发现问题,日志输出 + ``` + - rpm包安装卸载 + ``` + # bash + ## func 1 + DNF_INSTALL "vim bc" "$node_id" + DNF_REMOVE "$node_id" "jq" "$mode" + + # mode:默认为0,会删除用例中安装的包,当为非0时,则只卸载当软件包 + # python + import rpm_manage + tpmfile = rpm_manage.rpm_install(pkgs, node, tmpfile) + rpm_manage.rpm_remove(node, pkgs, tmpfile) + ``` + - 远程命令执行 + ``` + # bash + ## func 1 + SSH_CMD "$cmd" $remote_ip $remote_password $remote_user $time_out $remote_port` + ## func 2 + P_SSH_CMD --cmd $cmd --node $node(远端节点号) + P_SSH_CMD --cmd $cmd --ip $remote_ip --password $remote_password --port $remote_port --user $remote_user --timeout $timeout + # python + conn = ssh_cmd.pssh_conn(remote_ip, remote_password,remote_port,remote_user,remote_timeout) + exitcode, output = ssh_cmd.pssh_cmd(conn, cmd) + + # port:默认为22 + # user:默认为root + # timeout: 默认不限制 + ``` + - 目录文件传输 + ``` + # bash + ## func 1 + ### 本地文件传输到远端 + `SSH_SCP $local_path/$file $REMOTE_USER@$REMOTE_IP:$remote_path "$REMOTE_PASSWD"` + ### 远端文件传输到本地 + `SSH_SCP $REMOTE_USER@$REMOTE_IP:$remote_path/$file $local_path "$REMOTE_PASSWD"` + ## func 2 + ### 目录传输 + SFTP get/put --localdir $localdir --remotedir $remotedir + ### 文件传输 + SFTP get/put --localdir $localdir --remotedir $remotedir --localfile/remotefile $localfile/$remotefile + # python + ### 目录传输 + import ssh_cmd, sftp + conn = ssh_cmd.pssh_conn(remote_ip, remote_password,remote_port,remote_user,remote_timeout) + psftp_get(conn,remote_dir, local_dir) + psftp_put(local_dir=local_dir, remote_dir=remote_dir) + ### 文件传输 + import ssh_cmd, sftp + psftp_get(remote_dir, remote_file, local_dir) + psftp_put(local_dir=local_dir, local_file=local_file, remote_dir=remote_dir) + + + # get:从远端获取 + # put:传输到远端 + # localdir: 默认为当前目录 + # remotedir:默认为远端根目录 + ``` + - 获取空闲端口 + ``` + # bash + GET_FREE_PORT $ip + # python + import free_port + free_port.find_free_port(ip) + ``` + - 检查端口是否被占用 + ``` + # bash + IS_FREE_PORT $port $ip + # python + import free_port + free_port.is_free_port(port, ip) + ``` + - 获取测试网卡 + ``` + # bash + TEST_NIC $node_id + # python + import get_test_device + get_test_nic(node_id) + + # node_id:默认为1号节点 + ``` + - 获取测试磁盘 + ``` + # bash + TEST_DISK $node_id + # python + import get_test_device + get_test_disk(node_id) + + # node_id:默认为1号节点 + ``` + - 睡眠等待 + ``` + # bash + SLEEP_WAIT $wait_time $cmd + # python + import sleep_wait + sleep_wait.sleep_wait(wait_time,cmd) + ``` + - 远端重启等待 + ``` + # bash + REMOTE_REBOOT_WAIT $node_id $wait_time + ``` + +#### 用例如何调试 +- 设置OET_PATH(mugen框架路径环境变量)之后,就可以在用例所在目录直接执行用例脚本 + +#### 用例超时说明 +- 默认超时30m +- 如果某条用例执行超过30m,在用例对TIMEOUT重新赋值 + +#### FAQ +- 远程后台执行命令时,用例执行卡住,导致用例超时失败 + - 原因:ssh远程执行命令不会自动退出,会一直等待命令的控制台标准输出,直至命令运行信号结束 + - 解决方案:可以将标准输出与标准错误输出重定向到/dev/null,如此ssh就不会一直等待`cmd > /dev/nul 2>&1 &` \ No newline at end of file diff --git a/mugen/dep_install.sh b/mugen/dep_install.sh new file mode 100644 index 00000000..f8cc2eb3 --- /dev/null +++ b/mugen/dep_install.sh @@ -0,0 +1,20 @@ +#!/usr/bin/bash +# Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. +# This program is licensed under Mulan PSL v2. +# You can use it according to the terms and conditions of the Mulan PSL v2. +# http://license.coscl.org.cn/MulanPSL2 +# THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PSL v2 for more details. +#################################### +# @Author : lemon-higgins +# @email : lemon.higgins@aliyun.com +# @Date : 2021-04-23 16:08:22 +# @License : Mulan PSL v2 +# @Version : 1.0 +# @Desc : +##################################### + +yum install expect psmisc -y +pip3 install paramiko==2.7.2 || yum install python3-paramiko -y diff --git "a/mugen/doc/\346\265\213\350\257\225\347\224\250\344\276\213\346\243\200\350\247\206\350\247\204\350\214\203.md" "b/mugen/doc/\346\265\213\350\257\225\347\224\250\344\276\213\346\243\200\350\247\206\350\247\204\350\214\203.md" new file mode 100644 index 00000000..d8b770a8 --- /dev/null +++ "b/mugen/doc/\346\265\213\350\257\225\347\224\250\344\276\213\346\243\200\350\247\206\350\247\204\350\214\203.md" @@ -0,0 +1,77 @@ +# 测试用例检视规范 + +# 1 检视前提 + +必须有用例执行结果截图; + +用例文件必须格式化; + +每个用例中的测试命令控制在10个左右; + +用例文件命名规范: + +命名方式为:oe_test_包名.sh、oe_test_包名_序号.sh、oe_test_包名_命令.sh(用于同一个命令中参数比较多的场景) + +用例拆分规范: + +同一个命令中的参数超过10个以上,需要对用例进行拆分,命名方式为:oe_test_包名_命令1_序号.sh + +用例合并规范(非必须): + +软件包中有多个命令,并且所有命令行参数少于10个,命名方式可以为:oe_test_包名.sh、oe_test_包名_序号.sh + +oe_test_包名软件包中有多个命令参数均小于3个,建议合并至一个用例中(用例描述中说明测试了哪些命令) + +名词解释: + +包名:当前测试软件包名 命令:rpm -ql 包名 | grep bin 序号:01,02 ...同理 + +# 2 检视规范 + +## 2.1 注释 + +代码中所有注释必须为英文描述; + +## 2.2 命令和参数 + +测试软件包中的命令和参数要全部覆盖; + +## 2.3 临时文件 + +临时文件放在/tmp目录下,或者在当前路径下创建临时目录(临时文件比较多的情况),post_test中需要清理; + +## 2.4 其他 + +文件起始位置注释信息中年份必须为当前年份; + +每个用例必须独立(每个用例执行互不依赖、互不影响); + +config_test只放置预加载数据参数配置等操作代码,pre_test中只放置预置处理代码,run_test只放置测试代码,post_test只放置环境清理代码(必须清理完全,恢复初始使用环境),里面不能出现python xx的执行命令; + +pre_test和post_test不能有CHECK_RESULT方法; + +能归并在一起的语句,全部归并在一起。例如,多个rm命令,可以归并一个; + +userl删除用户,需要加-rf参数,连同用户目录一起删除; + +代码中尽量使用框架提供的公共变量(参考conf/mugen.env)和公共方法,如NODE1_XX系列; + +不要定义从不使用的变量,不要写无意义的代码; + +DNF_INSTALL方法可同时安装多个包,安装多个包的调用方式为:DNF_INSTALL "xx1 xx2"; + +DNF_REMOVE与DNF_INSTALL配合同时使用时,DNF_REMOVE可以省略参数; + +config_test,pre_test,post_test如无必要,不能再用例代码中出现; + +mv命令必须加上-rf,防止目的文件存在时,命令有提示操作; + +测试点必须加CHECK_RESULT,CHECK_ RESULT必须包含4个参数,日志信息必须正常易理解; + +不能出现重复代码,尽量使用相对路径,不能出现语句逻辑性错误; + +cp命令必须加上-f,防止目的文件存在时,命令有提示操作; + +需要通过网络实时获取的文件,代码中必须是实时获取处理,不能直接下载上库; + +**根据pr的检视意见修改之后必须进行回复,无需修改也请给出理由,修改完之后需要上传用例执行成功结果截图(整个测试套跑成功)**; \ No newline at end of file diff --git a/mugen/conf/env.conf b/mugen/libs/locallibs/__init__.py similarity index 100% rename from mugen/conf/env.conf rename to mugen/libs/locallibs/__init__.py diff --git a/mugen/libs/locallibs/common_lib.sh b/mugen/libs/locallibs/common_lib.sh index cf0e1a07..fb9b9bf8 100644 --- a/mugen/libs/locallibs/common_lib.sh +++ b/mugen/libs/locallibs/common_lib.sh @@ -1,5 +1,5 @@ #!/usr/bin/bash -# Copyright (c) [2020] Huawei Technologies Co.,Ltd.ALL rights reserved. +# Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. # This program is licensed under Mulan PSL v2. # You can use it according to the terms and conditions of the Mulan PSL v2. # http://license.coscl.org.cn/MulanPSL2 @@ -8,123 +8,32 @@ # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. # See the Mulan PSL v2 for more details. #################################### -#@Author : lemon.higgins -#@Contact : lemon.higgins@aliyun.com -#@Date : 2020-04-09 09:39:43 -#@License : Mulan PSL v2 -#@Version : 1.0 -#@Desc : Public function +# @Author : lemon-higgins +# @email : lemon.higgins@aliyun.com +# @Date : 2021-04-20 15:11:47 +# @License : Mulan PSL v2 +# @Version : 1.0 +# @Desc : ##################################### function LOG_INFO() { - printf "$(date +%Y-%m-%d\ %T) $0 [ INFO ] %s\n" "$@" + message=${*-"Developer does not write the log messages."} + python3 ${OET_PATH}/libs/locallibs/mugen_log.py --level 'info' --message "$message" } function LOG_WARN() { - printf "$(date +%Y-%m-%d\ %T) $0 [ WARN ] %s\n" "$@" + message=${*-"Developer does not write the log messages."} + python3 ${OET_PATH}/libs/locallibs/mugen_log.py --level 'warn' --message "$message" } -function LOG_ERROR() { - printf "$(date +%Y-%m-%d\ %T) $0 [ ERROR ] %s\n" "$@" -} - -function GET_RANDOM_PORT() { - start_port=${1-1} - end_port=${1-10000} - - mapfile used_ports < <(printf %d\\n 0x$(cat /proc/net/tcp* /proc/net/udp* | sed '/local_address/d' | awk -F ':' '{print $3}' | awk '{print $1}' | sort -u)) - - random_port=0 - while [ $random_port == 0 ]; do - random_port=$(shuf -i ${start_port}-${end_port} -n1) - for used_port in "${used_ports[@]}"; do - test ${used_port} -eq ${random_port} && random_port=0 - done - done - - echo $random_port +function LOG_DEBUG() { + message=${*-"Developer does not write the log messages."} + python3 ${OET_PATH}/libs/locallibs/mugen_log.py --level 'debug' --message "$message" } -function DNF_INSTALL() { - __pkg_list=$1 - if [ -z "${__pkg_list}" ]; then - LOG_ERROR "Wrong parameter." - exit 1 - fi - reponames=$(grep '^\[.*\]' /etc/yum.repos.d/*.repo | tr -d [] | sed -e ':a;N;$!ba;s/\n/ /g') - mapfile -t __install_pkgs < <(dnf --assumeno install ${__pkg_list[*]} 2>&1 | grep -wE "${reponames// /|}" | grep -wE "$(uname -m)|noarch" | awk '{print $1}') - dnf -y install ${__pkg_list[*]} - - if ! dnf -y install ${__pkg_list[*]}; then - LOG_ERROR "pkg_list:${__pkg_list[*]} install failed." - exit 1 - fi - - __installed_pkgs+=" ${__install_pkgs[*]}" - - return 0 -} - -function DNF_REMOVE() { - __pkg_list=$1 - type=${2-0} - - if [ ${type} -eq 0 ]; then - if ! dnf -y remove ${__installed_pkgs[*]} ${__pkg_list[*]}; then - LOG_ERROR "pkg_list:${__installed_pkgs[*]} ${__pkg_list[*]} remove failed." - exit 1 - fi - else - if ! dnf -y remove ${__pkg_list}; then - LOG_ERROR "pkg_list:${__pkg_list[*]} remove failed." - exit 1 - fi - fi -} - -function SLEEP_WAIT() { - wait_time=${1-1} - cmd=$2 - sleep_time=0 - - while [ $sleep_time -lt $wait_time ]; do - sleep 1 - if [ -n "$cmd" ]; then - if $cmd; then - return 0 - fi - fi - ((sleep_time++)) - done -} - -function REMOTE_REBOOT_WAIT() { - remoteip=$1 - remotepasswd=$2 - remoteuser=$3 - count=0 - - if [[ "$(dmidecode -s system-product-name)" =~ "KVM" ]]; then - SLEEP_WAIT 60 - else - SLEEP_WAIT 200 - fi - - while [ $count -lt 60 ]; do - if ping -c 1 $remoteip; then - if SSH_CMD "echo '' > /dev/null 2>&1" $remoteip $remotepasswd $remoteuser; then - return 0 - else - SLEEP_WAIT 10 - ((count++)) - fi - else - SLEEP_WAIT 10 - ((count++)) - fi - done - - return 1 +function LOG_ERROR() { + message=${*-"Developer does not write the log messages."} + python3 ${OET_PATH}/libs/locallibs/mugen_log.py --level 'error' --message "$message" } function CHECK_RESULT() { @@ -174,13 +83,42 @@ function CASE_RESULT() { exit $exec_result } +function POST_TEST_DEFAULT() { + LOG_INFO "$0 post_test" +} + +function main() { + if [ -n "$(type -t post_test)" ]; then + trap post_test EXIT INT HUP TERM || exit 1 + else + trap POST_TEST_DEFAULT EXIT INT HUP TERM || exit 1 + fi + + if ! rpm -qa | grep expect >/dev/null 2>&1; then + dnf -y install expect + fi + + if [ -n "$(type -t config_params)" ]; then + config_params + fi + + if [ -n "$(type -t pre_test)" ]; then + pre_test + fi + + if [ -n "$(type -t run_test)" ]; then + run_test + CASE_RESULT $? + fi +} + function SSH_CMD() { cmd=$1 remoteip=$2 - remotepasswd=${3:-"openEuler12#$"} - remoteuser=${4:-"root"} - timeout=${5:-"300"} - connport=${6:-"22"} + remotepasswd=${3-openEuler12#$} + remoteuser=${4-root} + timeout=${5-300} + connport=${6-22} bash ${OET_PATH}/libs/locallibs/sshcmd.sh -c "$cmd" -i "$remoteip" -u "$remoteuser" -p "$remotepasswd" -t "$timeout" -o "$connport" ret=$? @@ -200,33 +138,108 @@ function SSH_SCP() { return $ret } -function POST_TEST_DEFAULT() { - LOG_INFO "$0 post_test" +function P_SSH_CMD() { + python3 ${OET_PATH}/libs/locallibs/ssh_cmd.py "$@" } -function main() { - share_arg +function SFTP() { + python3 ${OET_PATH}/libs/locallibs/sftp.py "$@" +} - if [ -n "$(type -t post_test)" ]; then - trap post_test EXIT INT TERM || exit 1 - else - trap POST_TEST_DEFAULT EXIT INT TERM || exit 1 - fi +function TEST_NIC() { + id=${1-1} + python3 ${OET_PATH}/libs/locallibs/get_test_device.py \ + --device nic --node "$id" +} - if ! rpm -qa | grep expect >/dev/null 2>&1; then - dnf -y install expect +function TEST_DISK() { + id=${1-1} + python3 ${OET_PATH}/libs/locallibs/get_test_device.py \ + --device disk --node "$id" +} + +function DNF_INSTALL() { + pkgs=$1 + node=${2-1} + #多节点初始系统环境相同,本地和远端安装的包,在任何节点不不应该存在 + [ -z "$tmpfile" ] && tmpfile="" + + tmpfile2=$(python3 ${OET_PATH}/libs/locallibs/rpm_manage.py \ + install --pkgs "$pkgs" --node $node --tempfile "$tmpfile") + + [ -z "$tmpfile" ] && tmpfile=$tmpfile2 +} + +function DNF_REMOVE() { + node=${1-1} + pkg_list=${2-""} + mode=${3-0} + + if [[ -z "$tmpfile" && -z "$pkg_list" ]]; then + LOG_WARN "no thing to do." + return 0 fi - if [ -n "$(type -t config_params)" ]; then - config_params + [ $mode -ne 0 ] && { + tmpf=$tmpfile + tmpfile="" + } + + if [ "$node" == 0 ]; then + node_num=$(python3 ${OET_PATH}/libs/locallibs/read_conf.py node-num) + + for node_id in $(seq 1 $node_num); do + python3 ${OET_PATH}/libs/locallibs/rpm_manage.py \ + remove --node $node_id --pkgs "$pkg_list" --tempfile "$tmpfile" + done + else + python3 ${OET_PATH}/libs/locallibs/rpm_manage.py \ + remove --node $node --pkgs "$pkg_list" --tempfile "$tmpfile" fi - if [ -n "$(type -t pre_test)" ]; then - pre_test + [ $mode -ne 0 ] && { + tmpfile=$tmpf + } +} + +function GET_FREE_PORT() { + ip=${1-""} + start_port=${2-1000} + end_port=${3-10000} + python3 ${OET_PATH}/libs/locallibs/free_port.py \ + get --ip "$ip" --start "$start_port" --end "$end_port" +} + +function IS_FREE_PORT() { + port=$1 + ip=${2-""} + python3 ${OET_PATH}/libs/locallibs/free_port.py \ + check --port "$port" --ip "$ip" +} + +function REMOTE_REBOOT() { + node=${1-2} + waittime=${2-""} + if [ "$waittime"x != ""x ]; then + python3 ${OET_PATH}/libs/locallibs/remote_reboot.py "reboot" --node $node --waittime $waittime + else + python3 ${OET_PATH}/libs/locallibs/remote_reboot.py "reboot" --node $node fi +} - if [ -n "$(type -t run_test)" ]; then - run_test - CASE_RESULT $? +function REMOTE_REBOOT_WAIT() { + node=${1-2} + waittime=${2-""} + if [ "$waittime"x != ""x ]; then + python3 ${OET_PATH}/libs/locallibs/remote_reboot.py "wait" --node $node --waittime $waittime + else + python3 ${OET_PATH}/libs/locallibs/remote_reboot.py "wait" --node $node fi } + +function SLEEP_WAIT() { + wait_time=$1 + cmd=$2 + mode=${3-1} + python3 ${OET_PATH}/libs/locallibs/sleep_wait.py --time $wait_time --cmd "$cmd" --mode $mode +} diff --git a/mugen/libs/locallibs/free_port.py b/mugen/libs/locallibs/free_port.py new file mode 100644 index 00000000..5837047e --- /dev/null +++ b/mugen/libs/locallibs/free_port.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-22 17:03:00 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 查找空闲端口或检查端口是否空闲 +""" + + +import os +import sys +import subprocess +import socket +import random +import telnetlib +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + + +def find_free_port(ip="", start=1000, stop=10000): + """查找未使用的端口号 + + Args: + ip (str, optional): 指定查找的ip地址,默认未本地. Defaults to "". + start (int, optional): 空闲端口查找范围的起始端口号. Defaults to 1000. + stop (int, optional): 空闲端口查找范围的结束端口号. Defaults to 10000. + + Returns: + [int]: 空闲端口号 + """ + if start > stop: + mugen_log.logging( + "error", + "The initial value of the port range must be less than or equal to the end value.", + ) + sys.exit(2) + + if ip == "": + conn = socket.socket() + else: + exitcode = subprocess.getstatusoutput("ping " + ip + " -c 1")[0] + if exitcode != 0: + mugen_log.logging("error", "Unable to establish connection with IP:" + ip) + sys.exit(519) + + count = 0 + while count < 100: + count += 1 + port = random.randint(start, stop) + if ip == "": + try: + conn.bind(("", port)) + conn.close() + return port + except Exception: + continue + + else: + try: + telnetlib.Telnet(ip, port) + continue + except Exception: + return port + + +def is_free_port(port, ip=""): + """检测端口号是否被使用 + + Args: + port ([int]): 需要被检查的端口号 + ip (str, optional): 需要检查端口号的机器ip地址. Defaults to "". + + Returns: + [int]: 被使用-1,未被使用-0 + """ + if ip == "": + conn = socket.socket() + else: + exitcode = subprocess.getstatusoutput("ping " + ip + " -c 1")[0] + if exitcode != 0: + mugen_log.logging("error", "Unable to establish connection with IP:" + ip) + sys.exit(519) + + if ip == "": + try: + conn.bind(("", port)) + return 0 + except Exception: + return 1 + else: + try: + telnetlib.Telnet(ip, port) + return 1 + except Exception: + return 0 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("operation", type=str, choices=["get", "check"], default=None) + parser.add_argument("--ip", type=str, default="") + parser.add_argument("--port", type=int, default=None) + parser.add_argument("--start", type=int, default=1000) + parser.add_argument("--end", type=int, default=100000) + + args = parser.parse_args() + + if sys.argv[1] == "get": + print(find_free_port(args.ip, args.start, args.end)) + elif sys.argv[1] == "check": + sys.exit(is_free_port(args.port, args.ip)) + else: + mugen_log.logging( + "error", + "usage: free_port.py get|install [-h] [--ip IP] [--port PORT] [--start START PORT] [--end END PORT]", + ) + sys.exit(1) diff --git a/mugen/libs/locallibs/get_test_device.py b/mugen/libs/locallibs/get_test_device.py new file mode 100644 index 00000000..dc29354e --- /dev/null +++ b/mugen/libs/locallibs/get_test_device.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-22 10:52:19 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 测试设备名获取 +""" + +import os +import sys +import subprocess +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import ssh_cmd +import mugen_log +import rpm_manage + + +def get_test_nic(node=1): + """获取可测试使用的网卡 + + Args: + node (int, optional): 节点号. Defaults to 1. + + Returns: + [str]: 网卡名 + """ + exitcode, tmpfile = rpm_manage.rpm_install(pkgs="lshw", node=node) + if exitcode != 0: + mugen_log.logging( + "error", + "Failed to install the dependent software package required to obtain the network card.", + ) + sys.exit(exitcode) + + if os.environ.get("NODE" + str(node) + "_LOCALTION") == "local": + output = subprocess.getoutput( + "lshw -class network | grep -A 5 'description: Ethernet interface' | grep 'logical name:' | awk '{print $NF}' | grep -v '" + + os.environ.get("NODE" + str(node) + "_NIC") + + "'" + ).replace("\n", " ") + else: + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + ) + + output = ssh_cmd.pssh_cmd( + conn, + "lshw -class network | grep -A 5 'description: Ethernet interface' | grep 'logical name:' | awk '{print $NF}' | grep -v '" + + os.environ.get("NODE" + str(node) + "_NIC") + + "'", + )[1].replace("\n", " ") + + ssh_cmd.pssh_close(conn) + + if tmpfile is not None: + rpm_manage.rpm_remove(node=node, tmpfile=tmpfile) + + return output + + +def get_test_disk(node=1): + """获取可测试使用的磁盘 + + Args: + node (int, optional): 节点号. Defaults to 1. + + Returns: + [str]: 磁盘名称 + """ + if os.environ.get("NODE" + str(node) + "LOCALTION") == "local": + used_disk = subprocess.getoutput( + "lsblk -l | grep -e '/.*\|\[.*\]' | awk '{print $1}' | tr -d '[0-9]' | uniq | sed -e ':a;N;$!ba;s/\\n/ /g'" + ) + + test_disk = subprocess.getoutput( + "lsblk -n | grep -v '└─.*\|" + + used_disk.replace(" ", "\|") + + "' | awk '{print $1}' | sed -e ':a;N;$!ba;s/\\n/ /g'" + ) + else: + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + ) + used_disk = ssh_cmd.pssh_cmd( + conn, + "lsblk -l | grep -e '/.*\|\[.*\]' | awk '{print $1}' | tr -d '[0-9]' | uniq | sed -e ':a;N;$!ba;s/\\n/ /g'", + )[1] + test_disk = ssh_cmd.pssh_cmd( + conn, + "lsblk -n | grep -v '└─.*\|" + + used_disk.replace(" ", "\|") + + "' | awk '{print $1}' | sed -e ':a;N;$!ba;s/\\n/ /g'", + )[1] + + return test_disk + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--node", type=int, default=1) + parser.add_argument("--device", type=str, choices=["nic", "disk"], default="nic") + args = parser.parse_args() + + if args.device == "nic": + print(get_test_nic(args.node)) + elif args.device == "disk": + print(get_test_disk(args.node)) + else: + mugen_log.logging( + "warn", + "No other test driven acquisition is provided at this time, you can issue to us for follow-up.", + ) + sys.exit(1) diff --git a/mugen/libs/locallibs/mugen_log.py b/mugen/libs/locallibs/mugen_log.py new file mode 100644 index 00000000..52033401 --- /dev/null +++ b/mugen/libs/locallibs/mugen_log.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +""" + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-20 15:37:16 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 日志输出模板 +""" + + +import sys +import time +import argparse + + +def logging(level, message): + """日志打印模板 + + Args: + level ([str]): 日志等级 + message ([str]): 日志信息 + """ + level_list = ["INFO", "WARN", "DEBUG", "ERROR"] + log_level = level.upper() + + if level.upper() not in level_list: + sys.exit(1) + + if log_level in ["INFO", "WARN"]: + log_level = level.upper() + " " + sys.stderr.write( + "%s - %s - %s\n" + % (time.asctime(time.localtime(time.time())), log_level, message) + ) + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--level", type=str, default="info") + parser.add_argument( + "--message", type=str, default="Developer does not write the log messages." + ) + args = parser.parse_args() + + logging(args.level, args.message) diff --git a/mugen/libs/locallibs/read_conf.py b/mugen/libs/locallibs/read_conf.py new file mode 100644 index 00000000..5ad3f905 --- /dev/null +++ b/mugen/libs/locallibs/read_conf.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-20 17:08:33 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 读取框架环境变量 +""" + +import sys +import os +import json +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + +NODE_ITEM = [ + "LOCALTION", + "MACHINE", + "FRAME", + "USER", + "PASSWORD", + "SSH_PORT", + "NIC", + "IPV4", + "MAC", + "HOST_IP", + "HOST_USER", + "HOST_PASSWORD", + "BMC_IP", + "BMC_USER", + "BMC_PASSWORD", +] + + +def parse_json(): + """解析环境变量配置文件 + + Returns: + [dict]: 环境变量配置 + """ + if not os.path.exists("/etc/mugen"): + OET_PATH = os.environ.get("OET_PATH") + if OET_PATH is None: + mugen_log.logging("error", "环境变量:OET_PATH不存在,请检查mugen框架是否正确安装.") + return 1 + + conf_path = OET_PATH.rstrip("/") + "/" + "conf/env.json" + else: + conf_path = "/etc/mugen/env.json" + + if not os.path.exists(conf_path): + mugen_log.logging("error", "环境配置文件不存在,请先配置环境信息.") + sys.exit(1) + + try: + with open(conf_path, "r") as f: + return json.loads(f.read()) + except json.decoder.JSONDecodeError as e: + mugen_log.logging(e) + sys.exit(1) + + +def read_configure(): + """读取配置文件内容 + + Returns: + [str]: 环境变量 + """ + env_data = parse_json() + + env_var = "" + for node in env_data["NODE"]: + for item in NODE_ITEM: + if node["MACHINE"] == "kvm" and "BMC" in item: + continue + if node["MACHINE"] == "physical" and "HOST" in item: + continue + + env_var += ( + "export NODE" + + str(node["ID"]) + + "_" + + item + + "=" + + str(node[item]) + + "\n" + ) + return env_var + + +def node_num(): + """获取环境节点数 + + Returns: + [int]: 节点数 + """ + env_data = parse_json() + + node_list = list() + for node_data in env_data["NODE"]: + node_list.append(node_data["ID"]) + + return sorted(node_list)[-1] + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument( + "operation", type=str, choices=["env-var", "node-num"], default=None + ) + args = parser.parse_args() + + if args.operation == "env-var": + print(read_configure()) + elif args.operation == "node-num": + print(node_num()) diff --git a/mugen/libs/locallibs/remote_reboot.py b/mugen/libs/locallibs/remote_reboot.py new file mode 100644 index 00000000..b538c149 --- /dev/null +++ b/mugen/libs/locallibs/remote_reboot.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-22 17:20:06 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 远端重启 +""" + + +import os +import sys +import subprocess +import time +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log +import ssh_cmd + + +def reboot_wait(node=2, wait_time=None): + """等待重启成功 + + Args: + node (int, optional): 节点号. Defaults to 2. + wait_time ([int], optional): 等待重启的时长,默认虚拟机:300s,物理机:600s. Defaults to None. + + Returns: + [int]: 成功-0,失败-非0 + """ + count = 0 + + if node == 1: + mugen_log.logging( + "error", "The local machine is unavailable for reboot operation" + ) + sys.exit(1) + + machine_type = os.environ.get("NODE" + str(node) + "_MACHINE") + + if wait_time is not None: + time_sleep = wait_time + elif machine_type.lower() == "kvm": + time_sleep = 300 + else: + time_sleep = 600 + + while [count < time_sleep]: + exitcode = subprocess.getstatusoutput( + "ping -c 3 -w 3 " + os.environ.get("NODE" + str(node) + "_IPV4") + )[0] + if exitcode == 0: + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + log_level="warn", + ) + if conn: + if ssh_cmd.pssh_cmd(conn, "ls")[1]: + ssh_cmd.pssh_close(conn) + return 0 + time.sleep(1) + count += 1 + + mugen_log.logging( + "error", + "The remote machine:%s failed to restart." + % os.environ.get("NODE" + str(node) + "_IPV4"), + ) + return 519 + + +def remote_reboot(node=2, wait_time=None): + """重启 + + Args: + node (int, optional): 节点号. Defaults to 2. + wait_time ([int], optional): 等待重启的时长,默认虚拟机:300s,物理机:600s. Defaults to None. + """ + if node == 1: + mugen_log.logging( + "error", "The local machine is unavailable for reboot operation" + ) + sys.exit(1) + + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + ) + exitcode, error_output = ssh_cmd.pssh_cmd(conn, "reboot") + ssh_cmd.pssh_close(conn) + if exitcode != -1: + mugen_log.logging("error", error_output) + sys.exit(exitcode) + + sys.exit(reboot_wait(node, wait_time)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + usage="remote_reboot.py reboot|wait [-h] [--node NODE]", + description="manual to this script", + ) + parser.add_argument("operation", type=str, choices=["reboot", "wait"], default=None) + parser.add_argument("--node", type=int, default=2) + parser.add_argument("--waittime", type=int, default=None) + args = parser.parse_args() + + if args.operation == "wait": + sys.exit(reboot_wait(args.node, args.waittime)) + elif args.operation == "reboot": + remote_reboot(args.node, args.waittime) + else: + mugen_log.logging( + "error", "usage: remote_reboot.py reboot|wait [-h] [--node NODE]" + ) + sys.exit(1) diff --git a/mugen/libs/locallibs/rpm_manage.py b/mugen/libs/locallibs/rpm_manage.py new file mode 100644 index 00000000..89ac71fd --- /dev/null +++ b/mugen/libs/locallibs/rpm_manage.py @@ -0,0 +1,180 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-22 11:37:36 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 软件包的安装卸载 +""" + +import os +import sys +import subprocess +import tempfile +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log +import ssh_cmd + + +def local_cmd(cmd, conn=None): + """本地命令执行 + + Args: + cmd ([str]): 需要执行的命令 + conn ([class], optional): 建立和远端的连接. Defaults to None. + + Returns: + [list]: 命令执行后的返回码,命令执行结果 + """ + exitcode, output = subprocess.getstatusoutput(cmd) + return exitcode, output + + +def rpm_install(pkgs, node=1, tmpfile=""): + """安装软件包 + + Args: + pkgs ([str]): 软包包名,"bc" or "bc vim" + node (int, optional): 节点号. Defaults to 1. + tmpfile (str, optional): 软件包及其依赖包的缓存文件. Defaults to "". + + Returns: + [list]: 错误码,安装的包的列表 + """ + if pkgs == "": + mugen_log.logging("error", "the following arguments are required:pkgs") + sys.exit(1) + + localtion = os.environ.get("NODE" + str(node) + "_LOCALTION") + if localtion == "local": + conn = None + func = local_cmd + else: + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + ) + func = ssh_cmd.pssh_cmd + + result = func(conn=conn, cmd="dnf --assumeno install " + pkgs)[1] + if "is already installed" in result and "Nothing to do" in result: + mugen_log.logging("info", "pkgs:(%s) is already installed" % pkgs) + return 0, None + + repoCode, repoList = func( + conn=conn, + cmd="dnf repolist | awk '{print $NF}' | sed -e '1d;:a;N;$!ba;s/\\n/ /g'", + ) + if repoCode != 0: + return repoCode, repoList + + depCode, depList = func( + conn=conn, + cmd="dnf --assumeno install " + + pkgs + + ' 2>&1 | grep -wE "$(echo ' + + repoList + + " | sed 's/ /|/g')\" | grep -wE \"$(uname -m)|noarch\" | awk '{print $1}'", + ) + if depCode != 0: + return depCode, depList + + exitcode, result = func(conn=conn, cmd="dnf -y install " + pkgs) + + if tmpfile == "": + tmpfile = tempfile.mkstemp(dir="/tmp")[1] + + with open(tmpfile, "a+") as f: + for depPkg in depList.split("\n"): + f.write(depPkg + " ") + + if exitcode == 0: + result = f.name + + return exitcode, result + + +def rpm_remove(node=1, pkgs="", tmpfile=""): + """卸载软件包 + + Args: + node (int, optional): 节点号. Defaults to 1. + pkgs (str, optional): 需要卸载的软件包. Defaults to "". + tmpfile (str, optional): 安装时所有涉及的包. Defaults to "". + + Returns: + list: 错误码,卸载列表或错误信息 + """ + if pkgs == "" and tmpfile == "": + mugen_log.logging( + "error", "Packages or package files these need to be removed must be added" + ) + sys.exit(1) + + localtion = os.environ.get("NODE" + str(node) + "_LOCALTION") + if localtion == "local": + conn = None + func = local_cmd + else: + conn = ssh_cmd.pssh_conn( + os.environ.get("NODE" + str(node) + "_IPV4"), + os.environ.get("NODE" + str(node) + "_PASSWORD"), + os.environ.get("NODE" + str(node) + "_SSH_PORT"), + os.environ.get("NODE" + str(node) + "_USER"), + ) + func = ssh_cmd.pssh_cmd + + depList = "" + if tmpfile != "": + with open(tmpfile, "r") as f: + depList = f.read() + + exitcode = func(conn=conn, cmd="dnf -y remove " + pkgs + " " + depList)[0] + if localtion != "local": + ssh_cmd.pssh_close(conn) + return exitcode + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser( + usage="rpm_manage.py install|remove [-h] [--node NODE] [--pkgs PKG] [--tempfile TEPMFILE]", + description="manual to this script", + ) + parser.add_argument( + "operation", type=str, choices=["install", "remove"], default=None + ) + parser.add_argument("--node", type=int, default=1) + parser.add_argument("--pkgs", type=str, default="") + parser.add_argument("--tempfile", type=str, default="") + args = parser.parse_args() + + if sys.argv[1] == "install": + exitcode, output = rpm_install(args.pkgs, args.node, args.tempfile) + if output is not None: + print(output) + sys.exit(exitcode) + elif sys.argv[1] == "remove": + exitcode = rpm_remove(args.node, args.pkgs, args.tempfile) + sys.exit(exitcode) + else: + mugen_log.logging( + "error", + "usage: rpm_manage.py install|remove [-h] [--node NODE] [--pkg PKG] [--tempfile TEPMFILE]", + ) + sys.exit(1) diff --git a/mugen/libs/locallibs/sftp.py b/mugen/libs/locallibs/sftp.py new file mode 100644 index 00000000..aa93ede5 --- /dev/null +++ b/mugen/libs/locallibs/sftp.py @@ -0,0 +1,228 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-21 16:14:27 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 文件传输 +""" + +import os +import sys +import stat +import re +import paramiko +import argparse +import subprocess + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log +import ssh_cmd + + +def get_remote_file(sftp, remote_dir, remote_file=None): + """获取对端文件 + + Args: + sftp (class): 和对端建立连接 + remote_dir ([str]): 远端需要传输的文件所在的目录 + remote_file ([str], optional): 远端需要传输的文件. Defaults to None. + + Returns: + [list]: 文件列表 + """ + all_file = list() + + remote_dir = remote_dir.rstrip("/") + + dir_files = sftp.listdir_attr(remote_dir) + for d_f in dir_files: + if remote_file is not None and re.match(remote_file, d_f.filename) is None: + continue + + _name = remote_dir + "/" + d_f.filename + if stat.S_ISDIR(d_f.st_mode): + all_file.extend(get_remote_file(sftp, _name)) + else: + all_file.append(_name) + + return all_file + + +def psftp_get(conn, remote_dir, remote_file="", local_dir=os.getcwd()): + """获取远端文件 + + Args: + conn ([class]): 和远端建立连接 + remote_dir ([str]): 远端需要传输的文件所在的目录 + remote_file ([str], optional): 远端需要传输的文件. Defaults to None. + local_dir ([str], optional): 本地存放文件的目录. Defaults to os.getcwd(). + """ + if conn == 519: + sys.exit(519) + + sftp = paramiko.SFTPClient.from_transport(conn.get_transport()) + + if ssh_cmd.pssh_cmd(conn, "test -d " + remote_dir)[0]: + mugen_log.logging("error", "remote dir:%s does not exist" % remote_dir) + conn.close() + sys.exit(1) + + all_file = list() + if remote_file == "": + all_file = get_remote_file(sftp, remote_dir) + else: + if ssh_cmd.pssh_cmd(conn, "test -f " + remote_file)[0]: + mugen_log.logging("error", "remote file:%s does not exist" % remote_file) + conn.close() + sys.exit(1) + + all_file = get_remote_file(sftp, remote_dir, remote_file) + + for f in all_file: + if remote_file == "": + storage_dir = remote_dir.split("/")[-1] + storage_path = os.path.join( + local_dir, storage_dir + os.path.dirname(f).split(storage_dir)[-1] + ) + if not os.path.exists(storage_path): + os.makedirs(storage_path) + sftp.get(f, os.path.join(storage_path, f.split("/")[-1])) + else: + sftp.get(f, os.path.join(local_dir, f.split("/")[-1])) + mugen_log.logging("info", "start to get file:%s......" % f) + + conn.close() + + +def get_local_file(local_dir, local_file=None): + """获取本地文件列表 + + Args: + local_dir ([str]): 本地文件所在的目录 + local_file ([str], optional): 本地需要传输的文件. Defaults to None. + + Returns: + [list]: 文件列表 + """ + all_file = list() + + local_dir = local_dir.rstrip("/") + + dir_files = os.listdir(local_dir) + for d_f in dir_files: + if local_file is not None and re.match(local_file, d_f) is None: + continue + + _name = local_dir + "/" + d_f + if os.path.isdir(_name): + all_file.extend(get_local_file(_name)) + else: + all_file.append(_name) + + return all_file + + +def psftp_put(conn, local_dir=os.getcwd(), local_file="", remote_dir=""): + """将本地文件传输到远端 + + Args: + conn ([class]): 和远端建立连接 + local_dir ([str]): 本地文件所在的目录 + local_file ([str], optional): 本地需要传输的文件. Defaults to None. + remote_dir (str, optional): 远端存放文件的目录. Defaults to 根目录. + """ + if conn == 519: + sys.exit(519) + + sftp = paramiko.SFTPClient.from_transport(conn.get_transport()) + + if subprocess.getstatusoutput("test -d " + local_dir)[0]: + mugen_log.logging("error", "local dir:%s does not exist" % local_dir) + conn.close() + sys.exit(1) + + all_file = list() + if local_file == "": + all_file = get_local_file(local_dir) + else: + if subprocess.getstatusoutput("test -f " + local_dir + local_file)[0]: + mugen_log.logging("error", "local file:%s does not exist" % local_file) + conn.close() + sys.exit(1) + all_file = get_local_file(local_dir, local_file) + + if remote_dir == "": + remote_dir = ssh_cmd.pssh_cmd(conn, "pwd")[1] + + for f in all_file: + if local_file == "": + storage_dir = local_dir.split("/")[-1] + storage_path = os.path.join( + remote_dir, storage_dir + os.path.dirname(f).split(storage_dir)[-1] + ) + if ssh_cmd.pssh_cmd(conn, "test -d " + storage_path)[0]: + ssh_cmd.pssh_cmd(conn, "mkdir -p " + storage_path) + sftp.put(f, os.path.join(storage_path, f.split("/")[-1])) + else: + sftp.put(f, os.path.join(remote_dir, f.split("/")[-1])) + mugen_log.logging("info", "start to put file:%s......" % f) + + conn.close() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("operation", type=str, choices=["get", "put"], default=None) + parser.add_argument( + "--remotedir", type=str, default=None, help="Must be an absolute path" + ) + parser.add_argument("--node", type=int, default=2) + parser.add_argument("--remotefile", type=str, default="") + parser.add_argument("--localdir", type=str, default=os.getcwd()) + parser.add_argument("--localfile", type=str, default="") + parser.add_argument("--ip", type=str, default=None) + parser.add_argument("--password", type=str, default=None) + parser.add_argument("--port", type=int, default=22) + parser.add_argument("--user", type=str, default="root") + parser.add_argument("--timeout", type=int, default=None) + args = parser.parse_args() + + if args.node is not None: + args.ip = os.environ.get("NODE" + str(args.node) + "_IPV4") + args.password = os.environ.get("NODE" + str(args.node) + "_PASSWORD") + args.port = os.environ.get("NODE" + str(args.node) + "_SSH_PORT") + args.user = os.environ.get("NODE" + str(args.node) + "_USER") + + if ( + args.ip is None + or args.password is None + or args.port is None + or args.user is None + ): + mugen_log.logging( + "error", + "You need to check the environment configuration file to see if this node information exists.", + ) + sys.exit(1) + + conn = ssh_cmd.pssh_conn(args.ip, args.password, args.port, args.user, args.timeout) + + if sys.argv[1] == "get": + psftp_get(conn, args.remotedir, args.remotefile, args.localdir) + elif sys.argv[1] == "put": + psftp_put(conn, args.localdir, args.localfile, args.remotedir) + else: + mugen_log.logging("error", "the following arguments are required:get|put") + sys.exit(1) diff --git a/mugen/libs/locallibs/sleep_wait.py b/mugen/libs/locallibs/sleep_wait.py new file mode 100644 index 00000000..8914f33f --- /dev/null +++ b/mugen/libs/locallibs/sleep_wait.py @@ -0,0 +1,92 @@ +# -*- coding: utf-8 -*- +""" +# @Author : lemon-higgins +# @Date : 2021-07-01 02:09:51 +# @Email : lemon.higgins@aliyun.com +# @License: Mulan PSL v2 +# @Desc : 命令执行超时 +""" + +import subprocess +import time +import os +import sys +import argparse +import re + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + + +def sleep_wait(wait_time, cmd=None, mode=1): + """等待命令执行时长 + + Args: + wait_time ([str]): 待定时间 + cmd ([str], optional): 执行的命令. Defaults to None. + mode (int, optional): 命令执行等待模式. Defaults to 1. + """ + if list(wait_time)[-1] == "m": + wait_time = int(wait_time.replace("m", "")) * 60 + elif list(wait_time)[-1] == "h": + wait_time = int(wait_time.replace("h", "")) * 3600 + elif re.search("[0-9]|s", list(wait_time)[-1]): + wait_time = int(wait_time.replace("s", "")) + else: + mugen_log.logging("error", "sleep_wait的等待超时时长当前只支持小时(h),分钟(m),秒(s).") + sys.exit(1) + + if not cmd: + time.sleep(wait_time) + sys.exit(0) + if cmd and mode == 1: + try: + p = subprocess.Popen( + cmd, + stderr=sys.stderr, + close_fds=True, + stdout=sys.stdout, + text=True, + shell=True, + ) + p.communicate(timeout=wait_time) + exitcode = p.returncode + except subprocess.CalledProcessError as e: + mugen_log.logging("error", "CallError :" + e.output.decode("utf-8")) + exitcode = e.returncode + except subprocess.TimeoutExpired as e: + mugen_log.logging("error", "Timeout : " + str(e)) + p.send_signal(2) + exitcode = 143 + except KeyboardInterrupt as e: + mugen_log.logging("error", "KeyboardInterrupt : 使用ctrl c结束了进程.") + exitcode = 1 + except Exception as e: + mugen_log.logging("error", "Unknown Error : " + str(e)) + exitcode = 1 + + sys.exit(exitcode) + + if cmd and mode == 2: + init = 0 + while init < wait_time: + time.sleep(1) + exitcode, output = subprocess.getstatusoutput(cmd) + if exitcode == 0: + print(output) + sys.exit(0) + elif init == wait_time: + sys.exit(exitcode) + + init += 1 + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--time", type=str) + parser.add_argument("--cmd", type=str) + parser.add_argument("--mode", type=int, choices=[1, 2], default=1) + args = parser.parse_args() + + sleep_wait(args.time, args.cmd, args.mode) diff --git a/mugen/libs/locallibs/ssh_cmd.py b/mugen/libs/locallibs/ssh_cmd.py new file mode 100644 index 00000000..0c931472 --- /dev/null +++ b/mugen/libs/locallibs/ssh_cmd.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-21 11:54:57 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 远端命令执行 +""" + +import os +import sys +import argparse +import paramiko + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + + +def pssh_conn( + ip, + password, + port=22, + user="root", + timeout=None, + log_level="error", +): + """和远端建立连接 + + Args: + ip ([str]): 远端ip + password ([str]): 远端用户密码 + port (int, optional): 远端ssh的端口号. Defaults to 22. + user (str, optional): 远端用户名. Defaults to "root". + timeout ([int], optional): ssh的超时时长. Defaults to None. + + Returns: + [class]: 建立起来的连接 + """ + conn = paramiko.SSHClient() + conn.set_missing_host_key_policy(paramiko.AutoAddPolicy) + try: + conn.connect(ip, port, user, password, timeout=timeout) + except ( + paramiko.ssh_exception.NoValidConnectionsError, + paramiko.ssh_exception.AuthenticationException, + paramiko.ssh_exception.SSHException, + TypeError, + AttributeError, + ) as e: + mugen_log.logging(log_level, "Failed to connect the remote machine:%s." % ip) + mugen_log.logging(log_level, e) + return 519 + + return conn + + +def pssh_cmd(conn, cmd): + """远端命令执行 + + Args: + conn ([class]): 和远端建立连接 + cmd ([str]): 需要执行的命令 + + Returns: + [list]: 错误码,命令执行结果 + """ + if conn == 519: + return 519, "" + stdin, stdout, stderr = conn.exec_command(cmd, timeout=None) + + exitcode = stdout.channel.recv_exit_status() + + if exitcode == 0: + output = stdout.read().decode("utf-8").strip("\n") + else: + output = stderr.read().decode("utf-8").strip("\n") + + return exitcode, output + + +def pssh_close(conn): + """关闭和远端的连接 + + Args: + conn ([class]): 和远端的连接 + """ + if conn != 519: + conn.close() + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--cmd", type=str, default=None, required=True) + parser.add_argument("--node", type=int, default=2) + parser.add_argument("--ip", type=str, default=None) + parser.add_argument("--password", type=str, default=None) + parser.add_argument("--port", type=int, default=22) + parser.add_argument("--user", type=str, default="root") + parser.add_argument("--timeout", type=int, default=None) + args = parser.parse_args() + + if args.node is not None: + args.ip = os.environ.get("NODE" + str(args.node) + "_IPV4") + args.password = os.environ.get("NODE" + str(args.node) + "_PASSWORD") + args.port = os.environ.get("NODE" + str(args.node) + "_SSH_PORT") + args.user = os.environ.get("NODE" + str(args.node) + "_USER") + + if ( + args.ip is None + or args.password is None + or args.port is None + or args.user is None + ): + mugen_log.logging( + "error", + "You need to check the environment configuration file to see if this node information exists.", + ) + sys.exit(1) + + conn = pssh_conn(args.ip, args.password, args.port, args.user, args.timeout) + exitcode, output = pssh_cmd(conn, args.cmd) + pssh_close(conn) + + print(output) + + sys.exit(exitcode) diff --git a/mugen/libs/locallibs/sshcmd.sh b/mugen/libs/locallibs/sshcmd.sh index 6d046db1..2b7eaa6e 100644 --- a/mugen/libs/locallibs/sshcmd.sh +++ b/mugen/libs/locallibs/sshcmd.sh @@ -29,7 +29,7 @@ function sshcmd() { cmd=${cmd//\$/\\\$} test "$remoteip"x = ""x && LOG_ERROR "Missing ip." - test "$(echo ${remoteip} | awk -F"." '{if ($1!=0 && $NF!=0) split ($0,IPNUM,".")} END { for (k in IPNUM) if (IPNUM[k]==0) print IPNUM[k]; else if (IPNUM[k]!=0 && IPNUM[k]!~/[a-z|A-Z]/ && length(IPNUM[k])<=3 && IPNUM[k]<255 && IPNUM[k]!~/^0/) print IPNUM[k]}' | wc -l)" -ne 4 && LOG_ERROR "the remote ip is Incorrect." && exit 1 + test "$(echo ${remoteip} | awk -F"." '{if ($1!=0 && $NF!=0) split ($0,IPNUM,".")} END { for (k in IPNUM) if (IPNUM[k]==0) print IPNUM[k]; else if (IPNUM[k]!=0 && IPNUM[k]!~/[a-z|A-Z]/ && length(IPNUM[k])<=3 && IPNUM[k]<=255 && IPNUM[k]!~/^0/) print IPNUM[k]}' | wc -l)" -ne 4 && LOG_ERROR "the remote ip is Incorrect." && exit 1 if ping -c 1 ${remoteip} | grep "100% packet loss"; then LOG_ERROR "connection to $remoteip failed." exit 101 diff --git a/mugen/libs/locallibs/suite_case.py b/mugen/libs/locallibs/suite_case.py new file mode 100644 index 00000000..c03d332e --- /dev/null +++ b/mugen/libs/locallibs/suite_case.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-20 19:17:45 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 获取测试套信息 +""" + +import sys +import os +import json +import re +import argparse + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + + +def suite_path(suite): + """获取测试套路径 + + Args: + suite ([str]): 测试套名 + + Returns: + [str]: 测试套路径 + """ + oet_path = os.environ.get("OET_PATH") + if oet_path is None: + mugen_log.logging("error", "环境变量:OET_PATH不存在,请检查mugen框架.") + sys.exit(1) + suite_json = ( + os.environ.get("OET_PATH").rstrip("/") + "/suite2cases/" + suite + ".json" + ) + if not os.path.exists(suite_json): + mugen_log.logging("error", "无法找到测试套的json文件:%s." % suite_json) + sys.exit(1) + + try: + with open(suite_json, "r") as f: + suite_data = json.loads(f.read()) + if suite_data["path"] is None: + mugen_log.logging("error", "json文件:%s中没有path值." % suite_json) + sys.exit(1) + + oet = re.match(r'^"?\${?OET_PATH}?"?', suite_data["path"]) + if oet is not None: + return suite_data["path"].replace(oet.group(), os.environ.get("OET_PATH")) + else: + return suite_data["path"] + + except json.decoder.JSONDecodeError as e: + mugen_log.logging("error", e) + sys.exit(1) + except KeyError as e: + mugen_log.logging("error", "A key:%s error specifying JSON data" % e) + sys.exit(1) + + +def suite_cases(suite): + """获取测试套中用例列表 + + Args: + suite ([str]): 测试套名 + + Returns: + [list]: 用例列表 + """ + oet_path = os.environ.get("OET_PATH") + if oet_path is None: + mugen_log.logging("error", "环境变量:OET_PATH不存在,请检查mugen框架.") + sys.exit(1) + suite_json = ( + os.environ.get("OET_PATH").rstrip("/") + "/suite2cases/" + suite + ".json" + ) + if not os.path.exists(suite_json): + mugen_log.logging("error", "无法找到测试套的json文件:%s." % suite_json) + sys.exit(1) + + try: + with open(suite_json, "r") as f: + suite_data = json.loads(f.read()) + if suite_data["cases"] is None: + mugen_log.logging("error", "json文件:%s中没有cases值." % suite_json) + sys.exit(1) + + case_list = "" + for case_data in suite_data["cases"]: + case_list += case_data["name"] + "\n" + return case_list.rstrip("\n") + + except json.decoder.JSONDecodeError as e: + mugen_log.logging("error", e) + sys.exit(1) + except KeyError as e: + mugen_log.logging("error", "A key:%s error specifying JSON data" % e) + sys.exit(1) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--suite", type=str, default=None) + parser.add_argument("--key", type=str, choices=["path", "cases-name"], default=None) + args = parser.parse_args() + + if args.key == "path": + print(suite_path(args.suite)) + elif args.key == "cases-name": + print(suite_cases(args.suite)) + else: + mugen_log.logging( + "error", "Other key value fetching is not currently supported." + ) diff --git a/mugen/libs/locallibs/write_conf.py b/mugen/libs/locallibs/write_conf.py new file mode 100644 index 00000000..cbb06221 --- /dev/null +++ b/mugen/libs/locallibs/write_conf.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +""" + Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. + This program is licensed under Mulan PSL v2. + You can use it according to the terms and conditions of the Mulan PSL v2. + http://license.coscl.org.cn/MulanPSL2 + THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, + EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, + MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. + See the Mulan PSL v2 for more details. + + @Author : lemon-higgins + @email : lemon.higgins@aliyun.com + @Date : 2021-04-20 15:13:09 + @License : Mulan PSL v2 + @Version : 1.0 + @Desc : 生成测试环境配置 +""" + +import sys +import os +import json +import socket +import subprocess +import argparse +import paramiko + +SCRIPT_PATH = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(SCRIPT_PATH) +import mugen_log + +NODE_DATA = {"ID": 1} + + +def write_conf(ip, password, port=22, user="root"): + """写入测试环境的配置 + + Args: + ip ([str]): 测试环境地址 + password ([str]): 测试环境的用户密码 + port (int, optional): 测试环境ssh端口号. Defaults to 22. + user (str, optional): 测试环境用户名. Defaults to "root". + """ + if None in (ip, password): + mugen_log.logging("error", "必要参数ip or password存在缺失.") + sys.exit(1) + + if not os.path.exists("/etc/mugen"): + OET_PATH = os.environ.get("OET_PATH") + if OET_PATH is None: + mugen_log.logging("error", "环境变量:OET_PATH不存在,请检查mugen框架.") + sys.exit(1) + + conf_path = OET_PATH.rstrip("/") + "/" + "conf/env.json" + os.makedirs(OET_PATH.rstrip("/") + "/" + "conf", exist_ok=True) + else: + conf_path = "/etc/mugen/env.json" + + if os.path.exists(conf_path): + exitcode = subprocess.getstatusoutput("grep " + ip + " " + conf_path)[0] + if exitcode == 0: + mugen_log.logging("warn", "当前机器:" + ip + "的相关信息已经录入到配置文件中.") + sys.exit(0) + + try: + f = open(conf_path, "r") + ENV_DATA = json.loads(f.read()) + f.close() + + node_id_list = list() + for node in ENV_DATA["NODE"]: + node_id_list.append(node["ID"]) + node_id_list.sort() + NODE_DATA.update({"ID": node_id_list[-1] + 1}) + except json.decoder.JSONDecodeError as e: + mugen_log.logging("warn", e) + ENV_DATA = {"NODE": []} + else: + ENV_DATA = {"NODE": []} + + if subprocess.getstatusoutput("ip a | grep " + ip)[0] == 0: + NODE_DATA["LOCALTION"] = "local" + else: + NODE_DATA["LOCALTION"] = "remote" + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy) + try: + ssh.connect(ip, port, user, password) + except paramiko.ssh_exception.NoValidConnectionsError as e: + mugen_log.logging("error", e) + sys.exit(1) + + if os.path.exists(conf_path): + stdin, stdout, stderr = ssh.exec_command( + "ip a | grep 'inet ' | grep -v '127.0.0.1' | awk '{print $2}' | awk -F '/' '{print $1}'" + ) + for remote_ip in stdout.read().decode("utf-8").strip("\n").split("\n"): + exitcode, output = subprocess.getstatusoutput( + "grep " + remote_ip + " " + conf_path + ) + if exitcode == 0: + mugen_log.logging("warn", ip + "和已录入到配置文件中的" + remote_ip + "是同一台机器.") + sys.exit(0) + + stdin, stdout, stderr = ssh.exec_command("hostnamectl | grep 'Virtualization: kvm'") + if stdout.read().decode("utf-8").strip("\n") == "": + NODE_DATA.update({"MACHINE": "physical"}) + else: + NODE_DATA["MACHINE"] = "kvm" + + stdin, stdout, stderr = ssh.exec_command("uname -m") + NODE_DATA["FRAME"] = stdout.read().decode("utf-8").strip("\n") + + stdin, stdout, stderr = ssh.exec_command( + " ip route | grep " + ip + " | awk '{print $3}' | sort -u" + ) + NODE_DATA["NIC"] = stdout.read().decode("utf-8").strip("\n") + + stdin, stdout, stderr = ssh.exec_command( + "cat /sys/class/net/" + NODE_DATA["NIC"] + "/address" + ) + NODE_DATA["MAC"] = stdout.read().decode("utf-8").strip("\n") + + NODE_DATA["IPV4"] = ip + NODE_DATA["USER"] = user + NODE_DATA["PASSWORD"] = password + NODE_DATA["SSH_PORT"] = port + + ssh.close() + + if NODE_DATA["MACHINE"] == "kvm": + NODE_DATA["HOST_IP"] = "" + NODE_DATA["HOST_USER"] = "" + NODE_DATA["HOST_PASSWORD"] = "" + NODE_DATA["HOST_SSH_PORT"] = "" + + if NODE_DATA["MACHINE"] == "physical": + NODE_DATA["BMC_IP"] = "" + NODE_DATA["BMC_USER"] = "" + NODE_DATA["BMC_PASSWORD"] = "" + + ENV_DATA["NODE"].append(NODE_DATA) + + with open(conf_path, "w") as f: + f.write(json.dumps(ENV_DATA, indent=4)) + mugen_log.logging("info", "配置文件加载完成...") + + sys.exit(0) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="manual to this script") + parser.add_argument("--ip", type=str, default=None) + parser.add_argument("--password", type=str, default=None) + parser.add_argument("--port", type=int, default=22) + parser.add_argument("--user", type=str, default="root") + args = parser.parse_args() + + write_conf(args.ip, args.password, args.port, args.user) diff --git a/mugen/libs/results_listener.sh b/mugen/libs/results_listener.sh new file mode 100644 index 00000000..7be29815 --- /dev/null +++ b/mugen/libs/results_listener.sh @@ -0,0 +1,79 @@ +#!/usr/bin/bash +# Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. +# This program is licensed under Mulan PSL v2. +# You can use it according to the terms and conditions of the Mulan PSL v2. +# http://license.coscl.org.cn/MulanPSL2 +# THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PSL v2 for more details. +#################################### +# @Author : Ethan-Zhang +# @email : ethanzhang55@outlook.com +# @Date : 2021-08-10 16:03:00 +# @License : Mulan PSL v2 +# @Version : 1.0 +# @Desc : listener for results +##################################### + +SCRIPT_PATH=$( + cd "$(dirname "$0")" || exit 1 + pwd +) + +INTERVAL=2 +SUCCEED=0 +FAIL=0 + +function is_mugen_running() { + if ps -ef | grep -e 'mugen.sh' | grep -qv 'grep'; then + return 0 + else + return 1 + fi +} + +function rsync_logs() { + [[ -z $(rpm -qa | grep rsync) ]] && yum install -y rsync + echo "$PASSWORD" >/tmp/rsync.password + chmod 600 /tmp/rsync.password + rsync -az --port=873 "${SCRIPT_PATH}"/../logs/* "${DEST_USER}"@"${DEST_IP}"::"${DEST_MODULE}"/"${JOB_NAME}" --password-file=/tmp/rsync.password + return 0 +} + +function check_results() { + if [[ -d ${SCRIPT_PATH}/../results/${TESTSUITE}/succeed ]]; then + SUCCEED=$(ls -l "${SCRIPT_PATH}/../results/${TESTSUITE}/succeed" | grep '^-' | wc -l) + fi + if [[ -d ${SCRIPT_PATH}/../results/${TESTSUITE}/failed ]]; then + FAIL=$(ls -l "${SCRIPT_PATH}/../results/${TESTSUITE}/failed" | grep '^-' | wc -l) + fi +} + +function run_listener() { + while :; do + if [[ -d ${SCRIPT_PATH}/../results/${TESTSUITE} ]]; then + check_results + fi + + curl -d "{\"name\": \"$JOB_NAME\", \"succeed\": \"$SUCCEED\", \"fail\": \"$FAIL\"}" -H 'Content-Type: application/json' -X POST "${SERVER_IP}:${SERVER_PORT}/api/testask/monitor" + + rsync_logs + + sleep ${INTERVAL}s + + if ! is_mugen_running; then + check_results + curl -d "{\"name\": \"$JOB_NAME\", \"succeed\": \"$SUCCEED\", \"fail\": \"$FAIL\"}" -H 'Content-Type: application/json' -X POST "${SERVER_IP}:${SERVER_PORT}/api/testask/monitor" + break + fi + done +} + +for ((i = 1; i <= 300; i++)); do + if is_mugen_running; then + run_listener + break + fi + sleep 1s +done diff --git a/mugen/mugen.sh b/mugen/mugen.sh index 5a8f35da..43d3ca7c 100644 --- a/mugen/mugen.sh +++ b/mugen/mugen.sh @@ -1,5 +1,5 @@ #!/usr/bin/bash -# Copyright (c) [2020] Huawei Technologies Co.,Ltd.ALL rights reserved. +# Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. # This program is licensed under Mulan PSL v2. # You can use it according to the terms and conditions of the Mulan PSL v2. # http://license.coscl.org.cn/MulanPSL2 @@ -8,275 +8,214 @@ # MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. # See the Mulan PSL v2 for more details. #################################### -#@Author : lemon.higgins -#@Contact : lemon.higgins@aliyun.com -#@Date : 2020-04-09 09:39:43 -#@License : Mulan PSL v2 -#@Version : 1.0 -#@Desc : Public function +# @Author : lemon-higgins +# @email : lemon.higgins@aliyun.com +# @Date : 2021-04-20 15:12:00 +# @License : Mulan PSL v2 +# @Version : 1.0 +# @Desc : ##################################### -export OET_PATH=$( +OET_PATH=$( cd "$(dirname "$0")" || exit 1 pwd ) +export OET_PATH source ${OET_PATH}/libs/locallibs/common_lib.sh -export EXECUTE_T="30m" -export case_num=0 -export success_num=0 -export fail_num=0 -export isCheck=yes -export command_x=no -export conf_file="/etc/mugen/env.conf" -export cases_url="https://gitee.com/openeuler/integration-test.git" -if [ ! -d "/etc/mugen" ]; then - export conf_file="${OET_PATH}/conf/env.conf" - mkdir -p ${OET_PATH}/conf -fi +[[ -d "/etc/mugen" ]] && export conf_file="/etc/mugen/mugen.json" || export conf_file="${OET_PATH}/conf/mugen.json" + +TIMEOUT="30m" +COMMAND_X="no" +CASE_NUM=0 +SUCCESS_NUM=0 +FAIL_NUM=0 function usage() { printf "Usage: \n -c: configuration environment of test framework\n - -d: download test cases what provided by the openEuler community.\n -a: execute all use cases\n -f:designated test suite\n -r:designated test case\n -x:the shell script is executed in debug mode\n - -C: the mapping in suite2cases does not need to be checked.(you must ensure that the case name does't exist.) \n Example: run all cases: normal mode: bash mugen.sh -a debug mode: - bash mugen.sh -xa + bash mugen.sh -a -x run test suite: normal mode: bash mugen.sh -f test_suite debug mode: - bash mugen.sh -xf test_suite + bash mugen.sh -f test_suite -x run test case: normal mode: bash mugen.sh -f test_suite -r test_case - normal mode and no check suite2case: - bash mugen.sh -f test_suite -Cr test_case debug mode: - bash mugen.sh -xf test_suite -r test_case - debug mode and no check suite2case: - bash mugen.sh -xf test_suite -Cr test_case + bash mugen.sh -f test_suite -r test_case -x \n - configure env of test framework:\n - bash mugen.sh -c localip username passwd port,like bash mugen.sh -c 1.1.1.1 root xxxxxxx 4567\n" + configure env of test framework: + bash mugen.sh -c --ip \$ip --password \$passwd --user \$user --port \$port\n" } function deploy_conf() { - local ipaddr=$2 - local user=${3:-"root"} - local password=${4:-"openEuler12#$"} - local connport=${5:-"22"} - - if [ -z "$ipaddr" ] || [ -z "$user" ] || [ -z "$password" ] || [ -z "$connport" ]; then - LOG_ERROR "Parameter missing." - exit 1 - fi - - if ! SSH_CMD "echo" "$ipaddr" "$password" "$user" "" "$connport" > /dev/null; then - LOG_ERROR "The user name($user) and password($password) can't be used to log in the address($ipaddr) and port($connport)" - exit 1 - fi - - if [ -f "$conf_file" ]; then - node_num=$(grep -ci 'node' "$conf_file") - fi - ((node_num++)) - - localtion=local - ip addr show | grep $ipaddr >/dev/nul || localtion=remote - - conf=$(SSH_CMD " - nics=(\$(ls /sys/class/net | grep -Ewv 'lo.*|docker.*|bond.*|vlan.*|virbr.*|br.*|vnet.*' | sed 's/ $//g')) - for nic in \${nics\[*\]}; do - mac+=(\$(cat /sys/class/net/\${nic}/address)) - - ipv4+=(\$(ip addr show \${nic} | grep -w inet | awk '{print \$2}' | awk -F '/' '{print \$1}')) - - ipv6+=(\$(ip addr show \${nic} | grep -w inet6 | awk '{print \$2}' | awk -F '/' '{print \$1}')) - done + python3 ${OET_PATH}/libs/locallibs/write_conf.py "$@" + test $? -ne 0 && exit 1 || exit 0 +} - machines=virtual - hostnamectl | grep 'Virtualization: kvm' || machines=physical +function load_conf() { + rm -rf ${OET_PATH}/results - frame=$(uname -m) + export_var=$(python3 ${OET_PATH}/libs/locallibs/read_conf.py env-var) + test $? -ne 0 && exit 1 - result=NODE=$node_num,LOCALTION=$localtion,USER=$user,PASSWORD=$password,MACHINE=\$machines,FRAME=\$frame,NICS='\('\${nics\[@\]}'\)',MAC='\('\${mac\[*\]}'\)',IPV4='\('\${ipv4\[*\]}'\)',IPV6='\('\${ipv6\[*\]}'\)' + $export_var - test \$machines == physical && { - dnf install ipmitool -y >/dev/null - bmcip=\$(ipmitool lan print | grep -i 'ip address' | grep -iv 'source' | awk '{print \$NF}') - result+="BMCIP=\$bmcip,BMCUSER=,BMCPASSWORD=" - } + env_conf="$(echo -e $conf_file | sed -e 's/json/env/')" + printf "%s\n" "$export_var" >$env_conf - echo -e \$result - " "$ipaddr" "$password" "$user" | tail -n 1) +} - echo "$conf" | grep ERROR >/dev/nul && { - LOG_ERROR "Failed in remote CMD operation:1" - exit 1 - } - echo -e "\n$conf" >>"$conf_file" - dos2unix "$conf_file" >/dev/nul 2>&1 +function generate_result_file() { + local suite=$1 + local case=$2 + local exitcode=$3 -} + if [ "$exitcode" -eq 0 ]; then + LOG_INFO "The case exit by code $ret_code." + ((SUCCESS_NUM++)) + result="succeed" + else + LOG_ERROR "The case exit by code $ret_code." + ((FAIL_NUM++)) + result="failed" + fi -function download_testcases() { - local tmpdir - tmpdir=$(mktemp -d) - git clone "$cases_url" "$tmpdir" - cp "$tmpdir"/* "$OET_PATH" -r - rm -rf "$tmpdir" + local result_path="$OET_PATH/results/$suite/$result" + mkdir -p "$result_path" + touch "$result_path"/"$case" } -function process() { +function exec_case() { local cmd=$1 - - (sleep "$EXECUTE_T" && { - case_pid=$(pgrep -f "$cmd") - test -n "$case_pid" && { - if kill -9 "$case_pid" >/dev/nul 2>&1; then - LOG_WARN "The case execution timeout." - fi - } - }) 2>/dev/nul & + local log_path=$2 + local case_name=$3 + local test_suite=$4 exec 6>&1 exec 7>&2 - exec >"$log_path"/"$(date +%Y-%m-%d-%T)".log 2>&1 - - ((case_num++)) + exec >>"$log_path"/"$(date +%Y-%m-%d-%T)".log 2>&1 - $cmd + SLEEP_WAIT $TIMEOUT "$cmd" ret_code=$? exec 1>&6 6>&- exec 2>&7 7>&- - sleep_pid=$(pgrep -f "sleep $EXECUTE_T") - test -n "$sleep_pid" && kill -9 "$sleep_pid" + test "$ret_code"x == "143"x && { + cmd_pid=$(pgrep "$cmd") + if [ -n "$cmd_pid" ]; then + for pid in ${cmd_pid}; do + pstree -p "$pid" | grep -o '([0-9]*)' | tr -d '()' | xargs kill -9 + done + fi + LOG_WARN "The case execution timeout." + } - if [ $ret_code -eq 0 ]; then - LOG_INFO "The case exit by code $ret_code." - mkdir -p ${OET_PATH}/results/succeed - touch ${OET_PATH}/results/succeed/${test_case} - ((success_num++)) - else - LOG_ERROR "The case exit by code $ret_code." - mkdir -p ${OET_PATH}/results/failed - touch ${OET_PATH}/results/failed/${test_case} - ((fail_num++)) - fi + generate_result_file "$test_suite" "$case_name" "$ret_code" } function run_test_case() { local test_suite=$1 local test_case=$2 + export exec_result if [[ -z "$test_suite" || -z "$test_case" ]]; then LOG_ERROR "Parameter(test suite or test case) loss." exit 1 fi - [ "$isCheck"x == "yes"x ] && { - [ -z "$(find "$OET_PATH"/suite2cases -name "$test_suite")" ] && { - LOG_ERROR "In the suite2cases directory, Can't find the file of testsuite:${test_suite}." - return 1 - } - if ! grep -q --line-regexp --fixed-strings "$test_case" suite2cases/"$test_suite"; then - LOG_ERROR "In the suite2cases directory, Can't find the case name:${test_case} in the file of testsuite:${test_suite}." - return 1 - fi + result_files=$(find ${OET_PATH}/results/${test_suite} -name "$test_case" >/dev/null 2>&1) + for result_file in $result_files; do + test -f $result_file && rm -rf $result_file + done + + suite_path=$(python3 ${OET_PATH}/libs/locallibs/suite_case.py --suite $test_suite --key path) + test $? -ne 0 && return 1 + test -d "$suite_path" || { + LOG_ERROR "Path value:${suite_path} in a JSON file that does not exist in the environment." + return 1 } - mapfile suite_paths < <(find "$OET_PATH"/testcases -name "$test_suite") - - if [ ${#suite_paths[@]} -ge 1 ]; then - for suite_path in ${suite_paths[*]}; do - case_path=() - while IFS="" read -r line; do - case_path+=("${line%/*}") - done < <(find "$suite_path" -type f -name "${test_case}.*") - - if [ ${#case_path[@]} -gt 1 ]; then - LOG_ERROR "There are multiple use cases with the same use case name:${test_case} under the test suite:${test_suite}." - return 1 - elif [ ${#case_path[@]} -eq 1 ]; then - break - fi - done - - [ ${#case_path[@]} -eq 0 ] && { - LOG_ERROR "No test cases found under the test suite:${test_suite}." - return 1 - } + ((CASE_NUM++)) + + if ! grep -q "$test_case" suite2cases/"${test_suite}.json"; then + LOG_ERROR "In the suite2cases directory, Can't find the case name:${test_case} in the file of testsuite:${test_suite}." + generate_result_file "$test_suite" "$case_name" 1 + return 1 fi + case_path=($(find ${suite_path} -name "${test_case}.*" | sed -e "s#/${test_case}.\(sh\|py\)##")) + test ${#case_path[@]} -gt 1 && { + LOG_ERROR "Multiple identical test case scripts have been found under the test suite. Please check your use case scripts." + return 1 + } + log_path=${OET_PATH}/logs/${test_suite}/${test_case} mkdir -p ${log_path} - LOG_INFO "start to run testcase:$test_case" + LOG_INFO "start to run testcase:$test_case." + + if [ -z "${case_path[*]}" ]; then + echo -e "Can't find the test script:$test_case, Please confirm whether the code is submitted." >>"$log_path"/"$(date +%Y-%m-%d-%T)".log 2>&1 + exec_case "exit 255" "$log_path" "$test_case" "$test_suite" + fi - pushd "${case_path[*]}" >/dev/null || return 1 + pushd "$case_path" >/dev/null || return 1 - local execute_t - execute_t=$(grep -w --fixed-strings EXECUTE_T ${test_case}.* 2>/dev/nul | awk -F '=' '{print $NF}') - test -n "$execute_t" && EXECUTE_T=$execute_t + local time_out + time_out=$(grep -w --fixed-strings EXECUTE_T ${test_case}.* 2>/dev/null | awk -F '=' '{print $NF}' | tr -d '"') + test -n "$time_out" && local TIMEOUT=$time_out local script_type - script_type=$(ls ${test_case}.* | awk -F '.' '{print $NF}') + script_type=$(find . -name "${test_case}.*" | awk -F '.' '{print $NF}') if [[ "$script_type"x == "sh"x ]] || [[ "$script_type"x == "bash"x ]]; then - if [ "$command_x"x == "yes"x ]; then - process "bash -x ${test_case}.sh" + if [ "$COMMAND_X"x == "yes"x ]; then + exec_case "bash -x ${test_case}.sh" "$log_path" "$test_case" "$test_suite" else - process "bash ${test_case}.sh" + exec_case "bash ${test_case}.sh" "$log_path" "$test_case" "$test_suite" fi elif [ "$script_type"x == "py"x ]; then - process "python3 ${test_case}.py" + exec_case "python3 ${test_case}.py" "$log_path" "$test_case" "$test_suite" fi - popd >/dev/nul || return 1 + popd >/dev/null || return 1 - LOG_INFO "End to run testcase:$test_case" -} - -function case_count() { - - LOG_INFO "A total of ${case_num} use cases were executed, with ${success_num} successes and ${fail_num} failures." - - if [ ${success_num} != ${case_num} ]; then - exit 1 - fi + LOG_INFO "End to run testcase:$test_case." } function run_test_suite() { local test_suite=$1 - [ -z "$(find "$OET_PATH"/suite2cases -name "$test_suite")" ] && { + [ -z "$(find $OET_PATH/suite2cases -name ${test_suite}.json)" ] && { LOG_ERROR "In the suite2cases directory, Can't find the file of testsuite:${test_suite}." return 1 } - for test_case in $(shuf ${OET_PATH}/suite2cases/${test_suite} | tr -d ' '); do + for test_case in $(python3 ${OET_PATH}/libs/locallibs/suite_case.py --suite $test_suite --key cases-name | shuf); do run_test_case "$test_suite" "$test_case" done } function run_all_cases() { - mapfile test_suites < <(find ${OET_PATH}/suite2cases/ -type f -name "*" | awk -F '/' '{print $NF}') + test_suites=($(find ${OET_PATH}/suite2cases/ -type f -name "*.json" | awk -F '/' '{print $NF}' | sed -e 's/.json$//g')) test ${#test_suites[@]} -eq 0 && { LOG_ERROR "Can't find recording about test_suites." return 1 @@ -287,83 +226,77 @@ function run_all_cases() { done } -function load_conf() { - if [ ! -f "$conf_file" ]; then - LOG_ERROR "The configuration file does not exist." - exit 1 - fi +function statistic_result() { - node_num=$(grep -ci 'node=' "$conf_file") - - tmp=$(mktemp) - echo -e "function share_arg() {" >"$tmp" - for id in $(seq 1 $node_num); do - while IFS="" read -r var; do - if echo "$var" | grep '(' >/dev/nul; then - echo -e "$var" >>"$tmp" - else - declare -g -x ${var} - fi - done < <(grep -iw "node=$id" "$conf_file" | sed 's/,/\n/g' | sed "/node=/d;s/^/NODE${id}_/g") - done - echo -e "}" >>"$tmp" - if grep '=(' "$tmp" >/dev/nul; then - source "$tmp" - export -f share_arg - fi -} + LOG_INFO "A total of ${CASE_NUM} use cases were executed, with ${SUCCESS_NUM} successes and ${FAIL_NUM} failures." -function pre_run() { - rm -rf ${OET_PATH}/results - load_conf - $@ - case_count + exit ${FAIL_NUM} } -if ! rpm -qa | grep '^expect' >/dev/null 2>&1; then - yum -y install expect dos2unix >/dev/nul -fi - -while getopts ":xdcaf:Cr:h" option; do +while getopts "c:af:r:dx" option; do case $option in - x) - export command_x="yes" + c) + deploy_conf ${*//-c/} + ;; + d) + echo -e "The test script download function has been discarded." ;; a) - pre_run run_all_cases + if echo "$@" | grep -q -e ' -a *-x *$\| -x *-a *$\| -ax *$\| -xa *$'; then + COMMAND_X="yes" + elif ! echo "$@" | grep -q -e '-a *$'; then + usage + exit 1 + fi + + load_conf + run_all_cases + statistic_result ;; + f) test_suite=$OPTARG - [[ -z "$test_suite" ]] && { + + echo $test_suite | grep -q -e ' -a\| -r \|-x\|-d' && { usage exit 1 } - echo "$@" | grep -e '-r\|-Cr' >/dev/null 2>&1 || { - pre_run "run_test_suite $test_suite" + + echo "$@" | grep -q -e ' *-x *\| *-xf *' && { + COMMAND_X="yes" + } + + echo "$@" | grep -q -e ' -r ' || { + load_conf + run_test_suite $test_suite + statistic_result } ;; r) test_case=$OPTARG - [[ -z "$test_case" ]] && { + echo $test_case | grep -q -e ' -a\| -f\| -x\| -d' && { usage exit 1 } - pre_run "run_test_case $test_suite $test_case" - ;; - C) - isCheck="no" - ;; - c) - deploy_conf "$@" - ;; - d) - download_testcases + + echo "$@" | grep -q -e ' *-x *\| *-xr *\| *-xf *' && { + COMMAND_X="yes" + } + + load_conf + run_test_case $test_suite $test_case + statistic_result ;; - h) - usage + x) + echo "$@" | grep -q -e '^ [a-Z0-9-_]-x *$' && { + LOG_ERROR "The -x parameter must be used in combination with -a, -f, and -r." + exit 1 + } ;; *) usage + exit 1 ;; + esac done diff --git a/mugen/runoet.sh b/mugen/runoet.sh deleted file mode 120000 index 304c6ba0..00000000 --- a/mugen/runoet.sh +++ /dev/null @@ -1 +0,0 @@ -mugen.sh \ No newline at end of file diff --git a/mugen/runoet.sh b/mugen/runoet.sh new file mode 100644 index 00000000..9975943d --- /dev/null +++ b/mugen/runoet.sh @@ -0,0 +1,262 @@ +#!/usr/bin/bash +# Copyright (c) [2021] Huawei Technologies Co.,Ltd.ALL rights reserved. +# This program is licensed under Mulan PSL v2. +# You can use it according to the terms and conditions of the Mulan PSL v2. +# http://license.coscl.org.cn/MulanPSL2 +# THIS PROGRAM IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, +# EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, +# MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +# See the Mulan PSL v2 for more details. +#################################### +# @Author : lemon-higgins +# @email : lemon.higgins@aliyun.com +# @Date : 2021-04-20 15:12:00 +# @License : Mulan PSL v2 +# @Version : 1.0 +# @Desc : +##################################### + +OET_PATH=$( + cd "$(dirname "$0")" || exit 1 + pwd +) +export OET_PATH + +source ${OET_PATH}/libs/locallibs/common_lib.sh + +[[ -d "/etc/mugen" ]] && export conf_file="/etc/mugen/mugen.json" || export conf_file="${OET_PATH}/conf/mugen.json" +REPOSITORY="https://gitee.com/openeuler/integration-test.git" + +TIMEOUT="30m" +COMMAND_X="no" +CASE_NUM=0 +SUCCESS_NUM=0 +FAIL_NUM=0 + +function usage() { + echo TODO + #TODO +} + +function deploy_conf() { + python3 ${OET_PATH}/libs/locallibs/write_conf.py "$@" + test $? -ne 0 && exit 1 +} + +function load_conf() { + rm -rf ${OET_PATH}/results + + export_var=$(python3 ${OET_PATH}/libs/locallibs/read_conf.py env-var) + test $? -ne 0 && exit 1 + + $export_var + + env_conf="$(echo -e $conf_file | sed -e 's/json/env/')" + printf "%s\n" "$export_var" >$env_conf + +} + +function download_cases() { + local tmpdir + tmpdir=$(mktemp -d) + git clone "$REPOSITORY" "$tmpdir" + cp "$tmpdir"/* "$OET_PATH" -r + rm -rf "$tmpdir" +} + +function exec_case() { + local cmd=$1 + local log_path=$2 + local case_name=$3 + + exec 6>&1 + exec 7>&2 + + exec >"$log_path"/"$(date +%Y-%m-%d-%T)".log 2>&1 + + timeout --preserve-status $TIMEOUT $cmd + ret_code=$? + + exec 1>&6 6>&- + exec 2>&7 7>&- + + test "$ret_code"x == "143"x && { + LOG_WARN "The case execution timeout." + } + + if [ $ret_code -eq 0 ]; then + LOG_INFO "The case exit by code $ret_code." + ((SUCCESS_NUM++)) + mkdir -p ${OET_PATH}/results/succeed + touch ${OET_PATH}/results/succeed/${case_name} + else + LOG_ERROR "The case exit by code $ret_code." + ((FAIL_NUM++)) + mkdir -p ${OET_PATH}/results/failed + touch ${OET_PATH}/results/failed/${case_name} + fi +} + +function run_test_case() { + + local test_suite=$1 + local test_case=$2 + + if [[ -z "$test_suite" || -z "$test_case" ]]; then + LOG_ERROR "Parameter(test suite or test case) loss." + exit 1 + fi + + ((CASE_NUM++)) + + suite_path=$(python3 ${OET_PATH}/libs/locallibs/suite_case.py --suite $test_suite --key path) + test $? -ne 0 && return 1 + test -d "$suite_path" || { + LOG_ERROR "Path value:${suite_path} in a JSON file that does not exist in the environment." + return 1 + } + + if ! grep -q "$test_case" suite2cases/"${test_suite}.json"; then + LOG_ERROR "In the suite2cases directory, Can't find the case name:${test_case} in the file of testsuite:${test_suite}." + return 1 + fi + + case_path=($(find ${suite_path} -name "${test_case}.*" | sed -e "s#/${test_case}.\(sh\|py\)##")) + test ${#case_path[@]} -gt 1 && { + LOG_ERROR "Multiple identical test case scripts have been found under the test suite. Please check your use case scripts." + return 1 + } + + log_path=${OET_PATH}/logs/${test_suite}/${test_case} + mkdir -p ${log_path} + + LOG_INFO "start to run testcase:$test_case." + + pushd "$case_path" >/dev/null || return 1 + + local time_out + time_out=$(grep -w --fixed-strings EXECUTE_T ${test_case}.* 2>/dev/nul | awk -F '=' '{print $NF}' | tr -d '"') + test -n "$time_out" && TIMEOUT=$time_out + + local script_type + script_type=$(find . -name "${test_case}.*" | awk -F '.' '{print $NF}') + + if [[ "$script_type"x == "sh"x ]] || [[ "$script_type"x == "bash"x ]]; then + if [ "$COMMAND_X"x == "yes"x ]; then + exec_case "bash -x ${test_case}.sh" "$log_path" "$test_case" + else + exec_case "bash ${test_case}.sh" "$log_path" "$test_case" + fi + elif [ "$script_type"x == "py"x ]; then + exec_case "python3 ${test_case}.py" "$log_path" "$test_case" + fi + + popd >/dev/nul || return 1 + + LOG_INFO "End to run testcase:$test_case." +} + +function run_test_suite() { + local test_suite=$1 + + [ -z "$(find $OET_PATH/suite2cases -name ${test_suite}.json)" ] && { + LOG_ERROR "In the suite2cases directory, Can't find the file of testsuite:${test_suite}." + return 1 + } + + for test_case in $(python3 ${OET_PATH}/libs/locallibs/suite_case.py --suite $test_suite --key cases-name | shuf); do + run_test_case "$test_suite" "$test_case" + done +} + +function run_all_cases() { + test_suites=($(find ${OET_PATH}/suite2cases/ -type f -name "*.json" | awk -F '/' '{print $NF}' | sed -e 's/.json$//g')) + test ${#test_suites[@]} -eq 0 && { + LOG_ERROR "Can't find recording about test_suites." + return 1 + } + + for test_suite in ${test_suites[*]}; do + + run_test_suite "$test_suite" + done +} + +function statistic_result() { + + LOG_INFO "A total of ${CASE_NUM} use cases were executed, with ${SUCCESS_NUM} successes and ${FAIL_NUM} failures." + + [ ${FAIL_NUM} -ne 0 ] && exit 1 +} + +while getopts "c:af:r:dx" option; do + case $option in + c) + deploy_conf ${*//-c/} + ;; + d) + echo "$@" | grep -q -e '^ *-d *$' || { + usage + exit 1 + } + download_cases + ;; + a) + if echo "$@" | grep -q -e '-a *-x *$\|-x *-a *$\|-ax *$\|-xa *$'; then + COMMAND_X="yes" + elif ! echo "$@" | grep -q -e '-a *$'; then + usage + exit 1 + fi + + load_conf + run_all_cases + statistic_result + ;; + + f) + test_suite=$OPTARG + + echo $test_suite | grep -q -e '-a\| -r \|-x\|-d' && { + usage + exit 1 + } + + echo "$@" | grep -q -e ' *-x *\| *-xf *' && { + COMMAND_X="yes" + } + + echo "$@" | grep -q -e ' -r ' || { + load_conf + run_test_suite $test_suite + statistic_result + } + ;; + r) + test_case=$OPTARG + echo $test_case | grep -q -e '-a\|-f\|-x\|-d' && { + usage + exit 1 + } + + echo "$@" | grep -q -e ' *-x *\| *-xr *\| *-xf *' && { + COMMAND_X="yes" + } + + load_conf + run_test_case $test_suite $test_case + statistic_result + ;; + x) + echo "$@" | grep -q -e '^ *-x *$' && { + LOG_ERROR "The -x parameter must be used in combination with -a, -f, and -r." + exit 1 + } + ;; + *) + usage + exit 1 + ;; + + esac +done diff --git a/mugen/suite2cases/testsuite b/mugen/suite2cases/testsuite deleted file mode 100644 index 40833ccc..00000000 --- a/mugen/suite2cases/testsuite +++ /dev/null @@ -1,3 +0,0 @@ -oe_test_casename_01 -oe_test_casename_02 -oe_test_casename_03 \ No newline at end of file diff --git a/mugen/suite2cases/testsuite.json b/mugen/suite2cases/testsuite.json new file mode 100644 index 00000000..2efdbf4d --- /dev/null +++ b/mugen/suite2cases/testsuite.json @@ -0,0 +1,25 @@ +{ + "path": "$OET_PATH/testcases/testsuite", + "machine num": 1, + "machine type": "kvm", + "add network interface": 1, + "add disk": [ + 50 + ], + "cases": [ + { + "name": "oe_test_casename_01", + "machine num": 1, + "machine type": "kvm", + "add network interface": 1, + "add disk": [ + 50 + ] + }, + { + "name": "oe_test_casename_02", + "machine type": "physical", + "machine num": 1 + } + ] +} \ No newline at end of file diff --git a/mugen/testcases/testsuite/oe_test_casename_01/oe_test_casename_01.sh b/mugen/testcases/testsuite/oe_test_casename_01/oe_test_casename_01.sh index 806f7a13..cf79df24 100644 --- a/mugen/testcases/testsuite/oe_test_casename_01/oe_test_casename_01.sh +++ b/mugen/testcases/testsuite/oe_test_casename_01/oe_test_casename_01.sh @@ -1,6 +1,6 @@ #!/usr/bin/bash -# Copyright (c) 2020. Huawei Technologies Co.,Ltd.ALL rights reserved. +# Copyright (c) 2021. Huawei Technologies Co.,Ltd.ALL rights reserved. # This program is licensed under Mulan PSL v2. # You can use it according to the terms and conditions of the Mulan PSL v2. # http://license.coscl.org.cn/MulanPSL2 @@ -13,15 +13,13 @@ #@Contact : lemon.higgins@aliyun.com #@Date : 2020-04-09 09:39:43 #@License : Mulan PSL v2 -#@Version : 1.0 #@Desc : Take the test ls command as an example ##################################### source ${OET_PATH}/libs/locallibs/common_lib.sh # 需要预加载的数据、参数配置 -function config_params() -{ +function config_params() { LOG_INFO "Start to config params of the case." LOG_INFO "No params need to config." @@ -30,18 +28,16 @@ function config_params() } # 测试对象、测试需要的工具等安装准备 -function pre_test() -{ +function pre_test() { LOG_INFO "Start to prepare the test environment." - LOG_INFO "No pkgs need to install." + DNF_INSTALL "vim bc" LOG_INFO "End to prepare the test environment." } # 测试点的执行 -function run_test() -{ +function run_test() { LOG_INFO "Start to run test." # 测试命令:ls @@ -49,17 +45,16 @@ function run_test() CHECK_RESULT 0 # 测试/目录下是否存在proc|usr|roor|var|sys|etc|boot|dev目录 - CHECK_RESULT "$(ls / | grep -cE 'proc|usr|roor|var|sys|etc|boot|dev')" 7 + CHECK_RESULT "$(ls / | grep -cE 'proc|usr|roor|var|sys|etc|boot|dev')" 7 0 "The system is missing a base directory." LOG_INFO "End to run test." } # 后置处理,恢复测试环境 -function post_test() -{ +function post_test() { LOG_INFO "Start to restore the test environment." - LOG_INFO "Need't to restore the tet environment." + DNF_REMOVE LOG_INFO "End to restore the test environment." } diff --git a/mugen/testcases/testsuite/oe_test_casename_02/oe_test_casename_02.py b/mugen/testcases/testsuite/oe_test_casename_02/oe_test_casename_02.py index 597750d1..e4cf4564 100644 --- a/mugen/testcases/testsuite/oe_test_casename_02/oe_test_casename_02.py +++ b/mugen/testcases/testsuite/oe_test_casename_02/oe_test_casename_02.py @@ -1,6 +1,6 @@ #!/usr/bin/python3 -# Copyright (c) 2020. Huawei Technologies Co.,Ltd.ALL rights reserved. +# Copyright (c) 2021. Huawei Technologies Co.,Ltd.ALL rights reserved. # This program is licensed under Mulan PSL v2. # You can use it according to the terms and conditions of the Mulan PSL v2. # http://license.coscl.org.cn/MulanPSL2 @@ -13,22 +13,32 @@ # @Contact : lemon.higgins@aliyun.com # @Date : 2020-04-09 09:39:43 # @License : Mulan PSL v2 -# @Version : 1.0 # @Desc : Take the test ls command as an example ##################################### -import subprocess +import os, sys, subprocess + +LIBS_PATH = os.environ.get("OET_PATH") + "/libs/locallibs" +sys.path.append(LIBS_PATH) +import ssh_cmd ret = 0 cmd_status = subprocess.getstatusoutput("ls -CZl --all")[0] if cmd_status != 0: ret += 1 - -dir_num = subprocess.getoutput( - "ls / | grep -cE 'proc|usr|roor|var|sys|etc|boot|dev'") +dir_num = subprocess.getoutput("ls / | grep -cE 'proc|usr|roor|var|sys|etc|boot|dev'") if dir_num != "7": ret += 1 +conn = ssh_cmd.pssh_conn(os.environ.get("NODE2_IPV4"), os.environ.get("NODE2_PASSWORD")) +exitcode, output = ssh_cmd.pssh_cmd(conn, "ls") +ssh_cmd.pssh_close(conn) +if exitcode != 0: + ret += 1 +else: + if output != "test": + ret += 1 + +sys.exit(ret) -exit(ret) diff --git a/mugen/testcases/testsuite/oe_test_casename_03.sh b/mugen/testcases/testsuite/oe_test_casename_03.sh index 208447a7..983d1e82 100644 --- a/mugen/testcases/testsuite/oe_test_casename_03.sh +++ b/mugen/testcases/testsuite/oe_test_casename_03.sh @@ -1,6 +1,6 @@ #!/usr/bin/bash -# Copyright (c) 2020. Huawei Technologies Co.,Ltd.ALL rights reserved. +# Copyright (c) 2021. Huawei Technologies Co.,Ltd.ALL rights reserved. # This program is licensed under Mulan PSL v2. # You can use it according to the terms and conditions of the Mulan PSL v2. # http://license.coscl.org.cn/MulanPSL2 @@ -13,7 +13,6 @@ # @Contact : lemon.higgins@aliyun.com # @Date : 2020-11-19 09:39:43 # @License : Mulan PSL v2 -# @Version : 1.0 # @Desc : Take the test ls command as an example #################################### -- Gitee