From a9e086876e848006548dfe04293771a68c67ab8e Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 20:24:22 +0800 Subject: [PATCH 01/10] tests/hydropper: add cmdline and concurrency testcases add cmdline and concurrency testcases for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- .../functional/test_microvm_cmdline.py | 215 ++++++++++++++++++ .../functional/test_microvm_concurrency.py | 53 +++++ 2 files changed, 268 insertions(+) create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_cmdline.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_concurrency.py diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_cmdline.py b/tests/hydropper/testcases/microvm/functional/test_microvm_cmdline.py new file mode 100644 index 000000000..4df7b6193 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_cmdline.py @@ -0,0 +1,215 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm cmdline""" + +import os +import logging +import platform +from subprocess import run +from subprocess import PIPE +from subprocess import getstatusoutput +import pytest +import utils.exception +import utils.utils_common +from utils.config import CONFIG + +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + +def _get_corefilesize(vm_pid, dump_guestcore): + """ + Check corefile size + + Args: + dump_guestcore: enable the capability, when it is true + + Returns: + corefilesize + """ + (status, output) = getstatusoutput("coredumpctl -r info %s" % vm_pid) + if status == 0: + (status, output) = getstatusoutput("coredumpctl -r info %s | grep Storage | head -1" % vm_pid) + if "truncated" in output and not dump_guestcore: + logging.error("corefile is truncated, test failed!") + assert False + corefile = str(output).split()[1] + (status, output) = getstatusoutput("ls -s %s | awk '{print $1}'" % corefile) + assert status == 0 + else: + (status, output) = getstatusoutput("cat /proc/sys/kernel/core_pattern") + assert status == 0 + coredirectory = os.path.dirname(str(output)) + (status, output) = getstatusoutput("ls -s %s | awk '/-%s-/' | awk '{print $1}'" % (coredirectory, vm_pid)) + assert status == 0 + return output + +@pytest.mark.acceptance +def test_microvm_with_unsupported_param(): + """ + 1) Launch microvm with a unsupported param. + 2) Expect run with error code, but not panic. + """ + _cmd = "%s --unsupport" % CONFIG.stratovirt_microvm_bin + try: + _result = run(_cmd, shell=True, capture_output=True, check=False) + except TypeError: + _result = run(_cmd, shell=True, stderr=PIPE, stdout=PIPE, check=False) + assert 'panicked' not in str(_result.stderr, encoding='utf-8') + assert _result.returncode != 0 + + +@pytest.mark.acceptance +def test_microvm_start_with_initrd(test_microvm_with_initrd): + """ + Use -initrd to replace -drive for boot device: + + 1) Set vcpu_count to 4. + 2) Launch to test_vm by "-initrd". + 3) Assert vcpu_count is 4. + """ + test_vm = test_microvm_with_initrd + test_vm.basic_config(vcpu_count=4, vnetnums=0) + test_vm.launch() + rsp = test_vm.query_cpus() + assert len(rsp.get("return", [])) == 4 + rsp = test_vm.query_hotpluggable_cpus() + logging.debug(rsp) + test_vm.shutdown() + + +@pytest.mark.acceptance +def test_microvm_with_json(microvm): + """Test microvm start with json""" + test_vm = microvm + test_vm.basic_config(with_json=True) + test_vm.launch() + test_vm.query_cpus() + test_vm.shutdown() + + +@pytest.mark.acceptance +def test_microvm_with_pidfile(microvm): + """Test microvm start with pidfile""" + test_vm = microvm + test_vm.basic_config(withpid=True) + test_vm.launch() + assert test_vm.get_pid() == test_vm.get_pid_from_file() + test_vm.shutdown() + test_vm.launch() + assert test_vm.get_pid() == test_vm.get_pid_from_file() + + +@pytest.mark.acceptance +def test_microvm_without_daemonize(microvm): + """Test microvm without daemonize""" + test_vm = microvm + test_vm.basic_config(daemon=False, vcpu_count=4) + test_vm.launch() + rsp = test_vm.query_cpus() + assert len(rsp.get("return", [])) == 4 + test_vm.stop() + test_vm.event_wait(name='STOP') + test_vm.cont() + test_vm.event_wait(name='RESUME') + + +@pytest.mark.acceptance +def test_microvm_freeze(microvm): + """ + Test freeze a normal microvm's CPU at startup: + + 1) Set CPU freeze. + 2) Launch to test_vm but Login Timeout. + 3) Resume test_vm successfully. + """ + test_vm = microvm + test_vm.basic_config(freeze=True) + try: + test_vm.launch() + except utils.exception.LoginTimeoutError: + test_vm.qmp_reconnect() + test_vm.cont() + test_vm.event_wait(name='RESUME') + test_vm.launch() + ret, _ = test_vm.serial_cmd("ls") + assert ret == 0 + + + +@pytest.mark.acceptance +@pytest.mark.parametrize("dump_guestcore", [True, False]) +def test_microvm_with_dump_guestcore(microvm, dump_guestcore): + """ + Test microvm without memdump: + + 1) Set dump_guest_core configure up. + 2) Launch to test_vm. + 3) Get core file size as expect. + + Args: + dump_guestcore: enable the capability, when it is true + """ + test_vm = microvm + test_vm.basic_config(_machine="microvm", dump_guest_core=dump_guestcore) + test_vm.launch() + vm_pid = test_vm.pid + test_vm.destroy(signal=6) + output = _get_corefilesize(vm_pid, dump_guestcore) + logging.debug("coredump size is %s", output) + assert (int(output) < 102400 or dump_guestcore) + + +@pytest.mark.acceptance +@pytest.mark.parametrize("with_seccomp", [True, False]) +@pytest.mark.skipif( + platform.machine() != "x86_64", + reason="psyscall tools fail to run on aarch64." +) +def test_microvm_with_seccomp(microvm, with_seccomp): + """ + Test microvm with seccomp: + + 1) Set seccomp up. + 2) Launch to test_vm. + 3) Excute some system call that not in seccomp. + 4) seccomp will shutdown VM + + Args: + with_seccomp: secure computing mode + """ + test_vm = microvm + test_vm.basic_config(seccomp=with_seccomp) + test_vm.launch() + vm_pid = test_vm.pid + + # Get psyscall + try: + path = os.path.realpath(os.path.dirname(__file__)) + psyscall_path = "{}/{}".format(path, "psyscall") + run( + "git clone https://gitee.com/EulerRobot/psyscall.git %s" + % psyscall_path, + shell=True, + check=True + ) + run("cd %s && make" % psyscall_path, shell=True, check=True) + + # bad syscall + _cmd = "%s/psyscall %s dup2 3 4" % (psyscall_path, vm_pid) + logging.debug("execute command %s", _cmd) + status, output = getstatusoutput(_cmd) + logging.debug("bad syscall output: %s", output) + assert status == 0 + if with_seccomp: + test_vm.wait_pid_exit() + finally: + utils.utils_common.remove_existing_dir(psyscall_path) diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_concurrency.py b/tests/hydropper/testcases/microvm/functional/test_microvm_concurrency.py new file mode 100644 index 000000000..760794c1b --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_concurrency.py @@ -0,0 +1,53 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm concurrency""" + +import logging +import threading +import pytest +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + + +@pytest.mark.system +def test_microvm_concurrency(microvms): + """ + Test multi microvms start: + + 1) Set each VM vcpu_count = 4, then launch it and comfirm vcpu count is 4. + And increase _succ_sum. + 2) Execute step 1 concurrency by threads. + 3) Comfirm each VM is execute successffully by _succ_sum + + Note: You can modify CONCURRENT_QUANTITY tag in config/config.ini to set vm quantity. + """ + + def _check_vm_life(testvm): + test_vm = testvm + test_vm.basic_config(vcpu_count=4, vnetnums=0) + test_vm.launch() + rsp = test_vm.query_cpus() + assert len(rsp.get("return", [])) == 4 + rsp = test_vm.query_hotpluggable_cpus() + logging.debug("vm %s return: %s", test_vm.name, rsp) + test_vm.shutdown() + + test_ths = [] + for testvm in microvms: + vm_test_th = threading.Thread(target=_check_vm_life, args=(testvm,)) + test_ths.append(vm_test_th) + + for testthr in test_ths: + testthr.start() + + for testthr in test_ths: + testthr.join() -- Gitee From 6ed2d020c1fa053e659b00d74a03905b6352c685 Mon Sep 17 00:00:00 2001 From: Ren weijun Date: Fri, 26 Mar 2021 17:55:10 +0800 Subject: [PATCH 02/10] tests/hydropper: add some utils file for hydropper add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/utils/__init__.py | 0 tests/hydropper/utils/config.py | 159 +++++++++++++++++++++++++++ tests/hydropper/utils/decorators.py | 25 +++++ tests/hydropper/utils/exception.py | 133 ++++++++++++++++++++++ tests/hydropper/utils/resources.py | 165 ++++++++++++++++++++++++++++ 5 files changed, 482 insertions(+) create mode 100644 tests/hydropper/utils/__init__.py create mode 100644 tests/hydropper/utils/config.py create mode 100644 tests/hydropper/utils/decorators.py create mode 100644 tests/hydropper/utils/exception.py create mode 100644 tests/hydropper/utils/resources.py diff --git a/tests/hydropper/utils/__init__.py b/tests/hydropper/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/hydropper/utils/config.py b/tests/hydropper/utils/config.py new file mode 100644 index 000000000..d86c969a1 --- /dev/null +++ b/tests/hydropper/utils/config.py @@ -0,0 +1,159 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""global config parser""" + + +import os +import configparser +from queue import Queue +from utils.decorators import Singleton + +CONFIG_FILE = "../config/config.ini" + + +class ParserConfig(Singleton): + """Global settings class""" + + def __init__(self, cfg_file=CONFIG_FILE): + """ + Constructor + + Args: + cfg_file: set the global config file + """ + self.conf = configparser.ConfigParser() + _tempfile = os.path.join(os.path.dirname(__file__), cfg_file) + self.conf.read(_tempfile) + self.flush() + self.vmconfigs = dict() + self.init_vmconfig_files() + self.test_session_root_path = self.test_dir + self.event_queue = Queue() + + def get_option(self, section, option, default=None): + """ + Get item value + + Args: + section: set the section value + option: set the option value + default: set the default value + """ + try: + return str(self.conf.get(section, option)) + except configparser.NoSectionError: + return default + except configparser.NoOptionError: + return default + + def flush(self): + """Read config from self.conf file""" + # parser global env config + self.test_dir = self.get_option("env.params", "TEST_DIR", "/var/tmp/") + self.vmtype = self.get_option("env.params", "VMTYPE", "stratovirt") + self.vm_templ_dir = self.get_option("env.params", "VM_TEMPL_DIR", + "../config/test_config/vm_config") + self.vm_username = self.get_option("env.params", "VM_USERNAME", "root") + self.vm_password = self.get_option("env.params", "VM_PASSWORD", "openEuler12#$") + self.timeout_factor = int(self.get_option("env.params", "TIMEOUT_FACTOR", "1")) + self.delete_test_session = bool(self.get_option("env.params", "DELETE_TEST_SESSION", + "false") == "true") + self.concurrent_quantity = int(self.get_option("env.params", "CONCURRENT_QUANTITY", "10")) + + # parser stratovirt config + self.stratovirt_microvm_bin = self.get_option("stratovirt.params", "STRATOVIRT_MICROVM_BINARY", None) + self.stratovirt_microvm_boottime_bin = self.get_option("stratovirt.params", + "STRATOVIRT_MICROVM_BOOTTIME_BINARY", None) + self.stratovirt_microvm_config = self.get_option("stratovirt.params", "STRATOVIRT_MICROVM_CONFIG", + "config/test_config/vm_config/micro_vm.json") + self.stratovirt_binary_name = self.get_option("stratovirt.params", + "STRATOVIRT_BINARY_NAME", "microvm") + self.stratovirt_vmlinux = self.get_option("stratovirt.params", "STRATOVIRT_VMLINUX", None) + self.stratovirt_rootfs = self.get_option("stratovirt.params", "STRATOVIRT_ROOTFS", None) + self.stratovirt_initrd = self.get_option("stratovirt.params", "STRATOVIRT_INITRD", None) + self.stratovirt_use_config_file = bool(self.get_option("stratovirt.params", "STRATOVIRT_USE_CONFIG_FILE", + "false") == "true") + self.stratovirt_feature = self.get_option("stratovirt.params", "STRATOVIRT_FEATURE", "mmio") + self.memory_usage_check = bool(self.get_option("stratovirt.params", "MEMORY_USAGE_CHECK", + "true") == "true") + self.rust_san_check = bool(self.get_option("stratovirt.params", "RUST_SAN_CHECK", + "false") == "true") + + + # parser network params + self.bridge_name = self.get_option("network.params", "BRIDGE_NAME", "stratobr0") + self.nets_num = int(self.get_option("network.params", "NETS_NUMBER", "10")) + self.ip_prefix = self.get_option("network.params", "IP_PREFIX", "192.168") + self.ip_3rd = int(self.get_option("network.params", "IP_3RD", "133")) + self.dhcp_lower_limit = int(self.get_option("network.params", "DHCP_LOWER_LIMIT", "100")) + self.dhcp_top_limit = int(self.get_option("network.params", "DHCP_TOP_LIMIT", "240")) + self.static_ip_lower_limit = int(self.get_option("network.params", "STATIC_IP_LOWER_LIMIT", "10")) + self.static_ip_top_limit = int(self.get_option("network.params", "STATIC_IP_TOP_LIMIT", "100")) + self.netmasklen = self.get_option("network.params", "NETMASK_LEN", "24") + self.netmask = self.get_option("network.params", "NETMASK", "255.255.255.0") + + def init_vmconfig_files(self): + """ + Init vmconfig files(self.vmconfigs) as follow: + {"microvm": {"cpuhotplug": "microvm_cpuhotplug.json", + "seccomp": "microvm_seccomp.json"} + } + """ + for cfg_file in os.listdir(self.vm_templ_dir): + for vmtype in ["microvm", "standvm"]: + if cfg_file.startswith(vmtype): + if vmtype not in self.vmconfigs: + self.vmconfigs[vmtype] = dict() + tag = str(cfg_file).replace(vmtype + "_", "").replace(".json", "") + self.vmconfigs[vmtype][tag] = os.path.join(self.vm_templ_dir, cfg_file) + break + + def list_vmconfigs(self, vmtype="microvm"): + """ + Get list of vmconfig file + + Args: + vmtype: specify prefix of filename + """ + if vmtype in self.vmconfigs: + return self.vmconfigs[vmtype].values() + + return list() + + def _list_vmconfig_with_vmtype_tag(self, vmtype, tag): + if vmtype in self.vmconfigs: + return self.vmconfigs[vmtype].get(tag, None) + + return None + + def get_microvm_by_tag(self, tag): + """ + Get microvm config by tag + + Args: + tag: such as -boottime, -initrd. + """ + return self._list_vmconfig_with_vmtype_tag("microvm", tag) + + def list_microvm_tags(self): + """List microvm all tags""" + if "microvm" in self.vmconfigs: + return self.vmconfigs["microvm"].keys() + + return list() + + def get_default_microvm_vmconfig(self): + """Get default microvm vmconfig file""" + return self.stratovirt_microvm_config + + +CONFIG = ParserConfig() diff --git a/tests/hydropper/utils/decorators.py b/tests/hydropper/utils/decorators.py new file mode 100644 index 000000000..a30526ca4 --- /dev/null +++ b/tests/hydropper/utils/decorators.py @@ -0,0 +1,25 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""define common decorator""" + + +class Singleton(): + """Decorate to a singleton class""" + def __init__(self): + self.name = 'singleton' + + def __new__(cls, *args, **kwargs): + if not hasattr(cls, "_instance"): + orig = super(Singleton, cls) + cls._instance = orig.__new__(cls, *args, **kwargs) + + return cls._instance diff --git a/tests/hydropper/utils/exception.py b/tests/hydropper/utils/exception.py new file mode 100644 index 000000000..c4f9834bf --- /dev/null +++ b/tests/hydropper/utils/exception.py @@ -0,0 +1,133 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Exceptions""" + +class UnknownFeatureException(Exception): + """Exception Class for invalid build feature.""" + + def __init__(self): + """Just a constructor.""" + Exception.__init__( + self, + "Trying to get build binaries for unknown feature!" + ) + +# ssh error +class SSHError(Exception): + """SSH error exception""" + + def __init__(self, msg, output): + Exception.__init__(self, msg, output) + self.msg = msg + self.output = output + + def __str__(self): + return "->message: %s ->(output: %r)" % (self.msg, self.output) + + +class LoginAuthenticationError(SSHError): + """Login authentication error exception""" + + + +class LoginTimeoutError(SSHError): + """Login timeout error exception""" + + def __init__(self, output): + SSHError.__init__(self, "Login timeout expired", output) + + +class LoginProcessTerminatedError(SSHError): + """Login process terminated error exception""" + + def __init__(self, status, output): + SSHError.__init__(self, None, output) + self.status = status + + def __str__(self): + return ("Client process terminated (status: %s, output: %r)" % + (self.status, self.output)) + + +class LoginBadClientError(SSHError): + """Login bad client error exception""" + + def __init__(self, client): + SSHError.__init__(self, None, None) + self.client = client + + def __str__(self): + return "Unknown remote shell client: %r" % self.client + + +class SCPAuthenticationError(SSHError): + """SCP authentication error exception""" + + +class SCPAuthenticationTimeoutError(SCPAuthenticationError): + """SCP authentication timeout error exception""" + def __init__(self, output): + SCPAuthenticationError.__init__(self, "Authentication timeout expired", + output) + + +class SCPTransferTimeoutError(SSHError): + """SCP transfer timeout error exception""" + def __init__(self, output): + SSHError.__init__(self, "Transfer timeout expired", output) + + +class SCPTransferError(SSHError): + """SCP transfer failed exception""" + def __init__(self, status, output): + SSHError.__init__(self, None, output) + self.status = status + + def __str__(self): + return ("SCP transfer failed (status: %s, output: %r)" % + (self.status, self.output)) + +# console error +class ConsoleError(Exception): + """Console base exception""" + + +class NoConsoleError(ConsoleError): + """No Console Error""" + def __str__(self): + return "No console available" + + +class ConsoleBusyError(ConsoleError): + """Console Busy Error""" + def __str__(self): + return "Console is in use" + +# qmp error +class QMPError(Exception): + """QMP base exception""" + + +class QMPConnectError(QMPError): + """QMP connection exception""" + + +class QMPCapabilitiesError(QMPError): + """QMP negotiate capabilities exception""" + + +class QMPTimeoutError(QMPError): + """QMP timeout exception""" + + +class VMLifeError(Exception): + """Vmlife error exception""" diff --git a/tests/hydropper/utils/resources.py b/tests/hydropper/utils/resources.py new file mode 100644 index 000000000..b8e168030 --- /dev/null +++ b/tests/hydropper/utils/resources.py @@ -0,0 +1,165 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""global resources""" + +import threading +import random +from subprocess import run +from subprocess import CalledProcessError +from utils.config import CONFIG +from utils.utils_network import generate_random_name, generate_random_mac +from utils.decorators import Singleton + + +class NetworkResource(Singleton): + """Network resource""" + tap_cmd = "ip" + + def __init__(self, bridge=CONFIG.bridge_name, nets_num=CONFIG.nets_num, + ip_3rd=CONFIG.ip_3rd, ip_prefix=CONFIG.ip_prefix, + dhcp_lower_limit=CONFIG.dhcp_lower_limit, dhcp_top_limit=CONFIG.dhcp_top_limit, + static_ip_lower_limit=CONFIG.static_ip_lower_limit, netmasklen=CONFIG.netmasklen, + static_ip_top_limit=CONFIG.static_ip_top_limit, netmask=CONFIG.netmask): + self.bridge = bridge + self.ip_3rd = ip_3rd + self.ip_prefix = ip_prefix + self.dhcp_lower_limit = dhcp_lower_limit + self.dhcp_top_limit = dhcp_top_limit + self.ipaddr = "%s.%s.1" % (self.ip_prefix, str(self.ip_3rd)) + self.static_ip_range = list(range(static_ip_lower_limit, static_ip_top_limit)) + self.netmasklen = netmasklen + self.netmask = netmask + self.lock = threading.Lock() + self.ip_resources = dict() + self.nets_num = nets_num + + def check_env(self): + """Check dnsmasq process is running normal""" + # create bridge if it does not exist + run("brctl show %s || brctl addbr %s" % (self.bridge, self.bridge), shell=True, check=True) + + run("ifconfig %s up" % self.bridge, shell=True, check=True) + + for index in range(self.nets_num): + ipaddr = "%s.%s.1" % (self.ip_prefix, str(self.ip_3rd + index)) + run("ip addr add %s/%s dev %s" % (ipaddr, self.netmasklen, self.bridge), + shell=True, check=False) + + # create dnsmasq to alloc ipaddr if it's not running + _cmd = "ps -ef | grep dnsmasq | grep -w %s" % self.bridge + _result = run(_cmd, shell=True, check=True) + iprange_1 = "%s.%s.%s" % (self.ip_prefix, str(self.ip_3rd), str(self.dhcp_lower_limit)) + iprange_2 = "%s.%s.%s" % (self.ip_prefix, str(self.ip_3rd), str(self.dhcp_top_limit)) + if _result.returncode != 0: + _cmd = "dnsmasq --no-hosts --no-resolv --strict-order --bind-interfaces" \ + "--interface=%s --except-interface=lo --leasefile-ro " \ + "--dhcp-range=%s,%s" % (self.bridge, iprange_1, iprange_2) + _result = run(_cmd, shell=True, check=True) + return not bool(_result.returncode) + + return True + + def generator_tap(self, create_tap=True): + """ + Generator a tap device to vm, and link tap to bridge + + Returns: + {"name": tapname, "mac": mac} + """ + self.check_env() + tapname = generate_random_name() + + if create_tap: + try: + _cmd = "ip tuntap add %s mode tap && brctl addif %s %s && ip link set %s up" % \ + (tapname, self.bridge, tapname, tapname) + run(_cmd, shell=True, check=True) + except CalledProcessError: + _cmd = "tunctl -t %s && brctl addif %s %s && ip link set %s up" % \ + (tapname, self.bridge, tapname, tapname) + run(_cmd, shell=True, check=True) + NetworkResource.tap_cmd = "tunctl" + + mac = generate_random_mac() + return {"name": tapname, "mac": mac} + + def add_to_bridge(self, tapname): + """Add tap device to bridge""" + _cmd = "ip link show %s && brctl addif %s %s && ip link set %s up" % \ + (tapname, self.bridge, tapname, tapname) + run(_cmd, shell=True, check=True) + + def clean_tap(self, tapname): + """Clean tap device from host""" + if NetworkResource.tap_cmd == "tunctl": + _cmd = "ip link set %s down 2>/dev/null; brctl delif %s %s 2>/dev/null;" \ + "tunctl -d %s 2>/dev/null" % (tapname, self.bridge, tapname, tapname) + else: + _cmd = "ip link set %s down 2>/dev/null; brctl delif %s %s 2>/dev/null;" \ + "ip tuntap del %s mode tap 2>/dev/null" % \ + (tapname, self.bridge, tapname, tapname) + run(_cmd, shell=True, check=False) + with self.lock: + if tapname in self.ip_resources: + static_index = int(str(self.ip_resources[tapname]["ipaddr"]).split(".")[-1]) + self.static_ip_range.append(static_index) + del self.ip_resources[tapname] + + def alloc_ipaddr(self, tapname, index=0): + """ + Alloc an static ip address + + Returns: + {"ipaddr": xxx, "netmask": xxx, "netmasklen": xxx, "gateway": xxx} + """ + with self.lock: + if tapname in self.ip_resources: + return self.ip_resources[tapname] + + if not self.static_ip_range: + return None + static_index = random.choice(self.static_ip_range) + self.static_ip_range.remove(static_index) + _temp = {"ipaddr": "%s.%s.%s" % (self.ip_prefix, str(self.ip_3rd + index), str(static_index)), + "netmask": self.netmask, + "netmasklen": self.netmasklen, + "gateway": self.ipaddr} + self.ip_resources[tapname] = _temp + return self.ip_resources[tapname] + + +class VsockResource(Singleton): + """Vsock resource""" + def __init__(self): + self.lock = threading.Lock() + self.used_cids = set() + + @staticmethod + def init_vsock(): + """Init vsock""" + if run("lsmod | grep vhost_vsock", shell=True, check=False).returncode != 0: + if run("modprobe vhost_vsock", shell=True, check=False).returncode != 0: + return False + + return True + + @staticmethod + def find_contextid(): + """Find uniq context ID""" + first_cid = 3 + max_cid = 10000 + rand_cid = random.choice(range(first_cid, max_cid)) + return rand_cid + + +NETWORKS = NetworkResource() +VSOCKS = VsockResource() -- Gitee From 81c61b7808fdf0428003c18c079bc88cff5bc275 Mon Sep 17 00:00:00 2001 From: Ke Zhiming Date: Fri, 26 Mar 2021 17:56:35 +0800 Subject: [PATCH 03/10] tests/hydropper: add other some utils file for hydropper add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/utils/utils_common.py | 49 +++++ tests/hydropper/utils/utils_logging.py | 251 +++++++++++++++++++++++++ tests/hydropper/utils/utils_network.py | 118 ++++++++++++ tests/hydropper/utils/utils_qmp.py | 64 +++++++ 4 files changed, 482 insertions(+) create mode 100644 tests/hydropper/utils/utils_common.py create mode 100644 tests/hydropper/utils/utils_logging.py create mode 100644 tests/hydropper/utils/utils_network.py create mode 100644 tests/hydropper/utils/utils_qmp.py diff --git a/tests/hydropper/utils/utils_common.py b/tests/hydropper/utils/utils_common.py new file mode 100644 index 000000000..503358fe8 --- /dev/null +++ b/tests/hydropper/utils/utils_common.py @@ -0,0 +1,49 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Some common functions""" +import os +import errno +import ctypes +import shutil +from utils.utils_logging import TestLog + +LOG = TestLog.get_global_log() + +def stop_thread(thread): + """Raises the exception, performs cleanup if needed""" + tid = ctypes.c_long(thread.ident) + res = ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, ctypes.py_object(SystemExit)) + if res == 0: + raise ValueError("invalid thread id") + if res != 1: + # """if it returns a number greater than one, you're in trouble, + # and you should call it again with exc=NULL to revert the effect""" + ctypes.pythonapi.PyThreadState_SetAsyncExc(tid, None) + raise SystemError("PyThreadState_SetAsyncExc failed") + +def remove_existing_file(filepath): + """Remove file path if it exists""" + try: + os.remove(filepath) + except OSError as err: + if err.errno == errno.ENOENT: + return + raise + +def remove_existing_dir(dirpath): + """Remove dir path if it exists""" + try: + shutil.rmtree(dirpath) + except OSError as err: + if err.errno == errno.ENOENT: + return + raise diff --git a/tests/hydropper/utils/utils_logging.py b/tests/hydropper/utils/utils_logging.py new file mode 100644 index 000000000..6c60200ea --- /dev/null +++ b/tests/hydropper/utils/utils_logging.py @@ -0,0 +1,251 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""utils logging module""" + +import threading +import logging +import logging.handlers +from utils.config import CONFIG + +# Configuring Colors +RESET = '\033[1;0m' +RED = '\033[1;31m' +GREEN = '\033[1;32m' +YELLOW = '\033[1;33m' +BLUE = '\033[1;34m' + +# Defining Log Colors +COLORS_SETTING = { + 'DEBUG': BLUE + '%s' + RESET, + 'INFO': GREEN + '%s' + RESET, + 'WARNING': YELLOW + '%s' + RESET, + 'ERROR': RED + '%s' + RESET, + 'CRITICAL': RED + '%s' + RESET, + 'EXCEPTION': RED + '%s' + RESET, +} + + +class ColoredFormatter(logging.Formatter): + """Color print""" + + def __init__(self, fmt=None, datefmt=None): + logging.Formatter.__init__(self, fmt, datefmt) + + def format(self, record): + log_level = record.levelname + msg = logging.Formatter.format(self, record) + + return COLORS_SETTING.get(log_level, '%s') % msg + + +class Logger(): + """Test logging class""" + + def __init__(self, name=None, file_path=None, level=None, + fmt=None, mode=None, backup_count=None, + limit=None, when=None, console=True): + + self.logger = None + self.name = name + self.file_path = file_path + self.level = level + self.console = console + + if not fmt: + self.fmt = "[%(asctime)s][%(levelname)s]%(filename)s" \ + ":%(funcName)s L%(lineno)-.4d => %(message)s" + else: + self.fmt = fmt + + self.s_level = 'DEBUG' + self.f_level = 'DEBUG' + + if level: + level = level.split('|') + if len(level) == 1: + # Both set to the same level + self.s_level = self.f_level = level[0] + else: + # StreamHandler log level + self.s_level = level[0] + # FileHandler log level + self.f_level = level[1] + + self.msg = "Hello Word, just test" + + if not mode: + self.mode = r'a' + else: + self.mode = mode + if not backup_count: + self.backup_count = 5 + else: + self.backup_count = backup_count + + if not limit: + self.limit = 10 * 1024 * 1024 + else: + self.limit = limit + + self.when = when + + self.set_logger() + + def _add_handler(self, cls, level, colorful, **kwargs): + """Add handler""" + + if isinstance(level, str): + level = getattr(logging, level.upper(), logging.DEBUG) + + handler = cls(**kwargs) + handler.setLevel(level) + + if colorful: + formatter = ColoredFormatter(self.fmt) + else: + formatter = logging.Formatter(self.fmt) + + handler.setFormatter(formatter) + + # check whether the log handle of the avocado is inherited + # if not, add a handle + if not logging.getLogger("avocado.test").handlers: + self.logger.addHandler(handler) + + def _add_streamhandler(self): + """Add stream handler""" + self._add_handler(logging.StreamHandler, self.s_level, True) + + def _add_filehandler(self): + """Add file handler""" + + kwargs = {'filename': self.file_path} + + # choose the file handler based on the passed arguments + if self.backup_count == 0: + # use FileHandler + cls = logging.FileHandler + kwargs['mode'] = self.mode + elif self.when is None: + # use RotatingFileHandler + cls = logging.handlers.RotatingFileHandler + kwargs['maxBytes'] = self.limit + kwargs['backupCount'] = self.backup_count + kwargs['mode'] = self.mode + else: + # use TimedRotatingFileHandler + cls = logging.handlers.TimedRotatingFileHandler + kwargs['when'] = self.when + kwargs['interval'] = self.limit + kwargs['backupCount'] = self.backup_count + + self._add_handler(cls, self.f_level, True, **kwargs) + + def set_logger(self, name=None, file_path=None, level=None, + fmt=None, mode=None, backup_count=None, + limit=None, when=None, console=None): + """Set logger params""" + if level: + level = level.split('|') + if len(level) == 1: + # both set to the same level + self.s_level = self.f_level = level[0] + else: + # StreamHandler log level + self.s_level = level[0] + # FileHandler log level + self.f_level = level[1] + + if fmt: + self.fmt = fmt + + if name: + self.name = name + + if console is not None: + self.console = console + + self.logger = logging.getLogger(self.name) + self.logger.setLevel(logging.DEBUG) + + if self.console: + self._add_streamhandler() + + if file_path: + self.file_path = file_path + + if self.file_path: + if mode: + self.mode = mode + if backup_count: + self.backup_count = backup_count + if limit: + self.limit = limit + + self.when = when + self._add_filehandler() + + self.import_log_funcs() + + def import_log_funcs(self): + """Import logging func""" + log_funcs = ['debug', 'info', 'warn', 'error', 'critical', 'exception', 'warning'] + + for func_name in log_funcs: + func = getattr(self.logger, func_name) + setattr(self, func_name, func) + + +class TestLog(): + """Test log""" + def __init__(self): + self.name = 'Testlog' + + _logmaps = dict() + _logmap_lock = threading.Lock() + + @classmethod + def get_log_handle(cls, logkey, root_path=CONFIG.test_session_root_path): + """Get log handle with logkey""" + try: + cls._logmap_lock.acquire() + loghandle = cls._logmaps.get(logkey) + if loghandle is None: + logpath = root_path + "/" + logkey + ".log" + loghandle = Logger(logkey, file_path=logpath, console=False, backup_count=10) + cls._logmaps[logkey] = loghandle + return loghandle + finally: + cls._logmap_lock.release() + + @classmethod + def get_log_handle_bypath(cls, logpath): + """Get log handle by logpath""" + try: + cls._logmap_lock.acquire() + loghandle = cls._logmaps.get(logpath) + if loghandle is None: + loghandle = Logger(logpath, file_path=logpath, console=False, backup_count=10) + cls._logmaps[logpath] = loghandle + return loghandle + finally: + cls._logmap_lock.release() + + @classmethod + def get_global_log(cls): + """Get global log""" + return cls.get_log_handle("global") + + @classmethod + def get_monitor_log(cls): + """Get monitor log""" + return cls.get_log_handle("monitor") diff --git a/tests/hydropper/utils/utils_network.py b/tests/hydropper/utils/utils_network.py new file mode 100644 index 000000000..c56a27d6e --- /dev/null +++ b/tests/hydropper/utils/utils_network.py @@ -0,0 +1,118 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""utils common ops""" + +import random +import socket +from subprocess import run + +def generate_random_name(): + """Generate a random name for tap""" + _code_list = [] + for i in range(10): + _code_list.append(str(i)) + for i in range(97, 123): + _code_list.append(chr(i)) + + ret = 't' + _prefix = random.sample(_code_list, 7) + ret += ''.join(_prefix) + _postfix = random.sample(_code_list, 2) + ret += '-' + ret += ''.join(_postfix) + return ret + +def generate_random_mac(): + """Generate random mac address""" + try: + output = run("ip route | grep default", shell=True, capture_output=True, check=True).stdout + hostdev = str(output).split()[-1] + hostmac = run(r"cat /sys/devices/virtual/net/%s/address" % hostdev, + shell=True, capture_output=True, check=False).stdout.strip() + first_mac = hostmac.split(":")[0] + second_mac, third_mac, fourth_mac = hostmac.split(":")[1:3] + except (TypeError, KeyError, IndexError): + first_mac, second_mac, third_mac = "00", "00", "00" + fourth_mac = hex(random.randint(0x00, 0xff))[2:].zfill(2) + + fifth_mac = hex(random.randint(0x00, 0xff))[2:].zfill(2) + mac = [first_mac, second_mac, third_mac, fourth_mac, + fifth_mac, hex(random.randint(0x00, 0xff))[2:].zfill(2)] + + return ':'.join(mac) + +def is_port_free(port, address): + """ + Return True if the given port is available + Currently we only check for TCP/UDP connections on IPv4/6 + + Args: + port: Port number + address: Socket address to connect + """ + families = (socket.AF_INET, socket.AF_INET6) + protocols_type = (socket.SOCK_STREAM, socket.SOCK_DGRAM) + sock = None + localhost = True + + if address and address != "localhost": + localhost = False + # sock.connect always connects for UDP + protocols_type = (socket.SOCK_STREAM, ) + + try: + for family in families: + for protocol in protocols_type: + try: + sock = socket.socket(family, protocol) + sock.connect((address, port)) + return False + except socket.error as exc: + # Unsupported combinations + if exc.errno in (93, 94): + continue + if localhost: + return True + sock.close() + return True + finally: + if sock is not None: + sock.close() + +def get_free_port(port_start=1024, port_end=65535, count=1, address='localhost', randomport=False): + """ + Return a host free port or counts of host free ports in the range [port_start, port_end]. + + Args: + port_start: Header of candidate port range, defaults to 1024 + port_end: Ender of candidate port range, defaults to 65535 + count: The number of available ports to get + address: Socket address to connect + random: Find port random, in order if it's False + + Returns: + Int if count=1, port_list if count > 1, None if no free port found + """ + free_ports = [] + port_list = range(port_start, port_end) + if randomport: + randomport.shuffle(list(port_list)) + for _, port in enumerate(port_list): + if is_port_free(port, address): + free_ports.append(port) + if len(free_ports) >= count: + break + if free_ports: + if count == 1: + return free_ports[0] + return free_ports + return None diff --git a/tests/hydropper/utils/utils_qmp.py b/tests/hydropper/utils/utils_qmp.py new file mode 100644 index 000000000..8acefed25 --- /dev/null +++ b/tests/hydropper/utils/utils_qmp.py @@ -0,0 +1,64 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Some qmp fuctions""" + +import re +from utils.exception import QMPError + +def dictpath(dictionary, path): + """Traverse a path in a nested dict""" + index_re = re.compile(r'([^\[]+)\[([^\]]+)\]') + for component in path.split('/'): + match = index_re.match(component) + if match: + component, idx = match.groups() + idx = int(idx) + + if not isinstance(dictionary, dict) or component not in dictionary: + raise QMPError('failed path traversal for "%s" in "%s"' % (path, str(dictionary))) + dictionary = dictionary[component] + + if match: + if not isinstance(dictionary, list): + raise QMPError('path component "%s" in "%s" is not a list in "%s"' % + (component, path, str(dictionary))) + try: + dictionary = dictionary[idx] + except IndexError: + raise QMPError('invalid index "%s" in path "%s" in "%s"' % (idx, path, str(dictionary))) + return dictionary + +def assert_qmp_absent(dictionary, path): + """Assert that the path is invalid in 'dictionary'""" + try: + result = dictpath(dictionary, path) + except AssertionError: + return + raise QMPError('path "%s" has value "%s"' % (path, str(result))) + +def assert_qmp(dictionary, path, value): + """ + Assert that the value for a specific path in a QMP dict + matches. When given a list of values, assert that any of + them matches. + """ + result = dictpath(dictionary, path) + + # [] makes no sense as a list of valid values, so treat it as + # an actual single value. + if isinstance(value, list) and value != []: + for val in value: + if result == val: + return + raise QMPError('no match for "%s" in %s' % (str(result), str(value))) + + assert result == value -- Gitee From 0f9f516ad79a0409f8cb9564b53b8b335ca9d5ad Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:47:14 +0800 Subject: [PATCH 04/10] tests/hydropper: add virt directory add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/virt/__init__.py | 0 tests/hydropper/virt/basevm.py | 1082 ++++++++++++++++++++++++++++++ tests/hydropper/virt/microvm.py | 112 ++++ 3 files changed, 1194 insertions(+) create mode 100644 tests/hydropper/virt/__init__.py create mode 100644 tests/hydropper/virt/basevm.py create mode 100644 tests/hydropper/virt/microvm.py diff --git a/tests/hydropper/virt/__init__.py b/tests/hydropper/virt/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/hydropper/virt/basevm.py b/tests/hydropper/virt/basevm.py new file mode 100644 index 000000000..0abe08233 --- /dev/null +++ b/tests/hydropper/virt/basevm.py @@ -0,0 +1,1082 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""basic virtual machine class""" + +import os +import time +import subprocess +import json +import logging +import socket +import errno +import aexpect +from aexpect.exceptions import ExpectError +from retrying import retry +from utils.config import CONFIG +from utils import utils_common +from utils import utils_network +from utils import remote +from utils.utils_logging import TestLog +from utils.session import ConsoleManager +from utils.resources import NETWORKS +from utils.resources import VSOCKS +from utils.exception import VMLifeError +from utils.exception import QMPError +from utils.exception import QMPConnectError +from utils.exception import QMPCapabilitiesError +from utils.exception import QMPTimeoutError +from utils.exception import SSHError +from utils.exception import LoginTimeoutError + +LOG = TestLog.get_global_log() +LOGIN_TIMEOUT = 10 +LOGIN_WAIT_TIMEOUT = 60 * CONFIG.timeout_factor +SERIAL_TIMEOUT = 0.5 if CONFIG.timeout_factor > 1 else None + + +class BaseVM: + """Class to represent a extract base vm.""" + logger = TestLog.get_global_log() + + def __init__(self, root_path, name, uuid, bin_path, + wrapper=None, args=None, mon_sock=None, + vnetnums=1, vrngnums=0, vsocknums=0, balloon=False, + vmtype=CONFIG.vmtype, machine=None, freeze=False, + daemon=False, config=None, ipalloc="static", incoming=False, + error_test=False, dump_guest_core=True, mem_share=True): + if wrapper is None: + wrapper = [] + if args is None: + args = [] + self.__qmp = None + self.__qmp_set = True + # Copy args in case ew modify them. + self._args = list(args) + self._console_address = None + self._console_device_index = None + self._console_device_type = None + self._console_set = True + self._events = [] + self._launched = False + self._machine = machine + self._monitor_address = mon_sock + self._name = name + self._popen = None + self._remove_files = list() + self._vm_monitor = None + self.bin_path = bin_path + self.config_json = config + self.configdict = json.load(fp=open(self.config_json, "r")) + self.console_manager = ConsoleManager() + self.daemon = daemon + self.dump_guest_core = dump_guest_core + self.env = dict() + self.error_test = error_test + self.freeze = freeze + self.full_command = None + self.guest_ip = None + self.guest_ips = list() + self.incoming = incoming + self.init_args = args + self.interfaces = [] + self.ipalloc_type = ipalloc + self.logpath = '/var/log/stratovirt' + self.mem_share = mem_share + self.mon_sock = mon_sock + self.pid = None + self.pidfile = None + self.root_path = root_path + self._sock_dir = self.root_path + self.seccomp = True + self.serial_console = None + self.serial_log = TestLog.get_log_handle(self._name + "_serial") + self.serial_session = None + self.ssh_session = None + self.taps = list() + self.vhost_type = None + self.vmid = uuid + self.vmtype = vmtype + self.vnetnums = vnetnums + self.vrngnums = vrngnums + self.vsock_cid = list() + self.vsocknums = vsocknums + self.with_json = False + self.withmac = False + self.withpid = False + self.wrapper = wrapper + self.balloon = balloon + self.deflate_on_oom = False + + def __enter__(self): + return self + + def __del__(self): + self.kill() + + def get_pid(self): + """Get pid from ps""" + _cmd = "ps -ef | grep %s | grep %s | " \ + "awk '{print $2}' | head -1" % (self.bin_path, self.vmid) + output = subprocess.getoutput(_cmd) + LOG.debug("get output %s" % output.strip()) + return int(output.strip()) + + def get_pid_from_file(self): + """Get pid from file""" + if self.pidfile is not None: + with open(self.pidfile, 'r') as pidf: + return int(pidf.read()) + + return None + + def _pre_shutdown(self): + pass + + def shutdown(self, has_quit=False): + """Terminate the VM and clean up""" + if not self._launched: + return + + self._pre_shutdown() + if self.daemon or self.is_running(): + if self.__qmp: + try: + if not has_quit: + self.cmd('quit') + self.event_wait(name='SHUTDOWN', timeout=10, + match={'data': {'guest': False, 'reason': 'host-qmp-quit'}}) + # Kill popen no matter what exception occurs + # pylint: disable=broad-except + except Exception: + logging.error('match failed!') + self._popen.kill() + else: + self._popen.kill() + if not self.daemon: + self._popen.wait() + else: + self.wait_pid_exit() + self._post_shutdown() + self._launched = False + + def destroy(self, signal=9): + """Destroy the vm by send signal""" + if not self._launched: + return + + self._pre_shutdown() + subprocess.run("kill -%d %s" % (signal, self.pid), shell=True, check=True) + if not self.daemon: + self._popen.wait() + else: + self.wait_pid_exit() + self._post_shutdown() + self._launched = False + + def inshutdown(self): + """Terminate the vm from inner""" + if not self._launched: + return + + self._pre_shutdown() + if self.daemon or self.is_running(): + if self.serial_session: + try: + self.serial_session.run_func("cmd_output", "reboot") + self.event_wait(name='SHUTDOWN') + # pass no matter what exception occurs + # pylint: disable=broad-except + except Exception: + pass + else: + return + if not self.daemon: + self._popen.wait() + else: + self.wait_pid_exit() + self._post_shutdown() + self._launched = False + + def _post_shutdown(self): + """Post shutdown""" + exitcode = self.exitcode() + if exitcode is not None and exitcode < 0: + msg = 'received signal %i: %s' + if self.full_command: + command = ' '.join(self.full_command) + else: + command = '' + LOG.warning(msg, exitcode, command) + + if self.__qmp: + self.close_sock() + + if self.serial_session: + self.serial_session.run_func("close") + + if self.ssh_session: + self.ssh_session.close() + + for _file in self._remove_files: + utils_common.remove_existing_file(_file) + + if self.withpid: + subprocess.run("rm -rf %s" % self.pidfile, shell=True, check=True) + + def _pre_launch(self): + if self.__qmp_set: + if self._monitor_address is not None: + self._vm_monitor = self._monitor_address + if not isinstance(self._vm_monitor, tuple): + self._remove_files.append(self._vm_monitor) + else: + self._vm_monitor = os.path.join(self._sock_dir, + self._name + "_" + self.vmid + ".sock") + self._remove_files.append(self._vm_monitor) + + self.parser_config_to_args() + + def create_serial_control(self): + """Create serial control""" + self._wait_console_create() + self.serial_console = aexpect.ShellSession( + "/usr/bin/nc -U %s" % self._console_address, + auto_close=False, + output_func=self.serial_log.debug, + prompt=r"[\#\$]", + status_test_command="echo $?" + ) + self.console_manager.config_console(self.serial_console) + + def create_ssh_session(self): + """Create ssh session""" + user_known_hosts_file = '/dev/null' + port = 22 + _, output = self.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + LOG.debug("check ping result %s" % output) + ssh_session = aexpect.ShellSession( + "ssh %s -o UserKnownHostsFile=%s -o StrictHostKeyChecking=no -p %s" % ( + self.guest_ip, user_known_hosts_file, port + ), + auto_close=False, + output_func=self.serial_log.debug, + prompt=r"[\#\$]", + status_test_command="echo $?" + ) + + try: + self.console_manager.handle_session(ssh_session, + username=CONFIG.vm_username, + password=CONFIG.vm_password, + prompt=r"[\#\$]", timeout=60.0) + except Exception: + ssh_session.close() + raise Exception("handle_prompts ssh session failed!") + + return ssh_session + + def scp_file(self, local_file, dest_file): + """ + Send file to guest + + Args: + local_file: local file in host + dest_file: dest file in guest + """ + remote.scp_to_remote(self.guest_ip, 22, CONFIG.vm_username, + CONFIG.vm_password, local_file, dest_file, + output_func=self.serial_log.debug, timeout=60.0) + + def launch(self): + """Start a vm and establish a qmp connection""" + del self._args + self._args = list(self.init_args) + self._pre_launch() + self.full_command = (self.wrapper + [self.bin_path] + self._base_args() + self._args) + LOG.debug(self.full_command) + if not self.env.keys(): + self._popen = subprocess.Popen(self.full_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=False, + close_fds=True) + else: + _tempenv = os.environ.copy() + for key, _ in self.env.items(): + _tempenv[key] = self.env[key] + self._popen = subprocess.Popen(self.full_command, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + shell=False, + close_fds=True, + env=_tempenv) + + if self.daemon: + self._popen.wait() + self.pid = self.get_pid() + else: + self.pid = self._popen.pid + if not self.error_test: + self._post_launch() + + def post_launch_serial(self): + """Create a serial and wait for active""" + if self._console_set: + self.create_serial_control() + self._wait_for_active() + else: + time.sleep(2) + + def post_launch_qmp(self): + """Set a QMPMonitorProtocol""" + if isinstance(self.mon_sock, tuple): + self.qmp_monitor_protocol(self.mon_sock) + else: + self.qmp_monitor_protocol(self._vm_monitor) + if self.__qmp: + self.connect() + + def post_launch_vnet(self): + """Nothing is needed at present""" + pass + + def _post_launch(self): + self._launched = True + if self.incoming: + return + + self.post_launch_serial() + self.post_launch_qmp() + if self.vnetnums > 0: + self.post_launch_vnet() + self.config_network(self.ipalloc_type) + if self.ssh_session: + self.ssh_session.close() + self.ssh_session = self.create_ssh_session() + + @retry(wait_fixed=200, stop_max_attempt_number=50) + def _wait_console_create(self): + os.stat(self._console_address) + + @retry(wait_fixed=1000, stop_max_attempt_number=30) + def wait_pid_exit(self): + """Wait vm pid when vm exit""" + LOG.debug("===== check pid %s exit" % self.pid) + if os.path.exists("/proc/%d" % self.pid): + raise VMLifeError("check pid exit failed, vm shutdown/destroy failed!") + + def _wait_for_active(self): + """Wait vm for active""" + self.serial_session = self.wait_for_serial_login() + + def config_network(self, model='dhcp', index=0): + """Config vm network""" + self.interfaces = self.get_interfaces_inner() + if 'stratovirt' in self.vmtype: + self.serial_session.run_func("cmd_output", 'systemctl stop NetworkManager') + self.serial_session.run_func("cmd_output", 'systemctl stop firewalld') + # enable ssh login + _cmd = "sed -i \"s/^PermitRootLogin.*/PermitRootLogin yes/g\" /etc/ssh/sshd_config" + self.serial_cmd(_cmd) + self.serial_cmd("systemctl restart sshd") + if 'dhcp' in model: + self.serial_session.run_func("cmd_output", ("dhclient %s" % self.interfaces[index])) + _cmd = "ifconfig %s | awk '/inet/ {print $2}' | cut -f2 -d ':' | " \ + "awk 'NR==1 {print $1}'" % self.interfaces[index] + output = self.serial_session.run_func("cmd_output", _cmd) + self.guest_ips.append(output) + if index == 0: + self.guest_ip = output + elif 'static' in model: + _cmd = "ip addr show %s | grep inet | awk '{print $2}' | xargs -i -n1 ip addr del {} dev %s" % ( + self.interfaces[index], self.interfaces[index]) + self.serial_console.cmd_output(_cmd) + _cmd = "ip link set %s up" % self.interfaces[index] + self.serial_console.cmd_output(_cmd) + ipinfo = NETWORKS.alloc_ipaddr(self.taps[index]["name"], index=index) + _cmd = "ip addr add %s/%s dev %s" % (ipinfo["ipaddr"], + ipinfo["netmasklen"], self.interfaces[index]) + self.serial_console.cmd_output(_cmd) + _cmd = "ip route add default gw %s" % ipinfo["gateway"] + self.serial_console.cmd_output(_cmd) + self.guest_ips.append(ipinfo["ipaddr"]) + if index == 0: + self.guest_ip = ipinfo["ipaddr"] + LOG.debug("==== check ip addr info in Guest ======\n %s" % + self.serial_session.run_func("cmd_output", "ip addr")) + + def kill(self): + """Kill vm""" + try: + self.shutdown() + # destroy vm no matter what exception occurs + # pylint: disable=broad-except + except Exception as err: + LOG.warning("got exception %s, try to destroy vm" % err) + self.destroy() + + for tap in self.taps: + NETWORKS.clean_tap(tap["name"]) + + def _machine_args(self, args): + if self._machine == "microvm": + _dumpcore = "on" if self.dump_guest_core else "off" + _memshare = "on" if self.mem_share else "off" + args.extend(['-machine', '%s,dump-guest-core=%s,mem-share=%s' + % (self._machine, _dumpcore, _memshare)]) + else: + args.extend(['-machine', self._machine]) + + def _base_args(self): + args = [] + if self._name: + args.extend(['-name', self._name]) + # uuid is not supported yet, no need to add uuid + if self.logpath: + args.extend(['-D', self.logpath]) + if self.__qmp_set: + if "stratovirt" in self.vmtype: + if isinstance(self.mon_sock, tuple): + args.extend(['-api-channel', + "tcp:" + str(self.mon_sock[0]) + ":" + str(self.mon_sock[1])]) + else: + self.mon_sock += ",server,nowait" + args.extend(['-api-channel', "unix:" + self.mon_sock]) + else: + moncdev = 'socket,id=mon,path=%s' % self.mon_sock + args.extend(['-chardev', moncdev, '-mon', + 'chardev=mon,mode=control']) + + if self.withpid: + self.pidfile = os.path.join(self._sock_dir, self._name + "_" + "pid.file") + args.extend(['-pidfile', self.pidfile]) + if self._machine is not None: + self._machine_args(args) + + if self._console_set: + self._console_address = os.path.join(self._sock_dir, + self._name + "_" + self.vmid + "-console.sock") + # It doesn't need to create it first. + self._remove_files.append(self._console_address) + args.extend(['-chardev', 'id=console_0,path=%s' % self._console_address]) + + if self.vnetnums > 0: + for _ in range(self.vnetnums - len(self.taps)): + tapinfo = NETWORKS.generator_tap() + LOG.debug("current tapinfo is %s" % tapinfo) + self.taps.append(tapinfo) + + for tapinfo in self.taps: + tapname = tapinfo["name"] + _tempargs = "id=%s,netdev=%s" % (tapname, tapname) + if self.vhost_type: + _tempargs += ",vhost=on" + if self.withmac: + _tempargs += ",mac=%s" % tapinfo["mac"] + args.extend(['-netdev', _tempargs]) + + # rng is not supported yet. + if self.vrngnums > 0: + args.extend(['-object', 'rng-random,id=rng0,filename=/dev/urandom', + '-device', 'virtio-rng,rng=rng0,romfile=']) + + if self.vsocknums > 0: + if VSOCKS.init_vsock(): + for _ in range(self.vsocknums - len(self.vsock_cid)): + sockcid = VSOCKS.find_contextid() + self.vsock_cid.append(sockcid) + args.extend(['-device', + 'vsock,id=vsock-%s,' + 'guest-cid=%s' % (sockcid, sockcid)]) + + if self.balloon: + if self.deflate_on_oom: + ballooncfg = 'deflate-on-oom=true' + else: + ballooncfg = 'deflate-on-oom=false' + args.extend(['-balloon', ballooncfg]) + + if "stratovirt" in self.vmtype and not self.seccomp: + self._args.append('-disable-seccomp') + + if self.daemon: + self._args.append('-daemonize') + + if self.freeze: + self._args.extend(['-S']) + return args + + def is_running(self): + """Returns true if the VM is running.""" + return self._popen is not None and self._popen.poll() is None + + def exitcode(self): + """Returns the exit code if possible, or None.""" + if self._popen is None: + return None + return self._popen.poll() + + def add_drive(self, **kwargs): + """Add drive""" + drivetemp = dict() + drivetemp["drive_id"] = kwargs.get("drive_id", utils_network.generate_random_name()) + drivetemp["path_on_host"] = kwargs.get('path_on_host', None) + drivetemp["read_only"] = kwargs.get("read_only", "true") + if "drive" in self.configdict: + self.configdict["drive"].append(drivetemp) + else: + self.configdict["drive"] = [drivetemp] + + def basic_config(self, **kwargs): + """Change configdict""" + if "vcpu_count" in kwargs: + self.configdict["machine-config"]["vcpu_count"] = kwargs.get("vcpu_count") + del kwargs["vcpu_count"] + if "max_vcpus" in kwargs: + self.configdict["machine-config"]["max_vcpus"] = kwargs.get("max_vcpus") + del kwargs["max_vcpus"] + if "mem_size" in kwargs: + self.configdict["machine-config"]["mem_size"] = kwargs.get("mem_size") + del kwargs["mem_size"] + if "mem_path" in kwargs: + self.configdict["machine-config"]["mem_path"] = kwargs.get("mem_path") + del kwargs["mem_path"] + if "vhost_type" in kwargs: + self.vhost_type = kwargs.get("vhost_type") + del kwargs["vhost_type"] + + for key, value in kwargs.items(): + if hasattr(self, key): + setattr(self, key, value) + + def parser_config_to_args(self): + """Parser json config to args""" + if self.configdict is None: + return + + if self.with_json: + with open(self.config_json, "w") as fpdest: + json.dump(self.configdict, fpdest) + self.add_args('-config', self.config_json) + else: + configdict = self.configdict + if "boot-source" in configdict: + if "kernel_image_path" in configdict["boot-source"]: + self.add_args('-kernel', configdict["boot-source"]["kernel_image_path"]) + if "boot_args" in configdict["boot-source"]: + self.add_args('-append', configdict["boot-source"]["boot_args"]) + if "initrd" in configdict["boot-source"]: + self.add_args('-initrd', configdict["boot-source"]["initrd"]) + + if "machine-config" in configdict: + _temp_cpu_value = "" + if "vcpu_count" in configdict["machine-config"]: + _temp_cpu_value = str(configdict["machine-config"]["vcpu_count"]) + if "max_vcpus" in configdict["machine-config"]: + _temp_cpu_value += ",maxcpus=%s" % configdict["machine-config"]["max_vcpus"] + if _temp_cpu_value != "": + self.add_args('-smp', _temp_cpu_value) + _temp_mem_value = "" + if "mem_size" in configdict["machine-config"]: + _temp_mem_value = str(configdict["machine-config"]["mem_size"]) + if "mem_slots" in configdict["machine-config"] and \ + "max_mem" in configdict["machine-config"]: + _temp_mem_value += ",slots=%s,maxmem=%s" % \ + (configdict["machine-config"]["mem_slots"], + configdict["machine-config"]["max_mem"]) + if _temp_mem_value != "": + self.add_args('-m', _temp_mem_value) + if "mem_path" in configdict["machine-config"]: + self.add_args('-mem-path', configdict["machine-config"]["mem_path"]) + + for drive in configdict.get("drive", []): + _temp_drive_value = "" + if "drive_id" in drive: + _temp_drive_value = "id=%s" % drive["drive_id"] + if "path_on_host" in drive: + _temp_drive_value += ",file=%s" % drive["path_on_host"] + if "read_only" in drive: + _temp = "on" if drive["read_only"] else "off" + _temp_drive_value += ",readonly=%s" % _temp + if _temp_drive_value != "": + self.add_args('-drive', _temp_drive_value) + + def add_env(self, key, value): + """Add key, value to self.env""" + self.env[key] = value + + def add_args(self, *args): + """Adds to the list of extra arguments to cmdline""" + self._args.extend(args) + + def add_machine(self, machine_type): + """Add the machine type to cmdline""" + self._machine = machine_type + + def console_enable(self): + """Set console""" + self._console_set = True + + def console_disable(self): + """Unset console""" + self._console_set = False + + def set_device_type(self, device_type): + """Set device type""" + self._console_device_type = device_type + + def set_console_device_index(self, console_device_index): + """Set console device index""" + if not console_device_index: + console_device_index = 0 + self._console_device_index = console_device_index + + def serial_login(self, timeout=LOGIN_TIMEOUT, username=CONFIG.vm_username, + password=CONFIG.vm_password): + """Log into vm by virtio-console""" + prompt = r"[\#\$]\s*$" + status_test_command = "echo $?" + return self.console_manager.create_session(status_test_command, + prompt, username, password, + timeout) + + def wait_for_serial_login(self, timeout=LOGIN_WAIT_TIMEOUT, + internal_timeout=LOGIN_TIMEOUT, + username=CONFIG.vm_username, password=CONFIG.vm_password): + """ + Make multiple attempts to log into the guest via serial console. + + Args: + timeout: Time (seconds) to keep trying to log in. + internal_timeout: Timeout to pass to serial_login(). + + Returns: + ConsoleSession instance. + """ + LOG.debug("Attempting to log into '%s' via serial console " + "(timeout %ds)", self._name, timeout) + end_time = time.time() + timeout + while time.time() < end_time and (self.daemon or self.is_running()): + try: + session = self.serial_login(internal_timeout, + username, + password) + break + except SSHError: + time.sleep(0.5) + continue + else: + self.console_manager.close() + raise LoginTimeoutError('exceeded %s s timeout' % timeout) + return session + + def get_interfaces_inner(self): + """Get interfaces list from guest inner""" + cmd = "cat /proc/net/dev" + status, output = self.serial_cmd(cmd) + interfaces = [] + if status != 0: + return interfaces + for line in output.splitlines(): + temp = line.split(":") + if len(temp) != 2: + continue + if "lo" not in temp[0] and "virbr0" not in temp[0]: + interfaces.append(temp[0].strip()) + + interfaces.sort() + return interfaces + + def serial_cmd(self, cmd): + """ + Run a cmd in vm via serial console session + + Args: + cmd: cmd run in vm + + Returns: + A tuple (status, output) where status is the exit status + and output is the output of cmd + """ + LOG.debug("Attempting to run cmd '%s' in vm" % cmd) + return self.serial_session.run_func("cmd_status_output", cmd, internal_timeout=SERIAL_TIMEOUT) + + def get_guest_hwinfo(self): + """ + Get guest hwinfo via ssh_session + + Returns: + {"cpu": {"vcpu_count": xx, "maxvcpu": xx}, + "mem": {"memsize": xx, "maxmem": xx}, + "virtio": {"virtio_blk": [{"name": "virtio0"}], + "virtio_console": [{"name": "virtio1"}], + "virtio_net": [{"name": "virtio2"}], + "virtio_rng": [{"name": "virtio3"}], + } + } + """ + retdict = {"cpu": {}, "mem": {}, "virtio": {}} + if self.ssh_session is not None: + vcpu_count = int(self.ssh_session.cmd_output("grep -c processor /proc/cpuinfo")) + memsize = int(self.ssh_session.cmd_output("grep MemTotal /proc/meminfo | awk '{print $2}'")) + retdict["cpu"] = {"vcpu_count": vcpu_count, "maxvcpu": vcpu_count} + retdict["mem"] = {"memsize": memsize, "maxmem": memsize} + # ignore virtio_rng device now + for dev in ["virtio_blk", "virtio_net", "virtio_console"]: + devdir = "/sys/bus/virtio/drivers/%s" % dev + _cmd = "test -d %s && ls %s | grep virtio" % (devdir, devdir) + virtiodevs = self.ssh_session.cmd_output(_cmd).strip().split() + for virtiodev in virtiodevs: + _tempdev = {"name": virtiodev} + if dev not in retdict["virtio"]: + retdict["virtio"][dev] = list() + retdict["virtio"][dev].append(_tempdev) + + return retdict + + def get_lsblk_info(self): + """ + Get lsblk info + + Returns: + { + "vdx": {"size": xx, "readonly": xx}, + } + """ + retdict = {} + if self.ssh_session is not None: + _output = self.ssh_session.cmd_output("lsblk") + for line in _output.split("\n"): + temp = line.split() + if len(temp) == 6: + name = temp[0] + size = temp[3] + readonly = temp[4] + if name not in retdict: + retdict[name] = {"size": size, "readonly": readonly} + + return retdict + + def stop(self): + """Pause all vcpu""" + return self.qmp_command("stop") + + def cont(self): + """Resume paused vcpu""" + return self.qmp_command("cont") + + def device_add(self, **kwargs): + """Hotplug device""" + return self.qmp_command("device_add", **kwargs) + + def device_del(self, **kwargs): + """Unhotplug device""" + return self.qmp_command("device_del", **kwargs) + + def netdev_add(self, **kwargs): + """Hotplug a netdev""" + return self.qmp_command("netdev_add", **kwargs) + + def netdev_del(self, **kwargs): + """Unhotplug a netdev""" + return self.qmp_command("netdev_del", **kwargs) + + def add_disk(self, diskpath, index=1, check=True): + """Hotplug a disk to vm""" + LOG.debug("hotplug disk %s to vm" % diskpath) + devid = "drive-%d" % index + resp = self.qmp_command("blockdev-add", node_name="drive-%d" % index, + file={"driver": "file", "filename": diskpath}) + + LOG.debug("blockdev-add return %s" % resp) + if check: + assert "error" not in resp + resp = self.device_add(id=devid, driver="virtio-blk-mmio", addr=str(hex(index))) + LOG.debug("device_add return %s" % resp) + if check: + assert "error" not in resp + + return resp + + def del_disk(self, index=1, check=True): + """Unplug a disk""" + LOG.debug("unplug diskid %d to vm" % index) + devid = "drive-%d" % index + resp = self.device_del(id=devid) + if check: + assert "error" not in resp + + def add_net(self, check=True, config_addr=True): + """Hotplug a net device""" + tapinfo = NETWORKS.generator_tap() + LOG.debug("hotplug tapinfo is %s" % tapinfo) + self.taps.append(tapinfo) + tapname = tapinfo["name"] + resp = self.netdev_add(id=tapname, ifname=tapname) + if check: + assert "error" not in resp + LOG.debug("netdev_add return %s" % resp) + resp = self.device_add(id=tapname, driver="virtio-net-mmio", addr="0x1") + if check: + assert "error" not in resp + if config_addr: + self.config_network(index=1, model=self.ipalloc_type) + LOG.debug("device_add return %s" % resp) + return resp + + def del_net(self, check=True): + """Del net""" + tapinfo = self.taps[-1] + tapname = tapinfo["name"] + # clean ip addr in guest + + resp = self.device_del(id=tapname) + if check: + assert "error" not in resp + if "error" not in resp: + NETWORKS.clean_tap(tapinfo["name"]) + self.taps.pop() + if len(self.guest_ips) > 1: + self.guest_ips.pop() + LOG.debug("device_del return %s", resp) + + def query_hotpluggable_cpus(self): + """Query hotpluggable cpus""" + return self.qmp_command("query-hotpluggable-cpus") + + def query_cpus(self): + """Query cpus""" + return self.qmp_command("query-cpus") + + def query_status(self): + """Query status""" + return self.qmp_command("query-status") + + def query_balloon(self): + """Query balloon size""" + return self.qmp_command("query-balloon") + + def balloon_set(self, **kwargs): + """Set balloon size""" + return self.qmp_command("balloon", **kwargs) + + def qmp_monitor_protocol(self, address): + """Set QMPMonitorProtocol""" + self.__qmp = {'events': [], + 'address': address, + 'sock': socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + } + + def enable_qmp_set(self): + """ + Enable qmp monitor + set in preparation phase + """ + self.__qmp_set = True + + def disable_qmp_set(self): + """ + Disable qmp monitor + set in preparation phase + """ + self.__qmp = None + self.__qmp_set = False + + def __sock_recv(self, only_event=False): + """Get data from socket""" + recv = self.__qmp['sock'].recv(1024).decode('utf-8').split('\n') + if recv and not recv[-1]: + recv.pop() + resp = None + while recv: + resp = json.loads(recv.pop(0)) + if 'event' not in resp: + return resp + self.logger.debug("-> %s", resp) + self.__qmp['events'].append(resp) + if only_event: + return resp + return resp + + def get_events(self, wait=False, only_event=False): + """ + Get new events or event from socket. + Push them to __qmp['events'] + + Args: + wait (bool): block until an event is available. + wait (float): If wait is a float, treat it as a timeout value. + + Raises: + QMPTimeoutError: If a timeout float is provided and the timeout + period elapses. + QMPConnectError: If wait is True but no events could be retrieved + or if some other error occurred. + """ + + # Wait for new events, if needed. + # if wait is 0.0, this means "no wait" and is also implicitly false. + if not self.__qmp['events'] and wait: + if isinstance(wait, float): + self.__qmp['sock'].settimeout(wait) + try: + ret = self.__sock_recv(only_event=True) + except socket.timeout: + raise QMPTimeoutError("Timeout waiting for event") + except: + raise QMPConnectError("Error while receiving from socket") + if ret is None: + raise QMPConnectError("Error while receiving from socket") + self.__qmp['sock'].settimeout(None) + + if self.__qmp['events']: + if only_event: + return self.__qmp['events'].pop(0) + return self.__qmp['events'] + return None + + def connect(self): + """ + Connect to the QMP Monitor and perform capabilities negotiation. + + Returns: + QMP greeting if negotiate is true + None if negotiate is false + + Raises: + QMPConnectError if the greeting is not received or QMP not in greetiong + QMPCapabilitiesError if fails to negotiate capabilities + """ + self.__qmp['sock'].connect(self.__qmp['address']) + greeting = self.__sock_recv() + if greeting is None or "QMP" not in greeting: + raise QMPConnectError + # Greeting seems ok, negotiate capabilities + resp = self.cmd('qmp_capabilities') + if resp and "return" in resp: + return greeting + raise QMPCapabilitiesError + + def qmp_reconnect(self): + """Reconnect qmp when sock is dead""" + if self.__qmp: + self.close_sock() + + if isinstance(self.mon_sock, tuple): + self.qmp_monitor_protocol(self.mon_sock) + else: + self.qmp_monitor_protocol(self._vm_monitor) + if self.__qmp: + self.connect() + + def cmd(self, name, args=None, cmd_id=None): + """ + Build a QMP command and send it to the monitor. + + Args: + name: command name + args: command arguments + cmd_id: command id + """ + qmp_cmd = {'execute': name} + if args: + qmp_cmd.update({'arguments': args}) + if cmd_id: + qmp_cmd.update({'id': cmd_id}) + + self.logger.debug("<- %s", qmp_cmd) + try: + self.__qmp['sock'].sendall(json.dumps(qmp_cmd).encode('utf-8')) + except OSError as err: + if err.errno == errno.EPIPE: + return None + raise err + resp = self.__sock_recv() + self.logger.debug("-> %s", resp) + return resp + + def error_cmd(self, cmd, **kwds): + """Build and send a QMP command to the monitor, report errors if any""" + ret = self.cmd(cmd, kwds) + if "error" in ret: + raise Exception(ret['error']['desc']) + return ret['return'] + + def clear_events(self): + """Clear current list of pending events.""" + self.__qmp['events'] = [] + + def close_sock(self): + """Close the socket and socket file.""" + if self.__qmp['sock']: + self.__qmp['sock'].close() + + def settimeout(self, timeout): + """Set the socket timeout.""" + self.__qmp['sock'].settimeout(timeout) + + def is_af_unix(self): + """Check if the socket family is AF_UNIX.""" + return socket.AF_UNIX == self.__qmp['sock'].family + + def qmp_command(self, cmd, **args): + """Run qmp command""" + qmp_dict = dict() + for key, value in args.items(): + if key.find("_") != -1: + qmp_dict[key.replace('_', '-')] = value + else: + qmp_dict[key] = value + + rep = self.cmd(cmd, args=qmp_dict) + if rep is None: + raise QMPError("Monitor was closed") + + return rep + + def qmp_event_acquire(self, wait=False, return_list=False): + """ + Get qmp event or events. + + Args: + return_list: if return_list is True, then return qmp + events. Else, return a qmp event. + """ + if not return_list: + if not self._events: + return self.get_events(wait=wait, only_event=True) + return self._events.pop(0) + event_list = self.get_events(wait=wait) + event_list.extend(self._events) + self._events.clear() + self.clear_events() + return event_list + + def event_wait(self, name, timeout=60.0, match=None): + """ + Wait for an qmp event to match expection event. + + Args: + match: qmp match event, such as + {'data':{'guest':False,'reason':'host-qmp-quit'}} + """ + while True: + event = self.get_events(wait=timeout, only_event=True) + try: + if event['event'] == name: + for key in match: + if key in event and match[key] == event[key]: + return event + except TypeError: + if event['event'] == name: + return event + self._events.append(event) diff --git a/tests/hydropper/virt/microvm.py b/tests/hydropper/virt/microvm.py new file mode 100644 index 000000000..c74f6a591 --- /dev/null +++ b/tests/hydropper/virt/microvm.py @@ -0,0 +1,112 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""class microvm""" + +import os +import json +import logging +from virt.basevm import BaseVM +from utils.config import CONFIG +from monitor.mem_usage_info import MemoryUsageExceededInfo + +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename="/var/log/pytest.log", level=logging.DEBUG, format=LOG_FORMAT) + +class MicroVM(BaseVM): + """Class to represent a microvm""" + def __init__(self, root_path, name, uuid, bin_path=CONFIG.stratovirt_microvm_bin, + vmconfig=CONFIG.get_default_microvm_vmconfig(), + vmlinux=CONFIG.stratovirt_vmlinux, rootfs=CONFIG.stratovirt_rootfs, initrd=CONFIG.stratovirt_initrd, + vcpus=4, max_vcpus=8, memslots=0, maxmem=None, + memsize=2524971008, socktype="unix", loglevel="info"): + self.name = name + self.vmid = uuid + if "unix" in socktype: + sock_path = os.path.join(root_path, self.name + "_" + self.vmid + ".sock") + else: + sock_path = ("127.0.0.1", 32542) + self.vmconfig_template_file = vmconfig + self.vm_json_file = None + self.vmlinux = vmlinux + self.rootfs = rootfs + self.initrd = initrd + self.vcpus = vcpus + self.max_vcpus = max_vcpus + self.memslots = memslots + self.memsize = memsize + self.maxmem = maxmem + self.inited = False + self.init_vmjson(root_path) + self._args = list() + super(MicroVM, self).__init__(root_path=root_path, + name=name, + uuid=uuid, + args=self._args, + bin_path=bin_path, + mon_sock=sock_path, + daemon=True, + config=self.vm_json_file) + self.add_env("RUST_BACKTRACE", "1") + if CONFIG.rust_san_check: + self.add_env("RUSTFLAGS", "-Zsanitizer=address") + self.add_env("STRATOVIRT_LOG_LEVEL", loglevel) + if CONFIG.memory_usage_check: + self.memory_check = MemoryUsageExceededInfo(0) + self.memory_check.disable() + self.memory_check.start() + + def _post_launch(self): + super(MicroVM, self)._post_launch() + if CONFIG.memory_usage_check: + self.memory_check.update_pid(self.pid) + self.memory_check.enable() + + def _post_shutdown(self): + super(MicroVM, self)._post_shutdown() + if CONFIG.memory_usage_check: + self.memory_check.update_pid(0) + self.memory_check.disable() + + def kill(self): + if CONFIG.memory_usage_check: + self.memory_check.set_state("stop") + self.memory_check.join() + super(MicroVM, self).kill() + + def init_vmjson(self, root_path): + """Generate a temp vm json file""" + self.vm_json_file = os.path.join(root_path, self.name + "_" + self.vmid + ".json") + with open(self.vmconfig_template_file, "r") as cfp: + _vm_json = json.load(cfp) + if "boot-source" in _vm_json: + _vm_json["boot-source"]["kernel_image_path"] = self.vmlinux + if "initrd" in _vm_json["boot-source"]: + _vm_json["boot-source"]["initrd"] = self.initrd + if "drive" in _vm_json: + _vm_json["drive"][0]["path_on_host"] = self.rootfs + if "machine-config" in _vm_json: + _vm_json["machine-config"]["vcpu_count"] = int(self.vcpus) + _vm_json["machine-config"]["mem_size"] = self.memsize + if "max_vcpus" in _vm_json["machine-config"]: + _vm_json["machine-config"]["max_vcpus"] = int(self.max_vcpus) + if "mem_slots" in _vm_json["machine-config"]: + _vm_json["machine-config"]["mem_slots"] = int(self.memslots) + if self.maxmem is None and "max_mem" in _vm_json["machine-config"]: + _vm_json["machine-config"]["max_mem"] = self.memsize + + with open(self.vm_json_file, "w") as fpdest: + json.dump(_vm_json, fpdest) + self.inited = True + + def add_fake_pci_bridge_args(self): + """Add fake pcibridge config""" + self._args.extend(['-serial']) -- Gitee From 8ccc386cf1089b9e0fa404817be3e175b2f43769 Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:49:10 +0800 Subject: [PATCH 05/10] tests/hydropper: add README for hydropper add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/README.cn.md | 102 ++++++++++++++++++++++++++++++++++ tests/hydropper/README.md | 104 +++++++++++++++++++++++++++++++++++ 2 files changed, 206 insertions(+) create mode 100644 tests/hydropper/README.cn.md create mode 100644 tests/hydropper/README.md diff --git a/tests/hydropper/README.cn.md b/tests/hydropper/README.cn.md new file mode 100644 index 000000000..2dec6b912 --- /dev/null +++ b/tests/hydropper/README.cn.md @@ -0,0 +1,102 @@ +# Hydropper: +hydropper是一个基于pytest的轻量级测试框架,在其基础上封装了虚拟化的相关测试原子,用于stratovirt的黑盒测试。当前hydropper已经提供了一些测试用例,可以帮助开发人员发现和定位stratovirt的问题。 + +## 如何开始 + +### 获取软件 +你可以通过git clone的方式来获取hydropper: +```sh +$ git clone xxx +``` + +### 环境准备 +requirements.txt里面包含了python3依赖包。 + +- pytest>5.0.0 +- aexpect>1.5.0 +- retrying + +你可以通过下面的命令来安装这些包: +```sh +$ pip install -r config/requirements.txt +``` + +配置网卡你可能还需要安装: +```sh +$ yum install nmap +``` + +### 参数配置 +请在config目录下的config.ini里配置参数和对应路径,通常的用例都需要配置好kernel和rootfs: +```ini +[env.params] +... +VM_USERNAME = +VM_PASSWORD = +... +[stratovirt.params] +... +STRATOVIRT_VMLINUX = /path/to/kernel +STRATOVIRT_ROOTFS = /path/to/rootfs +... +``` + +请在config.ini中配置好IP_PREFIX和IP_3RD,这两项表示虚拟机IPv4地址的前24位, +最后8位会由hydropper来自动配置。请注意虚拟机需要和主机在同一网段。 +```ini +[network.params] +# such as 'IP_PREFIX.xxx.xxx' +IP_PREFIX = xxx.xxx +# such as 'xxx.xxx.IP_3RD.xxx' +IP_3RD = xxx +``` + +### 运行测试用例 +你可以hydropper目录下通过以下的命令来执行用例: +```sh +# 执行所有用例 +$ pytest + +# 执行所有带有关键字microvm的用例 +$ pytest -k microvm + +# 执行test_microvm_cmdline中的全部用例 +$ pytest testcases/microvm/functional/test_microvm_cmdline.py + +# 执行test_microvm_with_json用例 +$ pytest testcases/microvm/functional/test_microvm_cmdline.py::test_microvm_with_json +``` + +### 增加测试用例 +在testcases目录下的microvm目录里来增加自定义的用例。你可以新增一个python文件或者是在已存在的python文件里新增一个新的函数,文件名和函数名都必须形如test_*: +```python +test_microvm_xxx.py +def test_microvm_xxx() +``` + +我们已经预置了一些虚拟机对象,用户可以通过生成它们的实例来对虚拟机测试: +```python +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.launch() +``` + +另外,Fixture也可以帮助我们来更好的编写用例,用户可以参照以下方式来使用Fixture: +```python +# 标记该函数为system用例 +@pytest.mark.system +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.launch() +``` + +现在你可以使用pytest -m system来执行所有的“system”用例了。 + +用户可以使用basic_config()函数,来配置一些虚拟机的参数: +```python +# 设置虚拟机配置4个VCPU和4G内存 +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.basic_config(vcpu_count=4, mem_size='4G') + test_vm.launch() +``` \ No newline at end of file diff --git a/tests/hydropper/README.md b/tests/hydropper/README.md new file mode 100644 index 000000000..071585b01 --- /dev/null +++ b/tests/hydropper/README.md @@ -0,0 +1,104 @@ +# Hydropper: +Hydropper is a lightweight test framework based on pytest. It encapsulates virtualization-related test atoms and is used for stratovirt black-box tests.Hydropper has provided some test cases to help Developers find and locate Stratovirt problems. + +## How to start + +### Get hydropper +You can run the git clone command to get the hydroper: +```sh +$ git clone xxx +``` + +### Preparation +The requirements.txt file contains the Python3 dependency package. + +- pytest>5.0.0 +- aexpect>1.5.0 +- retrying + +You can install these packages by running the following commands: +```sh +$ pip install -r config/requirements.txt +``` + +To configure the NIC, you may also need to install: +```sh +$ yum install nmap +``` + +### Parameter configuration +Set parameters and corresponding paths in the config/config.ini. Generally, the kernel and rootfs must be configured for test cases. +```ini +[env.params] +... +VM_USERNAME = +VM_PASSWORD = +... +[stratovirt.params] +... +STRATOVIRT_VMLINUX = /path/to/kernel +STRATOVIRT_ROOTFS = /path/to/rootfs +... +``` + +Configure IP_PREFIX and IP_3RD in the "config.ini" file, +which indicate the first 24 bits of the VM IPv4 address, +The last 8 bits are automatically configured by the hydropper. +Note that the VM and the host must be in the same network segment. +```ini +[network.params] +# such as 'IP_PREFIX.xxx.xxx' +IP_PREFIX = xxx.xxx +# such as 'xxx.xxx.IP_3RD.xxx' +IP_3RD = xxx +``` + +### Run testcases +You can run the following commands in the hydroper directory to execute cases: +```sh +# Run all cases +$ pytest + +# Run all cases with the keyword microvm +$ pytest -k microvm + +# Run all cases in test_microvm_cmdline.py +$ pytest testcases/microvm/functional/test_microvm_cmdline.py + +# Run test_microvm_with_json +$ pytest testcases/microvm/functional/test_microvm_cmdline.py::test_microvm_with_json +``` + +### Add new testcases +Add customized cases to the microvm directory under testcases.You can add a python file or add a function to an existing python file.The file name and function name must be in the format of test_*. +```python +test_microvm_xxx.py +def test_microvm_xxx() +``` + +We have preset some virtual machine objects. You can test the virtual machine by generating their instances: +```python +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.launch() +``` + +In addition, Fixture is useful to write testcases.You can use Fixture in the following ways: +```python +# Mark the tag to system +@pytest.mark.system +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.launch() +``` + +Now you can use the pytest -m system command to run all the "system" cases. + +You can use the basic_config() function to configure VM parameters: +```python +# Configure four vCPUs and 4 GB memory for the VM. +def test_microvm_xxx(microvm): + test_vm = microvm + test_vm.basic_config(vcpu_count=4, mem_size='4G') + test_vm.launch() +``` \ No newline at end of file -- Gitee From 42eddaffdbd31cc5cb1ed26a06620efe73780b4d Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:50:41 +0800 Subject: [PATCH 06/10] tests/hydropper: add hydropper config directory add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/README.md | 2 +- tests/hydropper/config/config.ini | 72 +++++++++++++++++++ .../test_config/vm_config/micro_vm.json | 18 +++++ .../vm_config/microvm_boottime.json | 18 +++++ .../vm_config/microvm_cpuhotplug.json | 17 +++++ .../test_config/vm_config/microvm_initrd.json | 11 +++ .../microvm_katacontainer_vnetplug.json | 13 ++++ .../microvm_katacontainer_vnetunplug.json | 3 + .../vm_config/microvm_largeinitrd.json | 11 +++ .../vm_config/microvm_seccomp.json | 17 +++++ 10 files changed, 181 insertions(+), 1 deletion(-) create mode 100644 tests/hydropper/config/config.ini create mode 100644 tests/hydropper/config/test_config/vm_config/micro_vm.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_boottime.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_cpuhotplug.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_initrd.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetplug.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetunplug.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_largeinitrd.json create mode 100644 tests/hydropper/config/test_config/vm_config/microvm_seccomp.json diff --git a/tests/hydropper/README.md b/tests/hydropper/README.md index 071585b01..17fd8166e 100644 --- a/tests/hydropper/README.md +++ b/tests/hydropper/README.md @@ -83,7 +83,7 @@ def test_microvm_xxx(microvm): test_vm.launch() ``` -In addition, Fixture is useful to write testcases.You can use Fixture in the following ways: +In addition, Fixture is useful to write testcases.You can use Fixture in the following ways: ```python # Mark the tag to system @pytest.mark.system diff --git a/tests/hydropper/config/config.ini b/tests/hydropper/config/config.ini new file mode 100644 index 000000000..5b43fc016 --- /dev/null +++ b/tests/hydropper/config/config.ini @@ -0,0 +1,72 @@ +[env.params] +TEST_DIR = /var/tmp/ +# test vm type (stratovirt) +VMTYPE = stratovirt +# vm template dir +VM_TEMPL_DIR = config/test_config/vm_config +# vm username and password +VM_USERNAME = root +VM_PASSWORD = openEuler12#$ +# Timeout Base Unit +TIMEOUT_FACTOR = 1 +DELETE_TEST_SESSION = false +# vm concurrent quantity +CONCURRENT_QUANTITY = 10 + +[stratovirt.params] +# Default Configuration Parameters +# Stratovirt binary file path +STRATOVIRT_MICROVM_BINARY = /usr/bin/stratovirt + +# Configure common basic parameters in the JSON file. +STRATOVIRT_MICROVM_CONFIG = config/test_config/vm_config/micro_vm.json + +# User can use initrd or rootfs to start stratvirt vm. +# Hydropper use rootfs to start vm by default. +# Testcases use rootfs: +# def test_xxx(microvm): +# +# Testcases use initrd: +# def test_xxx(test_microvm_with_initrd): +# +STRATOVIRT_ROOTFS = /home/microvm_image/openEuler-21.03-stratovirt-x86_64.img + +# Kernel +STRATOVIRT_VMLINUX = /home/microvm_image/vmlinux.bin + +# User-defined parameters. +# Users can add parameters here. Then adapt configparser in config.py to use them. + +# Users can use initrd to replace rootfs to start vm up +STRATOVIRT_INITRD = /home/microvm_image/stratovirt-initrd.img + +STRATOVIRT_BINARY_NAME = 'microvm' +STRATOVIRT_USE_CONFIG_FILE = false + +# enable memory usage check +MEMORY_USAGE_CHECK = true +# use mmio or pci for io device +STRATOVIRT_FEATURE = 'mmio' +# enable rust san check +RUST_SAN_CHECK = false + +[network.params] +BRIDGE_NAME = strato_br0 +NETS_NUMBER = 10 +# such as 'IP_PREFIX.xxx.xxx' +IP_PREFIX = 200.200 + +# such as 'xxx.xxx.IP_3RD.xxx' +IP_3RD = 133 + +# Stratovirt vm dhcp range +DHCP_LOWER_LIMIT = 100 +DHCP_TOP_LIMIT = 240 + +# Stratovirt vm static ip range +STATIC_IP_LOWER_LIMIT = 10 +STATIC_IP_TOP_LIMIT = 100 + +# Netmask setting +NETMASK_LEN = 24 +NETMASK = 255.255.255.0 diff --git a/tests/hydropper/config/test_config/vm_config/micro_vm.json b/tests/hydropper/config/test_config/vm_config/micro_vm.json new file mode 100644 index 000000000..227045e94 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/micro_vm.json @@ -0,0 +1,18 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "boot_args": "rw console=hvc0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1 root=/dev/vda" + }, + "drive": [ + { + "drive_id": "rootfs", + "path_on_host": "${ROOTFS}", + "direct": true, + "read_only": false + } + ], + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_boottime.json b/tests/hydropper/config/test_config/vm_config/microvm_boottime.json new file mode 100644 index 000000000..ac47fc900 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_boottime.json @@ -0,0 +1,18 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "boot_args": "rw console=hvc0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1 root=/dev/vda quiet" + }, + "drive": [ + { + "drive_id": "rootfs", + "path_on_host": "${ROOTFS}", + "direct": false, + "read_only": false + } + ], + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_cpuhotplug.json b/tests/hydropper/config/test_config/vm_config/microvm_cpuhotplug.json new file mode 100644 index 000000000..e9c6400a0 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_cpuhotplug.json @@ -0,0 +1,17 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "boot_args": "console=ttyS0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1" + }, + "drives": [ + { + "drive_id": "rootfs", + "path_on_host": "${ROOTFS}", + "is_read_only": false + } + ], + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_initrd.json b/tests/hydropper/config/test_config/vm_config/microvm_initrd.json new file mode 100644 index 000000000..e9f9237e0 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_initrd.json @@ -0,0 +1,11 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "initrd": "${ROOTFS}", + "boot_args": "console=hvc0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1 root=/dev/ram" + }, + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetplug.json b/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetplug.json new file mode 100644 index 000000000..6a567bfa5 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetplug.json @@ -0,0 +1,13 @@ +{ + "device": "tap0", + "name": "eth0", + "linkType":"tap", + "IPAddresses": [ + { + "address": "172.16.0.7", + "mask": "24" + } + ], + "mtu": 1500, + "hwAddr":"02:42:20:6f:a3:69" +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetunplug.json b/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetunplug.json new file mode 100644 index 000000000..317875390 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_katacontainer_vnetunplug.json @@ -0,0 +1,3 @@ +{ + "name":"eth0" +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_largeinitrd.json b/tests/hydropper/config/test_config/vm_config/microvm_largeinitrd.json new file mode 100644 index 000000000..d413d9440 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_largeinitrd.json @@ -0,0 +1,11 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "initrd": "${ROOTFS}", + "boot_args": "console=hvc0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1 root=/dev/ram rdinit=/sbin/init" + }, + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} diff --git a/tests/hydropper/config/test_config/vm_config/microvm_seccomp.json b/tests/hydropper/config/test_config/vm_config/microvm_seccomp.json new file mode 100644 index 000000000..e9c6400a0 --- /dev/null +++ b/tests/hydropper/config/test_config/vm_config/microvm_seccomp.json @@ -0,0 +1,17 @@ +{ + "boot-source": { + "kernel_image_path": "${VMLINUX}", + "boot_args": "console=ttyS0 lastbus=0 reboot=k panic=1 tsc=reliable ipv6.disable=1" + }, + "drives": [ + { + "drive_id": "rootfs", + "path_on_host": "${ROOTFS}", + "is_read_only": false + } + ], + "machine-config": { + "vcpu_count": "${VCPU}", + "mem_size": "${MEMSIZE}" + } +} -- Gitee From b3bb3102eb978d86b49441e9551adff17ade5981 Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:51:54 +0800 Subject: [PATCH 07/10] tests/hydropper: add conftest and some test file add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/conftest.py | 183 +++++++++++++++++++++++++++++++ tests/hydropper/pytest.ini | 8 ++ tests/hydropper/requirements.txt | 3 + 3 files changed, 194 insertions(+) create mode 100644 tests/hydropper/conftest.py create mode 100644 tests/hydropper/pytest.ini create mode 100644 tests/hydropper/requirements.txt diff --git a/tests/hydropper/conftest.py b/tests/hydropper/conftest.py new file mode 100644 index 000000000..e6d417ee0 --- /dev/null +++ b/tests/hydropper/conftest.py @@ -0,0 +1,183 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""conftest""" + +import os +import uuid +import shutil +import tempfile +import time +from subprocess import run +import pytest +from virt.microvm import MicroVM +from monitor.monitor_thread import MonitorThread +from utils.config import CONFIG + +TIMESTAMP = time.strftime('%Y%m%d_%H%M%S', time.localtime(time.time())) +SESSION_PATH = os.path.join(CONFIG.test_dir, TIMESTAMP) +CONFIG.test_session_root_path = SESSION_PATH +if not os.path.exists(CONFIG.test_session_root_path): + os.makedirs(CONFIG.test_session_root_path) + +@pytest.fixture(autouse=True, scope='session') +def test_session_root_path(): + """Create a new test path in each session""" + created_test_session_root_path = False + + delete_test_session = CONFIG.delete_test_session + monitor_thread = MonitorThread() + monitor_thread.start() + if "stratovirt" in CONFIG.vmtype: + _cmd = "cp %s %s.bak" % (CONFIG.stratovirt_rootfs, CONFIG.stratovirt_rootfs) + run(_cmd, shell=True, check=True) + + yield CONFIG.test_session_root_path + + if "stratovirt" in CONFIG.vmtype: + _cmd = "cp %s.bak %s" % (CONFIG.stratovirt_rootfs, CONFIG.stratovirt_rootfs) + run(_cmd, shell=True, check=True) + monitor_thread.stop() + monitor_thread.join() + if delete_test_session and created_test_session_root_path: + shutil.rmtree(CONFIG.test_session_root_path) + + +@pytest.fixture +def test_session_tmp_path(test_session_root_path): + """Generate a temporary directory on Setup. Remove on teardown.""" + # pylint: disable=redefined-outer-name + # The pytest.fixture triggers a pylint rule. + + temp_path = tempfile.mkdtemp(prefix=test_session_root_path) + yield temp_path + shutil.rmtree(temp_path) + + +def init_microvm(root_path, bin_path=CONFIG.stratovirt_microvm_bin, **kwargs): + """Auxiliary to init a microvm""" + vm_uuid = str(uuid.uuid4()) + testvm = MicroVM(root_path, "microvm", vm_uuid, + bin_path=bin_path, + **kwargs + ) + + return testvm + + +def init_microvm_with_json(root_path, vm_config_json, vmtag): + """Init a microvm from a json file""" + vm_uuid = str(uuid.uuid4()) + vmname = "microvm" + "_" + vmtag + testvm = MicroVM(root_path, vmname, vm_uuid, bin_path=CONFIG.stratovirt_microvm_bin, + vmconfig=vm_config_json) + return testvm + + +def _gcc_compile(src_file, output_file): + """Build a source file with gcc.""" + compile_cmd = 'gcc {} -o {} -O3'.format( + src_file, + output_file + ) + run( + compile_cmd, + shell=True, + check=True + ) + + +@pytest.fixture() +def nc_vsock_path(test_session_root_path): + """Wget nc-vsock.c and build a nc-vsock app.""" + # pylint: disable=redefined-outer-name + # The pytest.fixture triggers a pylint rule. + path = os.path.realpath(os.path.dirname(__file__)) + nc_path = "{}/{}".format( + path, + "nc-vsock.c" + ) + if not os.path.exists(nc_path): + run( + "wget https://gitee.com/EulerRobot/nc-vsock/raw/master/nc-vsock.c -O %s" + % nc_path, + shell=True, + check=True + ) + nc_vsock_bin_path = os.path.join( + test_session_root_path, + 'nc-vsock' + ) + _gcc_compile( + 'nc-vsock.c', + nc_vsock_bin_path + ) + yield nc_vsock_bin_path + + +@pytest.fixture() +def microvm(test_session_root_path): + """Instantiate a microvm""" + # pylint: disable=redefined-outer-name + # The pytest.fixture triggers a pylint rule. + testvm = init_microvm(test_session_root_path) + yield testvm + testvm.kill() + + +@pytest.fixture() +def microvm_with_tcp(test_session_root_path): + """Init a microvm""" + # pylint: disable=redefined-outer-name + # The pytest.fixture triggers a pylint rule. + testvm = init_microvm(test_session_root_path, socktype='tcp') + yield testvm + testvm.kill() + + +@pytest.fixture() +def microvms(test_session_root_path): + """Init multi microvms""" + # pylint: disable=redefined-outer-name + # The pytest.fixture triggers a pylint rule. + micro_vms = [] + for index in range(CONFIG.concurrent_quantity): + tempvm = init_microvm_with_json(test_session_root_path, + CONFIG.get_microvm_by_tag('initrd'), + "initrd%d" % index) + micro_vms.append(tempvm) + + yield micro_vms + for tempvm in micro_vms: + tempvm.kill() + + +@pytest.fixture() +def directvm(request): + """Get vm fixture value""" + return request.getfixturevalue(request.param) + + +TEST_MICROVM_CAP_FIXTURE_TEMPLATE = ( + "@pytest.fixture()\n" + "def test_microvm_with_CAP(test_session_root_path):\n" + " microvm = init_microvm_with_json(test_session_root_path,\n" + " CONFIG.get_microvm_by_tag(\"CAP\"), \"CAP\")\n" + " yield microvm\n" + " microvm.kill()" +) + +for capability in CONFIG.list_microvm_tags(): + test_microvm_cap_fixture = ( + TEST_MICROVM_CAP_FIXTURE_TEMPLATE.replace('CAP', capability) + ) + + exec(test_microvm_cap_fixture) diff --git a/tests/hydropper/pytest.ini b/tests/hydropper/pytest.ini new file mode 100644 index 000000000..236e5f13c --- /dev/null +++ b/tests/hydropper/pytest.ini @@ -0,0 +1,8 @@ +[pytest] + +markers= + acceptance: acceptance test + system: system test + performance: performance test + +render_collapsed = False diff --git a/tests/hydropper/requirements.txt b/tests/hydropper/requirements.txt new file mode 100644 index 000000000..d7afd5abf --- /dev/null +++ b/tests/hydropper/requirements.txt @@ -0,0 +1,3 @@ +pytest>5.0.0 +aexpect>1.5.0 +retrying -- Gitee From cf2fb071332f316ac8500737d19a7ac88ce9b8f5 Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:52:59 +0800 Subject: [PATCH 08/10] tests/hydropper: add hydropper monitor directory add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/monitor/__init__.py | 20 +++++ tests/hydropper/monitor/mem_usage_info.py | 79 ++++++++++++++++++ tests/hydropper/monitor/monitor_info.py | 77 ++++++++++++++++++ tests/hydropper/monitor/monitor_thread.py | 98 +++++++++++++++++++++++ 4 files changed, 274 insertions(+) create mode 100644 tests/hydropper/monitor/__init__.py create mode 100644 tests/hydropper/monitor/mem_usage_info.py create mode 100644 tests/hydropper/monitor/monitor_info.py create mode 100644 tests/hydropper/monitor/monitor_thread.py diff --git a/tests/hydropper/monitor/__init__.py b/tests/hydropper/monitor/__init__.py new file mode 100644 index 000000000..4d3739306 --- /dev/null +++ b/tests/hydropper/monitor/__init__.py @@ -0,0 +1,20 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""define constants about monitor""" + +MONITOR_LEVEL_INFO = "info" +MONITOR_LEVEL_ERROR = "error" +MONITOR_LEVEL_FATAL = "fatal" + +# monitor type +MEMORY_USAGE_EXCEEDED = "memory_usage" +CPU_USAGE_EXCEEDED = "cpu_usage" diff --git a/tests/hydropper/monitor/mem_usage_info.py b/tests/hydropper/monitor/mem_usage_info.py new file mode 100644 index 000000000..73377034d --- /dev/null +++ b/tests/hydropper/monitor/mem_usage_info.py @@ -0,0 +1,79 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""monitor vm memory usage""" + +import logging +from subprocess import run +from subprocess import CalledProcessError +from subprocess import PIPE +from monitor import monitor_info +from monitor import MEMORY_USAGE_EXCEEDED + + +class MemoryUsageExceededInfo(monitor_info.MonitorInfo): + """Check vm memory usage""" + + def __init__(self, pid, max_memory=4096): + """Max memory default value is 4096Kib""" + _monitor_type = MEMORY_USAGE_EXCEEDED + _monitor_cycle = 10 + super(MemoryUsageExceededInfo, self).__init__(_monitor_type, + _monitor_cycle, + "vm") + self._pid = pid + self.max_memory = max_memory + # guest memory top limit is 131072(128M) + self.guest_memory_limit = 131072 + + def update_pid(self, pid): + """Update vm pid""" + self._pid = pid + + def check(self): + """ + Check memory usage exceeded or not(overwrite to the monitorinfo) + + Returns: + (bool, level, err_msg) + """ + exceeded = False + level = "info" + err_msg = "" + pmap_cmd = "pmap -xq {}".format(self._pid) + mem_total = 0 + try: + pmap_out = run(pmap_cmd, shell=True, check=True, + stdout=PIPE).stdout.decode('utf-8').split("\n") + except CalledProcessError: + return exceeded + for line in pmap_out: + tokens = line.split() + if not tokens: + break + try: + total_size = int(tokens[1]) + rss = int(tokens[2]) + except ValueError: + continue + if total_size > self.guest_memory_limit: + # this is the guest memory region + continue + mem_total += rss + + logging.debug("mem_total:%s", mem_total) + + if mem_total >= self.max_memory: + exceeded = True + level = "error" + err_msg = "memory usage is %s, it's greater than %s" % (mem_total, self.max_memory) + + return exceeded, level, err_msg diff --git a/tests/hydropper/monitor/monitor_info.py b/tests/hydropper/monitor/monitor_info.py new file mode 100644 index 000000000..d57063b1f --- /dev/null +++ b/tests/hydropper/monitor/monitor_info.py @@ -0,0 +1,77 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""common monitor info""" + +import threading +import time +from queue import Full +import monitor +from utils.utils_logging import TestLog +from utils.config import CONFIG + +LOG = TestLog.get_monitor_log() + + +class MonitorInfo(threading.Thread): + """Monitor info(basic class)""" + + def __init__(self, monitor_type, monitor_cycle, monitor_level, e_queue=CONFIG.event_queue): + self.monitor_type = monitor_type + self.monitor_cycle = monitor_cycle + self.monitor_level = monitor_level + self._state = 'init' + self.state_lock = threading.Lock() + self._enable = False + self.e_queue = e_queue + super(MonitorInfo, self).__init__() + + def enable(self): + """Enable monitor item""" + self._enable = True + + def disable(self): + """Disable monitor item""" + self._enable = False + + def set_state(self, state): + """Set state atomic""" + with self.state_lock: + self._state = state + + def run(self): + """Run monitor""" + self.set_state('running') + while self._state != 'stop': + if self._enable: + (ret, level, err) = self.check() + if ret: + self.enqueue(level, err) + time.sleep(self.monitor_cycle) + + def check(self): + """ + Check it's normal or not + + Returns: + (bool, level, err_msg) + """ + return False, monitor.MONITOR_LEVEL_INFO, "not implement" + + def enqueue(self, level, err): + """Put event into queue""" + _item = {"type": self.monitor_type, + "level": level, + "errmsg": err} + try: + self.e_queue.put(_item, False) + except Full: + LOG.debug("insert alarm(%s) to queue failed!" % _item) diff --git a/tests/hydropper/monitor/monitor_thread.py b/tests/hydropper/monitor/monitor_thread.py new file mode 100644 index 000000000..0f4388561 --- /dev/null +++ b/tests/hydropper/monitor/monitor_thread.py @@ -0,0 +1,98 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""monitor thread""" + +import threading +from queue import Empty +from utils.utils_logging import TestLog +from utils.config import CONFIG + +LOG = TestLog.get_monitor_log() + + +class MonitorThread(threading.Thread): + """Monitor thread""" + + items = dict() + + def __init__(self): + """Construtor""" + super(MonitorThread, self).__init__() + self.state_lock = threading.Lock() + self._state = 'init' + + def set_state(self, state): + """Set state atomic""" + with self.state_lock: + self._state = state + + def stop(self): + """Stop monitor""" + self.set_state('stop') + + @classmethod + def add_item(cls, monitoritem): + """ + Add item to cls.items, and start this monitor + + Args: + monitoritem: the monitor item class name + + Returns: + True/False + """ + cls.items[monitoritem.monitor_type] = monitoritem + monitoritem.run() + return True + + @classmethod + def del_item(cls, monitoritem): + """ + Del item from cls.items, and stop this monitor + + Args: + monitoritem: the monitor item class name + + Returns: + True/False + """ + timeout = 300 + monitoritem.stop() + monitoritem.join(timeout=timeout) + if monitoritem.monitor_type in cls.items: + del cls.items[monitoritem.monitor_type] + if monitoritem.isAlive(): + LOG.debug("stop monitor thread [%s] failed within %d seconds" % \ + (monitoritem.monitor_type, timeout)) + return False + LOG.debug("stop monitor thread [%s] sucessfully" % monitoritem.monitor_type) + return True + + def run(self): + self.set_state('running') + while self._state != 'stop': + try: + alarm_info = CONFIG.event_queue.get(block=False, timeout=1) + self.event_handler(alarm_info) + except Empty: + pass + + @classmethod + def event_handler(cls, alarm_info): + """Event handler to process the alarm/monitor info""" + monitor_type = alarm_info["type"] + if monitor_type in cls.items: + if hasattr(cls.items[monitor_type], "event_handler"): + handler = getattr(cls.items[monitor_type], "event_handler") + handler(alarm_info) + elif "fatal" in alarm_info["level"] or "error" in alarm_info["level"]: + LOG.error("get error alarm %s" % alarm_info) -- Gitee From 2b7eae4b769a0fbf963a5d98ff2d901f56b3e56e Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 17:57:26 +0800 Subject: [PATCH 09/10] tests/hydropper: add session and remote file for hydropper add Hydropper test code for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- tests/hydropper/utils/remote.py | 91 ++++++++++++++ tests/hydropper/utils/session.py | 200 +++++++++++++++++++++++++++++++ 2 files changed, 291 insertions(+) create mode 100644 tests/hydropper/utils/remote.py create mode 100644 tests/hydropper/utils/session.py diff --git a/tests/hydropper/utils/remote.py b/tests/hydropper/utils/remote.py new file mode 100644 index 000000000..e62242447 --- /dev/null +++ b/tests/hydropper/utils/remote.py @@ -0,0 +1,91 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +""" +Functions and classes used for logging into guests and transferring files. +""" +from __future__ import division +import logging +import pipes +import aexpect +from utils.exception import SSHError +from utils.exception import SCPTransferTimeoutError +from utils.exception import SCPTransferError +from utils.exception import SCPAuthenticationTimeoutError + +def _scp_operation(session, password, transfer_timeout=600, login_timeout=300): + """ + Get questions from console, and provide answer(such as password). + + Args: + session: An Expect instance from aexpect + """ + timeout = login_timeout + authentication = False + + while True: + try: + index, text = session.read_until_last_line_matches( + [r"yes/no/", r"[Pp]assword:\s*$", r"lost connection"], + timeout=timeout, internal_timeout=0.5) + # yes/no + if index == 0: + session.sendline("yes") + continue + # "password:" + if index == 1: + logging.debug("Got password prompt, sending '%s'", password) + session.sendline(password) + timeout = transfer_timeout + authentication = True + continue + # "lost connection" + if index == 2: + raise SSHError("SCP client said 'lost connection'", text) + except aexpect.ExpectTimeoutError as err: + if authentication: + raise SCPTransferTimeoutError(err.output) + raise SCPAuthenticationTimeoutError(err.output) + except aexpect.ExpectProcessTerminatedError as err: + if err.status == 0: + logging.debug("SCP process terminated with status 0") + break + raise SCPTransferError(err.status, err.output) + +def scp_to_remote(host, port, username, password, local_path, remote_path, + limit="", output_func=None, timeout=600): + """ + Copy files to a remote host (guest) through scp. + + Args: + limit: Speed limit of file transfer, it means bandwidth. + """ + transfer_timeout = timeout + login_timeout = 60 + if limit != "": + limit = "-l %s" % (limit) + + command = "scp" + command += (" -r " + "-v -o UserKnownHostsFile=/dev/null " + "-o StrictHostKeyChecking=no " + "-o PreferredAuthentications=password %s " + r"-P %s %s %s@\[%s\]:%s" % + (limit, port, pipes.quote(local_path), username, host, pipes.quote(remote_path))) + logging.debug("Trying to SCP with command '%s', timeout %ss", command, transfer_timeout) + output_params = () + session = aexpect.Expect(command, + output_func=output_func, + output_params=output_params) + try: + _scp_operation(session, password, transfer_timeout, login_timeout) + finally: + session.close() diff --git a/tests/hydropper/utils/session.py b/tests/hydropper/utils/session.py new file mode 100644 index 000000000..1eb3badc7 --- /dev/null +++ b/tests/hydropper/utils/session.py @@ -0,0 +1,200 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Create session""" + +import threading +import time +import aexpect +from utils.utils_logging import TestLog +from utils.exception import ConsoleBusyError +from utils.exception import NoConsoleError +from utils.exception import LoginAuthenticationError +from utils.exception import LoginTimeoutError +from utils.exception import LoginProcessTerminatedError + +LOG = TestLog.get_global_log() + +def lock(function): + """ + Get the ConsoleManager lock, run the function, then release the lock. + + Args: + function: Function to package. + """ + def package(*args, **kwargs): + console_manager = args[0] + if console_manager.console_lock.acquire_lock(False) is False: + raise ConsoleBusyError + try: + return function(*args, **kwargs) + finally: + console_manager.console_lock.release_lock() + return package + + +class ConsoleManager(): + """A class for console session communication pipeline.""" + + def __init__(self): + self._console = None + self.status_test_command = None + self.console_lock = threading.Lock() + + @lock + def login_session(self, status_test_command, prompt, username, password, timeout): + """Login session by handle_session()""" + self._console.set_status_test_command(status_test_command) + self.handle_session(self._console, username, password, prompt, timeout, True) + + def create_session(self, status_test_command, + prompt, username, password, timeout): + """Return a console session with itself as the manager.""" + if self._console is None: + raise NoConsoleError + self.login_session(status_test_command, prompt, username, password, timeout) + return ConsoleSession(self) + + def config_console(self, console): + """Configure console""" + self._console = console + self.status_test_command = self._console.status_test_command + + def close(self): + """Close console""" + self._console.close() + + @lock + def get_func(self, func, *args, **kwargs): + """ + Get the func provided by a Console. + + Args: + func: function name + """ + _func = getattr(self._console, func) + return _func(*args, **kwargs) + + @staticmethod + def handle_session(session, username, password, prompt, timeout=10, + debug=False): + """ + Connect to a remote host (guest) using SSH or Telnet or else. + Provide answers to each questions. + """ + password_prompt_count = 0 + login_prompt_count = 0 + last_chance = False + last_line = [r"[Aa]re you sure", # continue connect + r"[Pp]assword:\s*", # password: + r"(? 0: + raise LoginAuthenticationError("Got username prompt twice", text) + raise LoginAuthenticationError("Got username prompt after password prompt", text) + if match == 5: + if debug: + LOG.debug("Got shell prompt, logged successfully") + break + if match == 6: + if debug: + LOG.debug("Got 'Warning added RSA to known host list") + continue + except aexpect.ExpectTimeoutError as err: + # send a empty line to avoid unexpect login timeout + # because some message from linux kernel maybe impact match + if not last_chance: + time.sleep(0.5) + session.sendline() + last_chance = True + continue + raise LoginTimeoutError(err.output) + except aexpect.ExpectProcessTerminatedError as err: + raise LoginProcessTerminatedError(err.status, err.output) + + return output + + +class ConsoleSession(): + """ + The wrapper of ShellSession from aexpect. + """ + + def __init__(self, manager): + self.__closed = False + self.__manager = manager + self.status_test_command = manager.status_test_command + + def __repr__(self): + return "console session id <%s>" % id(self) + + def run_func(self, name, *args, **kwargs): + """ + Execute console session function + + Args: + name: function name. available name: is_responsive cmd_output cmd_output_safe + cmd_status_output cmd_status cmd close send sendline sendcontrol send_ctrl set_linesep + read_nonblocking read_until_output_matches read_until_last_line_matches + read_until_any_line_matches read_up_to_prompt + """ + + if name == "close": + if self.__closed: + raise RuntimeError("%s is closed." % self) + self.__manager.close() + self.__closed = True + else: + return self.__manager.get_func(name, *args, **kwargs) + return None -- Gitee From eb751518c2b37acecd60c40b37fd977a0cc8c71b Mon Sep 17 00:00:00 2001 From: Zhu Huankai Date: Fri, 26 Mar 2021 20:16:29 +0800 Subject: [PATCH 10/10] tests/hydropper: add basic function testcases for StratoVirt add some basic function testcases for StratoVirt Signed-off-by: Zhu Huankai Signed-off-by: Chen Qun Signed-off-by: Pan Nengyuan Signed-off-by: Wang Shengfang Signed-off-by: Han Kai Signed-off-by: Wei Gao Signed-off-by: Guo Xinle Signed-off-by: Ming Yang Signed-off-by: Xiaohe Yang Signed-off-by: Ke Zhiming Signed-off-by: Gan Qixin Signed-off-by: Ren Weijun --- .../microvm/functional/test_microvm_api.py | 34 +++ .../functional/test_microvm_cpu_features.py | 172 +++++++++++++++ .../microvm/functional/test_microvm_timer.py | 33 +++ .../functional/test_microvm_vhost_vsock.py | 124 +++++++++++ .../functional/test_microvm_virtio_blk.py | 157 ++++++++++++++ .../functional/test_microvm_virtio_net.py | 205 ++++++++++++++++++ .../microvm/functional/test_microvm_vmlife.py | 119 ++++++++++ 7 files changed, 844 insertions(+) create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_api.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_cpu_features.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_timer.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_vhost_vsock.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_virtio_blk.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_virtio_net.py create mode 100644 tests/hydropper/testcases/microvm/functional/test_microvm_vmlife.py diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_api.py b/tests/hydropper/testcases/microvm/functional/test_microvm_api.py new file mode 100644 index 000000000..01fcd0404 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_api.py @@ -0,0 +1,34 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm api""" + +import logging +import pytest +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + + +@pytest.mark.acceptance +def test_api_lifecycle(microvm): + """ + Test a normal microvm start: + + 1) Set vcpu_count to 4. + 2) Launch to test_vm. + 3) Assert vcpu_count is 4. + """ + test_vm = microvm + test_vm.basic_config(vcpu_count=4) + test_vm.launch() + rsp = test_vm.query_cpus() + assert len(rsp.get("return", [])) == 4 + test_vm.shutdown() diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_cpu_features.py b/tests/hydropper/testcases/microvm/functional/test_microvm_cpu_features.py new file mode 100644 index 000000000..f72d6e3f3 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_cpu_features.py @@ -0,0 +1,172 @@ +# Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +"""Tests for the CPU topology emulation feature.""" + +import platform +import logging +import re +from enum import Enum +from enum import auto +import pytest +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', + level=logging.DEBUG, format=LOG_FORMAT) + + +class CpuVendor(Enum): + """CPU vendors enum.""" + + AMD = auto() + INTEL = auto() + + +def _get_cpu_vendor(): + cif = open('/proc/cpuinfo', 'r') + host_vendor_id = None + while True: + line = cif.readline() + if line == '': + break + matchoutput = re.search("^vendor_id\\s+:\\s+(.+)$", line) + if matchoutput: + host_vendor_id = matchoutput.group(1) + cif.close() + assert host_vendor_id is not None + + if host_vendor_id == "AuthenticAMD": + return CpuVendor.AMD + return CpuVendor.INTEL + + +def _check_guest_cmd_output(microvm, guest_cmd, expected_header, + expected_separator, + expected_key_value_store): + status, output = microvm.serial_cmd(guest_cmd) + + assert status == 0 + for line in output.splitlines(): + line = line.strip() + if line != '': + # all the keys have been matched. Stop. + if not expected_key_value_store: + break + + # try to match the header if needed. + if expected_header not in (None, ''): + if line.strip() == expected_header: + expected_header = None + continue + + # see if any key matches. + # we use a try-catch block here since line.split() may fail. + try: + [key, value] = list( + map(lambda x: x.strip(), line.split(expected_separator))) + except ValueError: + continue + + if key in expected_key_value_store.keys(): + assert value == expected_key_value_store[key], \ + "%s does not have the expected value" % key + del expected_key_value_store[key] + + else: + break + + assert not expected_key_value_store, \ + "some keys in dictionary have not been found in the output: %s" \ + % expected_key_value_store + + +def _check_cpu_topology(test_microvm, expected_cpu_count, + expected_threads_per_core, + expected_cores_per_socket, + expected_cpus_list): + expected_cpu_topology = { + "CPU(s)": str(expected_cpu_count), + "On-line CPU(s) list": expected_cpus_list, + "Thread(s) per core": str(expected_threads_per_core), + "Core(s) per socket": str(expected_cores_per_socket), + "Socket(s)": str(int(expected_cpu_count / expected_cores_per_socket / expected_threads_per_core)), + } + + _check_guest_cmd_output(test_microvm, "lscpu", None, ':', + expected_cpu_topology) + + +@pytest.mark.acceptance +def test_1vcpu_topo(microvm): + """ + Check the cpu topo for a microvm with the specified config: + + 1) Set vcpu_count=1, then launch. + 2) Check cpu topology with `lscpu` command. + """ + test_vm = microvm + test_vm.basic_config(vcpu_count=1) + test_vm.launch() + + _check_cpu_topology(test_vm, 1, 1, 1, "0") + + +@pytest.mark.acceptance +def test_128vcpu_topo(microvm): + """ + Check the CPUID for a microvm with the specified config: + + 1) Set vcpu_count=128 then launch. + 2) Check cpu topology with `lscpu` command. + """ + test_vm = microvm + test_vm.basic_config(vcpu_count=128) + test_vm.launch() + + if 'x86_64' in platform.machine(): + _check_cpu_topology(test_vm, 128, 1, 128, "0-127") + else: + _check_cpu_topology(test_vm, 128, 2, 2, "0-127") + + +@pytest.mark.skipif("platform.machine().startswith('aarch64')") +@pytest.mark.acceptance +def test_brand_string(microvm): + """Ensure correct format brand string in guest os. + + 1) Get brand string in '/proc/cpuinfo'. + 2) Check brand string format. + + """ + host_brand_string = None + cpuinfo = open('/proc/cpuinfo', 'r') + while True: + line = cpuinfo.readline() + if line != '': + matchoutput = re.search("^model name\\s+:\\s+(.+)$", line) + if matchoutput: + host_brand_string = matchoutput.group(1) + else: + break + cpuinfo.close() + assert host_brand_string is not None + + test_vm = microvm + + test_vm.basic_config(vcpu_count=1) + test_vm.launch() + + guest_cmd = "cat /proc/cpuinfo | grep 'model name' | head -1" + status, output = test_vm.serial_cmd(guest_cmd) + assert status == 0 + + line = output.splitlines()[0].rstrip() + matchoutput = re.search("^model name\\s+:\\s+(.+)$", line) + assert matchoutput + guest_brand_string = matchoutput.group(1) + assert guest_brand_string + + cpu_vendor = _get_cpu_vendor() + expected_guest_brand_string = "" + if cpu_vendor == CpuVendor.INTEL: + expected_guest_brand_string = host_brand_string + + assert guest_brand_string == expected_guest_brand_string diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_timer.py b/tests/hydropper/testcases/microvm/functional/test_microvm_timer.py new file mode 100644 index 000000000..922747e29 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_timer.py @@ -0,0 +1,33 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm timer""" + +from subprocess import run +from subprocess import PIPE +import pytest + +@pytest.mark.acceptance +def test_microvm_time(microvm): + """ + Test microvm time: + + 1) Launch to test_vm + 2) Get guest date and host date + 3) Compare them + """ + test_vm = microvm + test_vm.launch() + _, guest_date = test_vm.serial_cmd("date +%s") + host_date = run("date +%s", shell=True, check=True, + stdout=PIPE).stdout.decode('utf-8') + #The difference depends on machine performance + assert abs(int(guest_date) - int(host_date)) < 3 diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_vhost_vsock.py b/tests/hydropper/testcases/microvm/functional/test_microvm_vhost_vsock.py new file mode 100644 index 000000000..d1d0e383e --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_vhost_vsock.py @@ -0,0 +1,124 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm vhost vsock""" + +import os +import time +import logging +from subprocess import run +from threading import Thread +import pytest +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) +# Constants for nc-vsock setup and usage +NC_VSOCK_DIR = '/tmp/nc-vsock' +NC_VSOCK_CMD = os.path.join(NC_VSOCK_DIR, 'nc-vsock') +NC_VSOCK_SRV_OUT = os.path.join(NC_VSOCK_DIR, "server_out.txt") +NC_VSOCK_CLI_TXT = "/tmp/client_in.txt" +VSOCK_PORT = 1234 +BLOB_SIZE = 2000 + + +def _check_vsock_enable(microvm): + """Check virtio rng device in Guest""" + _cmd = "ls /dev/vsock" + status, _ = microvm.serial_cmd(_cmd) + if status != 0: + status1, _ = microvm.serial_cmd("modprobe vhost_vsock") + if status1 != 0: + logging.debug("vsock can't enable in guest, ignore to test") + return False + + status, _ = microvm.serial_cmd(_cmd) + assert status == 0 + return True + + +def _start_vsock_server_in_guest(microvm): + """Start vsock server in guest""" + _cmd = "%s -l %s > %s" % (NC_VSOCK_CMD, VSOCK_PORT, NC_VSOCK_SRV_OUT) + try: + ssh_session = microvm.create_ssh_session() + _ = ssh_session.cmd(_cmd, timeout=10, internal_timeout=10) + finally: + if ssh_session is not None: + ssh_session.close() + + +def _write_from_host_to_guest(nc_vsock_path, cid): + """Write data from host to guest""" + msg = "message from client" + _cmd = "echo %s > %s" % (msg, NC_VSOCK_CLI_TXT) + logging.debug("start to run %s", _cmd) + run(_cmd, shell=True, check=False) + _cmd = "%s %d %s < %s" % (nc_vsock_path, int(cid), VSOCK_PORT, NC_VSOCK_CLI_TXT) + logging.debug("start to run %s", _cmd) + output = run(_cmd, shell=True, check=False).stdout + logging.debug(output) + return msg + + +def _get_recv_data_from_guest(microvm): + _cmd = "cat %s" % NC_VSOCK_SRV_OUT + try: + ssh_session = microvm.create_ssh_session() + _, output = ssh_session.cmd_status_output(_cmd) + logging.debug("recv data from guest is %s", output.strip()) + return output.strip() + finally: + if ssh_session is not None: + ssh_session.close() + +@pytest.mark.acceptance +def test_microvm_virtio_vsock(microvm, nc_vsock_path, test_session_root_path): + """Test virtio-rng device""" + test_vm = microvm + test_vm.basic_config(vsocknums=1) + test_vm.launch() + + # check virtio-vsock device + if not _check_vsock_enable(test_vm): + pytest.skip("vhost-vsock init failed, skip this testcase") + + # generate the blob file. + blob_path = os.path.join(test_session_root_path, "vsock-test.blob") + run("rm -rf %s; dd if=/dev/urandom of=%s bs=1 count=%d" % + (blob_path, blob_path, BLOB_SIZE), shell=True, check=True) + vm_blob_path = "/tmp/nc-vsock/test.blob" + + # set up a tmpfs drive on the guest, then we can copy the blob file there. + session = test_vm.create_ssh_session() + cmd = "mkdir -p /tmp/nc-vsock" + cmd += " && mount -t tmpfs tmpfs -o size={} /tmp/nc-vsock".format( + BLOB_SIZE + 1024*1024 + ) + status, _ = session.cmd_status_output(cmd) + session.close() + assert status == 0 + + # copy nc-vsock tool and the blob file to the guest. + test_vm.scp_file(nc_vsock_path, NC_VSOCK_CMD) + test_vm.scp_file(blob_path, vm_blob_path) + + # start vsock server in guest + server = Thread(target=_start_vsock_server_in_guest, args=(test_vm,)) + server.start() + time.sleep(5) + + # write data from host to guest + msg = _write_from_host_to_guest(nc_vsock_path, test_vm.vsock_cid[0]) + + server.join(10) + if server.is_alive(): + logging.error("The server thread is still running in the guest") + msg_recv = _get_recv_data_from_guest(test_vm) + assert msg == msg_recv diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_blk.py b/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_blk.py new file mode 100644 index 000000000..ce7ce7ce1 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_blk.py @@ -0,0 +1,157 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm virtio block""" + +import os +import logging +from subprocess import run +from subprocess import PIPE +import pytest +from utils.config import CONFIG +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + + +def _check_virtio_blk_vectors(microvm): + _cmd = "lspci |grep -w Virtio" + _, output = microvm.serial_cmd(_cmd) + index = 0 + for line in output.splitlines(): + if "block" in line.strip(): + _check_vec_cmd = "grep -w virtio%d /proc/interrupts | wc -l" % index + _status, _output = microvm.serial_cmd(_check_vec_cmd) + vecs = int(_output.splitlines()[-2].strip()) + expect_vecs = 2 + assert vecs == expect_vecs + index += 1 + + +@pytest.mark.acceptance +@pytest.mark.parametrize("readonly", [True, False]) +def test_microvm_virtio_blk_configuration(test_session_root_path, microvm, readonly): + """ + Test virtio-blk configuration: + + 1) Generate a temp disk + 2) Configure temp disk read_only and Add it to test_vm + 3) Launch to test_vm and get block information + 4) Assert temp disk readonly as expect + """ + test_vm = microvm + temp_disk = os.path.join(test_session_root_path, "test_image") + run("rm -rf %s; dd if=/dev/zero of=%s bs=1M count=16" % + (temp_disk, temp_disk), shell=True, check=True) + test_vm.add_drive(path_on_host=temp_disk, read_only=readonly) + test_vm.launch() + _cmd = "ls /sys/bus/virtio/drivers/virtio_blk/ | grep -c virtio[0-9]*" + _, output = test_vm.serial_cmd(_cmd) + virtio_blk_number_in_guest = int(output.split('\n')[-2].strip()) + if CONFIG.stratovirt_feature == "pci": + assert virtio_blk_number_in_guest == 2 + + # check virtio-blk irq vectors + if CONFIG.stratovirt_feature == "pci": + _check_virtio_blk_vectors(test_vm) + + # check readonly + _cmd = "lsblk | grep vdb | awk '{print $5}'" + _, output = test_vm.serial_cmd(_cmd) + expect_ro = 1 if readonly else 0 + assert int(output.split('\n')[0].strip()) == expect_ro + + +@pytest.mark.system +@pytest.mark.parametrize("testtimes", [1, 10]) +def test_microvm_virtio_blk_at_dt(test_session_root_path, microvm, testtimes): + """ + Test virtio-blk hotplug and unplug: + + 1) Generate 5 temp disks and add them to test_vm. + 2) Assert disks' name and size as expect. + 3) Delete temp disks from test_vm. + 4) Assert temp disks are deleted. + """ + test_vm = microvm + test_vm.launch() + disknum = 5 + disklist = [] + for index in range(disknum): + temp_disk = os.path.join(test_session_root_path, "test_image%d" % (index + 1)) + run("rm -rf %s; dd if=/dev/zero of=%s bs=1M count=16" % + (temp_disk, temp_disk), shell=True, check=True) + disklist.append(temp_disk) + + for _ in range(testtimes): + index = 1 + for disk in disklist: + test_vm.add_disk(disk, index=index) + index += 1 + + blkinfo = test_vm.get_lsblk_info() + logging.debug("blkinfo is %s", blkinfo) + + for devid in ["vdb", "vdc", "vdd", "vde", "vdf"]: + assert devid in blkinfo + assert blkinfo[devid]["size"] == "16M" + + index = 1 + for disk in disklist: + test_vm.del_disk(index=index) + index += 1 + + blkinfo = test_vm.get_lsblk_info() + for devid in ["vdb", "vdc", "vdd", "vde", "vdf"]: + assert devid not in blkinfo + +@pytest.mark.acceptance +def test_microvm_virtio_blk_md5(test_session_root_path, microvm): + """ + Test data consistency by md5sum: + + 1) Generate a temp disk for test_vm and launch. + 2) Mount the temp disk + 3) Touch a file and compute it md5sum. + 4) Umount the temp disk + 5) Exit the vm, mount the temp disk to hostos and compute the file md5sum again. + 6) Assert the same values twice + """ + test_vm = microvm + temp_disk = os.path.join(test_session_root_path, "test_image") + run("rm -rf %s; dd if=/dev/zero of=%s bs=1M count=16" % + (temp_disk, temp_disk), shell=True, check=True) + test_vm.launch() + test_vm.add_disk(temp_disk) + + blkinfo = test_vm.get_lsblk_info() + logging.debug("blkinfo is %s", blkinfo) + + format_cmd = "mkfs.ext4 /dev/vdb" + test_vm.serial_cmd(format_cmd) + + mount_cmd = "mount /dev/vdb /mnt" + test_vm.serial_cmd(mount_cmd) + + wirte_cmd = "touch /mnt/test_virtioblk.c" + test_vm.serial_cmd(wirte_cmd) + + _cmd = "md5sum /mnt/test_virtioblk.c" + _, md5 = test_vm.serial_cmd(_cmd) + test_vm.serial_cmd("umount /mnt") + + test_vm.shutdown() + try: + run("mount %s /mnt" % temp_disk, shell=True, check=True) + output = run("md5sum /mnt/test_virtioblk.c", shell=True, check=True, + stdout=PIPE).stdout.decode('utf-8') + assert output == md5 + finally: + run("umount /mnt", shell=True, check=False) diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_net.py b/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_net.py new file mode 100644 index 000000000..f4a5a917b --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_virtio_net.py @@ -0,0 +1,205 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm net""" + +import logging +from subprocess import run +from subprocess import PIPE +import pytest +from utils.config import CONFIG +from utils.resources import NETWORKS +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + + +def _start_iperf_on_guest(microvm): + """Start iperf in server mode on guest through serial session""" + _cmd = "which iperf3" + status, _ = microvm.serial_cmd(_cmd) + if status != 0: + logging.warning("iperf3 is not running, ignore to test") + return False + + iperf_cmd = "iperf3 -sD -f KBytes\n" + status, _ = microvm.serial_cmd(iperf_cmd) + return not bool(status) + + +def _run_iperf_on_local(iperf_cmd): + process = run(iperf_cmd, shell=True, stdout=PIPE, check=True) + return process.stdout.decode("utf-8") + + +def _check_virtio_net_vectors(microvm, mqueue=1): + _cmd = "lspci |grep -w Virtio" + _, output = microvm.serial_cmd(_cmd) + index = 0 + for line in output.splitlines(): + if "network" in line.strip(): + _check_vec_cmd = "grep -w virtio%d /proc/interrupts | wc -l" % index + _status, _output = microvm.serial_cmd(_check_vec_cmd) + vecs = int(_output.splitlines()[-2].strip()) + expect_vecs = 2 * mqueue + 1 + assert vecs == expect_vecs + index += 1 + + +@pytest.mark.acceptance +@pytest.mark.parametrize("vhost_type", ["vhost-kernel", None]) +def test_microvm_vnet_send_recv(microvm, vhost_type): + """ + Test virtio-net send and recv: + + 1) Set vhost_type and launch to test_vm + 2) Check nic numbers + 3) Test the vnet by ping + 4) Test TCP/UDP by iperf3 + """ + if vhost_type == "vhost-kernel": + pytest.skip('vhost is not supportted in serverless.') + + test_vm = microvm + test_vm.basic_config(vhost_type=vhost_type) + test_vm.launch() + # check nic numbers + if CONFIG.stratovirt_feature == "pci": + assert len(test_vm.get_interfaces_inner()) == len(test_vm.taps) + _cmd = "ls /sys/bus/virtio/drivers/virtio_net/ | grep -c virtio[0-9]*" + _, output = test_vm.serial_cmd(_cmd) + logging.debug("virtio net output is %s", output) + virtio_net_number_in_guest = int(output.split('\n')[-2].strip()) + assert virtio_net_number_in_guest == len(test_vm.get_interfaces_inner()) + + # check nic irq vectors (multi queues is not support yet, so one virtio-net has 3 vectors) + if CONFIG.stratovirt_feature == "pci": + _check_virtio_net_vectors(test_vm) + + # test ICMP (ping to vm) + run("ping -c 2 %s" % test_vm.guest_ip, shell=True, check=True) + status, output = test_vm.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + assert status == 0 + # test TCP/UDP by iperf3 + if not _start_iperf_on_guest(test_vm): + return + + iperf_cmd = "/usr/bin/iperf3 -c %s -t 5" % test_vm.guest_ip + output = _run_iperf_on_local(iperf_cmd) + logging.debug(output) + + iperf_cmd = "iperf3 -c %s -u -t 5" % test_vm.guest_ip + output = _run_iperf_on_local(iperf_cmd) + logging.debug(output) + + iperf_cmd = "iperf3 -c %s -t 5 -R" % test_vm.guest_ip + output = _run_iperf_on_local(iperf_cmd) + logging.debug(output) + + iperf_cmd = "iperf3 -c %s -u -t 5 -R" % test_vm.guest_ip + output = _run_iperf_on_local(iperf_cmd) + logging.debug(output) + + +@pytest.mark.system +def test_microvm_vnet_stop_cont(microvm): + """ + Test virtio-net stop and continue, check vnet with mac: + + 1) Launch to testvm + 2) Test the vnet by ping + 3) Restart vnet and execute step 2 again + 4) Stop and continue test_vm + 5) Execute step 2 again + """ + test_vm = microvm + test_vm.basic_config() + test_vm.launch() + # test ICMP (ping to vm) + run("ping -c 2 %s" % test_vm.guest_ip, shell=True, check=True) + status, _ = test_vm.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + assert status == 0 + test_vm.serial_cmd("systemctl restart network") + # test ICMP (ping to vm) + run("ping -c 2 %s" % test_vm.guest_ip, shell=True, check=True) + status, _ = test_vm.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + assert status == 0 + test_vm.stop() + test_vm.event_wait(name='STOP') + test_vm.cont() + test_vm.event_wait(name='RESUME') + # test ICMP (ping to vm) + run("ping -c 2 %s" % test_vm.guest_ip, shell=True, check=True) + status, _ = test_vm.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + assert status == 0 + + +@pytest.mark.acceptance +@pytest.mark.parametrize("usemac", [True, False]) +def test_microvm_with_multi_vnet(microvm, usemac): + """ + Test microvm with multi vnet: + + 1) Configure some vnets for test_vm + 2) Check mac address + 3) Check nic numbers + 4) Test vnets by ping + 5) Delete vnet from test_vm + """ + test_vm = microvm + test_vm.basic_config(vnetnums=2, withmac=usemac) + test_vm.launch() + # check mac address + _cmd = "ls" + for tap in test_vm.taps: + _cmd += " && (ip addr | grep %s) " % tap["mac"] + status = test_vm.serial_session.run_func("cmd_status", _cmd) + expect_status = 0 if usemac else 1 + assert status == expect_status + + # check nic numbers + if CONFIG.stratovirt_feature == "pci": + assert len(test_vm.get_interfaces_inner()) == len(test_vm.taps) + _cmd = "ls /sys/bus/virtio/drivers/virtio_net/ | grep -c virtio[0-9]*" + _, output = test_vm.serial_cmd(_cmd) + virtio_net_number_in_guest = int(output.split('\n')[-2].strip()) + assert virtio_net_number_in_guest == len(test_vm.get_interfaces_inner()) + # test ICMP (ping to vm) + run("ping -c 2 %s" % test_vm.guest_ip, shell=True, check=True) + status, output = test_vm.serial_cmd("ping -c 2 %s" % NETWORKS.ipaddr) + assert status == 0 + + +@pytest.mark.system +@pytest.mark.parametrize("times", [1, 10]) +def test_microvm_vnet_hotplug(microvm, times): + """ + Test hotplug virtio-net: + + 1) Configure a vnet for test_vm + 2) Add vnet to test_vm + 3) Test the vnet by ping + 4) Delete vnet from test_vm + """ + test_vm = microvm + test_vm.basic_config(vnetnums=1, withmac=False) + test_vm.launch() + for index in range(times): + logging.debug("test vnet hotplug loop %d", (index + 1)) + test_vm.add_net() + # check nic connection + try: + run("ping -c 2 %s" % test_vm.guest_ips[-1], shell=True, check=True) + # Retry no matter what exception occurs + # pylint: disable=broad-except + except Exception: + logging.debug("ping failed, try again") + run("ping -c 10 %s" % test_vm.guest_ips[-1], shell=True, check=True) + test_vm.del_net() diff --git a/tests/hydropper/testcases/microvm/functional/test_microvm_vmlife.py b/tests/hydropper/testcases/microvm/functional/test_microvm_vmlife.py new file mode 100644 index 000000000..6581b53b6 --- /dev/null +++ b/tests/hydropper/testcases/microvm/functional/test_microvm_vmlife.py @@ -0,0 +1,119 @@ +# Copyright (c) 2021 Huawei Technologies Co.,Ltd. All rights reserved. +# +# StratoVirt 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. +"""Test microvm vmlife""" + +import logging +import pytest +from utils import utils_qmp +from utils.config import CONFIG +from utils.exception import QMPTimeoutError +LOG_FORMAT = "%(asctime)s - %(levelname)s - %(message)s" +logging.basicConfig(filename='/var/log/pytest.log', level=logging.DEBUG, format=LOG_FORMAT) + + +@pytest.mark.acceptance +@pytest.mark.parametrize("vcpu_count, memsize, vnetnums", + [(1, 256, 1), + (2, 1024, 2), + (8, 8192, 3), + (32, 40960, 4)]) +def test_microvm_start(microvm, vcpu_count, memsize, vnetnums): + """Test a normal microvm start""" + if CONFIG.stratovirt_feature != "pci" and vnetnums > 2: + pytest.skip('It is not supportted if vnet number is greater than 2 in mmio.') + test_vm = microvm + test_vm.basic_config(vcpu_count=vcpu_count, mem_size=memsize * 1024 * 1024, vnetnums=vnetnums) + test_vm.launch() + vmhwinfo = test_vm.get_guest_hwinfo() + logging.debug("current vmhwinfo is %s", vmhwinfo) + assert vmhwinfo["cpu"]["vcpu_count"] == vcpu_count + assert vmhwinfo["mem"]["memsize"] > (memsize * 1024 * 90 / 100) + if CONFIG.stratovirt_feature == "pci": + assert len(vmhwinfo["virtio"]["virtio_blk"]) == 1 + assert len(vmhwinfo["virtio"]["virtio_net"]) == vnetnums + else: + assert len(vmhwinfo["virtio"]["virtio_blk"]) == 6 + assert len(vmhwinfo["virtio"]["virtio_net"]) == 2 + # virtio_rng is not supported yet. + # assert len(vmhwinfo["virtio"]["virtio_rng"]) == vrngnums + assert len(vmhwinfo["virtio"]["virtio_console"]) == 1 + test_vm.shutdown() + + +@pytest.mark.system +@pytest.mark.parametrize("destroy_value", [9, 15]) +def test_microvm_destroy(microvm, destroy_value): + """Test a normal microvm destroy(kill -9)""" + test_vm = microvm + test_vm.launch() + test_vm.destroy(signal=destroy_value) + + +@pytest.mark.system +def test_microvm_inshutdown(microvm): + """Test a normal microvm inshutdown""" + test_vm = microvm + test_vm.launch() + test_vm.inshutdown() + + +@pytest.mark.acceptance +def test_microvm_pause_resume(microvm): + """Test a normal microvm pause""" + test_vm = microvm + test_vm.launch() + resp = test_vm.query_status() + utils_qmp.assert_qmp(resp, "return/status", "running") + test_vm.stop() + test_vm.event_wait(name='STOP') + resp = test_vm.query_status() + utils_qmp.assert_qmp(resp, "return/status", "paused") + test_vm.cont() + test_vm.event_wait(name='RESUME') + resp = test_vm.query_status() + utils_qmp.assert_qmp(resp, "return/status", "running") + ret, _ = test_vm.serial_cmd("ls") + assert ret == 0 + + +@pytest.mark.system +def test_microvm_pause_resume_abnormal(microvm): + """Abnormal test for microvm pause/resume""" + test_vm = microvm + test_vm.launch() + resp = test_vm.cont() + utils_qmp.assert_qmp(resp, "error/class", "GenericError") + with pytest.raises(QMPTimeoutError): + test_vm.event_wait(name='RESUME', timeout=3.0) + test_vm.qmp_reconnect() + ret, _ = test_vm.serial_cmd("ls") + assert ret == 0 + resp = test_vm.stop() + utils_qmp.assert_qmp(resp, "return", {}) + test_vm.event_wait(name='STOP') + resp = test_vm.stop() + utils_qmp.assert_qmp(resp, "error/class", "GenericError") + with pytest.raises(QMPTimeoutError): + test_vm.event_wait(name='STOP', timeout=3.0) + test_vm.qmp_reconnect() + resp = test_vm.cont() + utils_qmp.assert_qmp(resp, "return", {}) + test_vm.event_wait(name='RESUME') + ret, _ = test_vm.serial_cmd("ls") + assert ret == 0 + resp = test_vm.cont() + utils_qmp.assert_qmp(resp, "error/class", "GenericError") + ret, _ = test_vm.serial_cmd("ls") + assert ret == 0 + with pytest.raises(QMPTimeoutError): + test_vm.event_wait(name='RESUME', timeout=3.0) + test_vm.qmp_reconnect() -- Gitee