From 79f6010911973a126b703b5ed699da4b1be4b6e5 Mon Sep 17 00:00:00 2001 From: Sebastian Mitterle Date: Wed, 17 Apr 2024 17:50:50 +0200 Subject: [PATCH] filesystem_device: test usage for unprivileged users Add new test cases checking that non-root users can mount and use a host directory. Variants: 2 vms, 1 filesystem 1 vm, 1 filesystem 1 vm, 2 filesystems Notes: 1. use wait_for_serial login because the unprivileged user's VMs are created with interface type 'user' per default, so there's no vnet nic and therefore wait_for_login can't get the address 2. Due to 1. it's better to use the 'safe' command functions to avoid timeouts because there might be kernel messages breaking the output scan. 3. For hugepages, the unprivileged user needs access to a hugetlbfs and update their local libvirt configuration. After updating the configuration, the process needs to be killed to force the daemon to be loaded with the new configuration. This is expected. 4. Make sure to close and reopen sessions for the virsh instance of the unprivileged user to avoid running into issues when setting up hugepages. 5. virsh_sysinfo: update reference after change in avocado-vt that this test implementation depends on. Signed-off-by: Sebastian Mitterle --- .../filesystem_device_unprivileged.cfg | 35 ++ .../tests/src/virsh_cmd/host/virsh_sysinfo.py | 2 +- .../filesystem_device_unprivileged.py | 408 ++++++++++++++++++ spell.ignore | 1 + 4 files changed, 445 insertions(+), 1 deletion(-) create mode 100644 libvirt/tests/cfg/virtual_device/filesystem_device_unprivileged.cfg create mode 100644 libvirt/tests/src/virtual_device/filesystem_device_unprivileged.py diff --git a/libvirt/tests/cfg/virtual_device/filesystem_device_unprivileged.cfg b/libvirt/tests/cfg/virtual_device/filesystem_device_unprivileged.cfg new file mode 100644 index 0000000000..50e57312c7 --- /dev/null +++ b/libvirt/tests/cfg/virtual_device/filesystem_device_unprivileged.cfg @@ -0,0 +1,35 @@ +- virtual_devices.filesystem_device_unprivileged: + type = filesystem_device_unprivileged + # 10.0.0-6 + func_supported_since_libvirt_ver = (10, 0, 0) + take_regular_screendumps = "no" + start_vm = no + vcpus_per_cell = 2 + extra_hugepages = 128 + test_user = test + test_passwd = test + pseries: + vcpus_per_cell = 5 + aarch64: + extra_hugepages = 0 + s390-virtio: + with_numa = no + variants plugmode: + - hotplug: + - coldplug: + variants: + - one_guest: + unpr_vms = unpr-vm + - two_guests: + unpr_vms = unpr-vm,unpr-vm2 + variants memorybacking: + - with_hugepages: + s390-virtio: + kvm_module_parameters = hpage=1 + - with_memfd: + - with_shm: + variants: + - one_fs: + fs_dicts = [{'accessmode': 'passthrough', 'source': {'dir': '/tmp/dir1'}, "target": {'dir': 'mount_tag1'}, 'binary': {'path':'/usr/libexec/virtiofsd', 'lock_posix':'off','flock':'off', 'sandbox_mode':'namespace', 'xattr':'on', 'cache_mode':'none'}, 'driver': {'type': 'virtiofs', 'queue':'1024'}}] + - two_fs: + fs_dicts = [{'accessmode': 'passthrough', 'source': {'dir': '/tmp/dir1'}, "target": {'dir': 'mount_tag1'}, 'binary': {'lock_posix':'off','flock':'off', 'sandbox_mode':'namespace'}, 'driver': {'type': 'virtiofs'}}, {'accessmode': 'passthrough', 'source': {'dir': '/tmp/dir2'}, "target": {'dir': 'mount_tag2'}, 'binary': {'lock_posix':'off','flock':'off', 'sandbox_mode':'namespace', 'xattr':'on', 'cache_mode':'none'}, 'driver': {'type': 'virtiofs'}}] diff --git a/libvirt/tests/src/virsh_cmd/host/virsh_sysinfo.py b/libvirt/tests/src/virsh_cmd/host/virsh_sysinfo.py index 4f66dd1951..1c621e7702 100644 --- a/libvirt/tests/src/virsh_cmd/host/virsh_sysinfo.py +++ b/libvirt/tests/src/virsh_cmd/host/virsh_sysinfo.py @@ -60,7 +60,7 @@ def run(test, params, env): dmidecode_version = get_processor_version() if dmidecode_version: # Get processor version from result - sysinfo_xml = libvirt_xml.SysinfoXML() + sysinfo_xml = libvirt_xml.VMSysinfoXML() sysinfo_xml['xml'] = output sysinfo_xml.xmltreefile.write() diff --git a/libvirt/tests/src/virtual_device/filesystem_device_unprivileged.py b/libvirt/tests/src/virtual_device/filesystem_device_unprivileged.py new file mode 100644 index 0000000000..2b466b2b6c --- /dev/null +++ b/libvirt/tests/src/virtual_device/filesystem_device_unprivileged.py @@ -0,0 +1,408 @@ +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +# +# Copyright Red Hat +# +# SPDX-License-Identifier: GPL-2.0 +# +# Author: smitterl@redhat.com +# +# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +import logging +import os +import shutil +import stat + +from aexpect import ShellSession + +from avocado.core.exceptions import TestError +from avocado.utils import process + +from virttest import libvirt_version +from virttest import remote +from virttest import utils_ids +from virttest import virsh +from virttest.staging import utils_memory +from virttest.libvirt_xml.vm_xml import VMXML +from virttest.utils_config import LibvirtQemuConfig +from virttest.utils_libvirt.libvirt_unprivileged import get_unprivileged_vm +from virttest.utils_libvirt.libvirt_vmxml import create_vm_device_by_type + +LOG = logging.getLogger('avocado.' + __name__) +allow_all = stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO + +unpr_virsh = "" +vmxmls = {} +backupxmls = [] +vms = [] +test_user = "" +test_passwd = "" +_params = {} +backup_huge_pages_num = 0 +user_config_path = "" +hugepages_user_path = "/user_hugepages" +with_hugepages = False +qemu_config = None + + +def get_unprivileged_vms(): + """ + Store the unprivileged user's VMs and the virsh + object for interaction as unprivileged user. + + Assumes that the non-root user exists on the system + and that they have VMs of given names. + """ + + global vmxmls, backupxmls, vms + + vm_names = _params.get("unpr_vms").split(",") + unpr_vm_args = { + "username": _params.get("username"), + "password": _params.get("password"), + } + + vms = [get_unprivileged_vm(vm_name, test_user, test_passwd, **unpr_vm_args) + for vm_name in vm_names] + + for vm_name in vm_names: + vmxmls[vm_name] = VMXML.new_from_inactive_dumpxml( + vm_name, + virsh_instance=unpr_virsh, + ) + + backupxmls = [vmxmls[name].copy() for name in vmxmls] + + +def _initialize_unpr_virsh(): + """ + Initializes the global instance of the virsh commands object. + """ + + global unpr_virsh + unpr_uri = f"qemu+ssh://{test_user}@localhost/session" + unpr_virsh = virsh.VirshPersistent(uri=unpr_uri, safe=True) + + host_session = ShellSession("su") + remote.VMManager.set_ssh_auth(host_session, "localhost", + test_user, test_passwd) + host_session.close() + + +def _refresh_vmxmls(): + """ + Refreshes the global list of VMXML instances under test + so they are up-to-date after changes. + """ + global vmxmls + for name in vmxmls: + vmxmls[name] = VMXML.new_from_dumpxml(name, virsh_instance=unpr_virsh) + LOG.debug("Current XML: %s", vmxmls[name]) + + +def configure_hugepages_for_unprivileged_user(mem): + """ + Allocates hugepages and creates a mount for the unprivileged + test user. + + Assume that all VMs have the same max_mem value so if + the QEMU configuration has already been altered it assumes + hugepages have already been set up and will return. + + It kills the user's process at the end to force reload + with the new configuration. + + :param mem: The memory size that needs to be allocated + in the same unit as the VMXML + """ + global backup_huge_pages_num, qemu_config + if qemu_config: + return + if not os.path.exists(user_config_path): + shutil.copyfile(LibvirtQemuConfig.conf_path, user_config_path) + os.chmod(user_config_path, allow_all) + extra_hugepages = _params.get_numeric("extra_hugepages") + host_hp_size = utils_memory.get_huge_page_size() + backup_huge_pages_num = utils_memory.get_num_huge_pages() + huge_pages_num = 0 + huge_pages_num += mem // host_hp_size + extra_hugepages + utils_memory.set_num_huge_pages(huge_pages_num) + process.run(f"mkdir {hugepages_user_path}", + ignore_status=False) + process.run(f"mount -t hugetlbfs hugetlbfs {hugepages_user_path}", + ignore_status=False) + process.run(f"chmod a+wrx {hugepages_user_path}", + ignore_status=False) + qemu_config = LibvirtQemuConfig(user_config_path) + qemu_config.hugetlbfs_mount = [hugepages_user_path] + process.run(f"killall --user {test_user} virtqemud", shell=True) + + +def clean_up_hugepages_for_unprivileged_user(): + """ + Cleans up the set up for hugepages + """ + qemu_config.restore() + process.run(f"killall --user {test_user} virtqemud", shell=True) + utils_memory.set_num_huge_pages(backup_huge_pages_num) + process.run(f"umount {hugepages_user_path}") + process.run(f"rm -rf {hugepages_user_path}") + + +def add_memory_backing(): + """ + Adds and removes all present filesystem devices + It also sets up hugepages for the unprivileged user assuming + that all VMs would require the same number of memory + """ + + global backup_huge_pages_num, with_hugepages + memorybacking = _params.get("memorybacking") + with_hugepages = "with_hugepages" == memorybacking + with_memfd = "with_memfd" == memorybacking + with_numa = "yes" == _params.get("with_numa") + vcpus_per_cell = _params.get("vcpus_per_cell") + + for name in vmxmls.copy(): + vmxml = vmxmls[name] + if vmxml.max_mem < 1024000: + vmxml.max_mem = 1024000 + if with_hugepages: + configure_hugepages_for_unprivileged_user(len(vms)*vmxml.max_mem) + numa_no = None + if with_numa: + numa_no = vmxml.vcpu // vcpus_per_cell if vmxml.vcpu != 1 else 1 + + vmxml.remove_all_device_by_type("filesystem") + + VMXML.set_vm_vcpus( + vmxml.vm_name, + vmxml.vcpu, + numa_number=numa_no, + virsh_instance=None, + vmxml=vmxml, + ) + VMXML.set_memoryBacking_tag( + vmxml.vm_name, + access_mode="shared", + hpgs=with_hugepages, + memfd=with_memfd, + virsh_instance=None, + vmxml=vmxml, + ) + + _initialize_unpr_virsh() + vmxml.sync(virsh_instance=unpr_virsh) + vmxmls[vmxml.vm_name] = vmxml + + +def cold_or_hot_plug_filesystem(): + """ + Cold or hot plugs the filesystem and + starts the VMs. + + It handles the VM state and makes sure + they are running for further testing. + """ + + for vm in vms: + if "hotplug" == _params.get("plugmode") and not vm.is_alive(): + vm.start() + vm.wait_for_serial_login().close() + if "coldplug" == _params.get("plugmode") and vm.is_alive(): + vm.destroy() + + for fs_dict in fs_dicts: + source_dir = fs_dict["source"]["dir"] + if not os.path.exists(source_dir): + os.mkdir(source_dir) + os.chmod(source_dir, allow_all) + + fs = create_vm_device_by_type("filesystem", fs_dict) + os.chmod(fs.xml, allow_all) + unpr_virsh.attach_device( + vm.name, + fs.xml, + flagstr="--current", + debug=True, + ignore_status=False + ) + + if not vm.is_alive(): + vm.start() + _refresh_vmxmls() + + +def check_virtiofs_idmap(): + """ + Checks if the unprivileged VM is running with the correct + user related ids. + """ + + user_info = utils_ids.get_user_ids(test_user) + for name in vmxmls: + for fs in vmxmls[name].get_devices("filesystem"): + utils_ids.check_idmap_xml_filesystem_device(user_info, fs) + + +def mount_fs(session): + """ + Mounts the folder inside of the guest + + :param session: Guest console session + """ + + for fs_dict in fs_dicts: + mount_tag = fs_dict["target"]["dir"] + mount_dir = f"/mnt/{mount_tag}" + session.cmd_output_safe(f"mkdir {mount_dir}") + session.cmd_output_safe(f"mount -t virtiofs {mount_tag} {mount_dir}") + + +def create_file(session): + """ + Creates a file in the guest's mounted filesystem(s) + + :param session: guest console session + """ + + for fs_dict in fs_dicts: + mount_tag = fs_dict["target"]["dir"] + mount_dir = f"/mnt/{mount_tag}" + session.cmd_output_safe(f"dd if=/dev/zero of={mount_dir}/testfile bs=1M count=10") + session.cmd_output_safe("sync") + + +def check_md5sum(sessions): + """ + Returns the md5sum of the created file + + :param sessions: a list of two console sessions + to take md5sum from and compare + them; the second session might + be a host session (if there is only + 1 vm in the test scenario) + """ + + sums = {0: [], 1: []} + for fs_dict in fs_dicts: + for i in range(len(sessions)): + mount_tag = fs_dict["target"]["dir"] + mount_dir = f"/mnt/{mount_tag}" + source_dir = fs_dict["source"]["dir"] + cmd = f"md5sum {mount_dir}/testfile" + if i == 1 and len(vms) == 1: + cmd = f"md5sum {source_dir}/testfile" + o = sessions[i].cmd_output_safe(cmd) + sums[i].append(o.split()[0]) + + if sums[0] != sums[1]: + raise TestError("The md5sums don't match: %s" % sums) + + +def check_filesystem(after="attach"): + """ + Prepares the filesystems in the guest(s) and runs some checks + + This assumes that the filesystem is shared either between guest + and host or between two guests. + + :param after: if "attach" expect commands to succeed + if "detach" expect commands to fail + """ + + session1 = vms[0].wait_for_serial_login() + if len(vms) == 2: + session2 = vms[1].wait_for_serial_login() + else: + session2 = ShellSession("su") + + try: + mount_fs(session1) + create_file(session1) + if len(vms) == 2: + mount_fs(session2) + check_md5sum([session1, session2]) + + except TestError as e: + if after == "detach": + pass + else: + raise e + + finally: + session1.close() + session2.close() + + +def cold_or_hot_unplug_filesystem(): + """ + Cold or hot unplugs the filesystem and + starts the VMs. + + It handles the VM state and makes sure they are + running for further checks. + """ + + for vm in vms: + if "coldplug" == _params.get("plugmode") and vm.is_alive(): + vm.destroy() + + for fs_dict in fs_dicts: + fs = create_vm_device_by_type("filesystem", fs_dict) + os.chmod(fs.xml, allow_all) + unpr_virsh.detach_device( + vm.name, + fs.xml, + flagstr="--current", + debug=True, + ignore_status=False + ) + + if not vm.is_alive(): + vm.start() + + +def initialize(params): + """ + Initializes parameters that are needed globally for all test + variants. + + :param params: the test parameters + """ + + global _params, fs_dicts, test_user, user_config_path, test_passwd + libvirt_version.is_libvirt_feature_supported(params) + _params = params + fs_dicts = eval(_params.get("fs_dicts")) + test_user = _params.get("test_user", "") + test_passwd = _params.get("test_passwd", "") + user_config_path = f"/home/{test_user}/.config/libvirt/qemu.conf" + _initialize_unpr_virsh() + + +def run(test, params, env): + """ + Test running VMs with a filesystem device as unprivileged users. + """ + + global backupxmls, unpr_virsh, fs_dicts, with_hugepages + initialize(params) + try: + get_unprivileged_vms() + add_memory_backing() + cold_or_hot_plug_filesystem() + check_virtiofs_idmap() + check_filesystem(after="attach") + cold_or_hot_unplug_filesystem() + check_filesystem(after="detach") + finally: + for xml in backupxmls: + xml.sync(virsh_instance=unpr_virsh) + if unpr_virsh: + del unpr_virsh + for fs_dict in fs_dicts: + source_dir = fs_dict["source"]["dir"] + if os.path.exists(source_dir): + shutil.rmtree(source_dir) + if with_hugepages: + clean_up_hugepages_for_unprivileged_user() diff --git a/spell.ignore b/spell.ignore index ffd6ad5ed0..808b27ec0a 100644 --- a/spell.ignore +++ b/spell.ignore @@ -585,6 +585,7 @@ mdev mdevctl mellanox mem +memoryBacking mems memballoon memhog