diff --git a/libvirt/tests/cfg/memory/memory_hotplug_virshdump.cfg b/libvirt/tests/cfg/memory/memory_hotplug_virshdump.cfg new file mode 100644 index 00000000000..f35b65f6d72 --- /dev/null +++ b/libvirt/tests/cfg/memory/memory_hotplug_virshdump.cfg @@ -0,0 +1,42 @@ +# Memory Hotplug with Virsh Dump Test Configuration + +- memory_hotplug_virshdump: + type = memory_hotplug_virshdump + start_vm = yes + kill_vm = yes + kill_vm_gracefully = yes + + # Memory configuration + mem = 2097152 + current_mem = 2097152 + max_mem = 20971520 + max_mem_rt = 20971520 + mem_unit = "KiB" + + # Memory device parameters + tg_size = 524288 + tg_sizeunit = "KiB" + tg_node = 0 + mem_model = "dimm" + slots = 40 + + # VM configuration + vcpu_cores = 4 + vcpu_threads = 1 + vcpu_sockets = 1 + numa_cells = 1 + + variants: + - continuous_hotplug: + variants: + - virshdump: + variants: + - positive: + variants: + - basic_test: + mem_hotplug_iterations = 16 + mem_hotplug_delay = 10 + virshdump_iterations = 16 + virshdump_delay = 2 + dump_path = "./virsh_dumps" + dump_options = "--memory-only --bypass-cache" diff --git a/libvirt/tests/cfg/storage/virsh_pool_vol.cfg b/libvirt/tests/cfg/storage/virsh_pool_vol.cfg new file mode 100644 index 00000000000..dce368480c9 --- /dev/null +++ b/libvirt/tests/cfg/storage/virsh_pool_vol.cfg @@ -0,0 +1,169 @@ +- virsh.pool_vol: + type = virsh_pool_vol + vms = '' + main_vm = '' + start_vm = no + vol_capacity = "10G" + status_error = "no" + test_attach = "no" + at_options = "--live" + dt_options = "--live" + test_attach_device = "no" + ad_options = "" + dd_options = "" + disk_bus = "virtio" + disk_slot = "0x09" + + variants: + - dirpool: + pool_name = "dirpool" + pool_type = "dir" + pool_target = "/var/lib/libvirt/images/dirpool" + variants: + - vol_qcow2: + vol_name = "vol1.qcow2" + vol_format = "qcow2" + - vol_raw: + vol_name = "vol1.raw" + vol_format = "raw" + - attach_disk_qcow2: + vol_name = "vol1.qcow2" + vol_format = "qcow2" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + - attach_disk_raw: + vol_name = "vol1.raw" + vol_format = "raw" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + + - attach_device_xml_qcow2: + vol_name = "dir-vol1.qcow2" + vol_format = "qcow2" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" + + - attach_device_xml_raw: + vol_name = "dir-vol1.raw" + vol_format = "raw" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" + + - fspool: + pool_name = "fspool" + pool_type = "fs" + pool_source_format = "ext4" + pool_target = "/mnt/fs-pool" + vol_capacity = "20G" + variants: + - vol_qcow2: + vol_name = "fsv1.qcow2" + vol_format = "qcow2" + - vol_raw: + vol_name = "fsv1.raw" + vol_format = "raw" + + - attach_disk_qcow2: + vol_name = "fsv1.qcow2" + vol_format = "qcow2" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + - attach_disk_raw: + vol_name = "fsv1.raw" + vol_format = "raw" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + + - attach_device_xml_qcow2: + vol_name = "fs-vol1.qcow2" + vol_format = "qcow2" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" + + - attach_device_xml_raw: + vol_name = "fs-vol1.raw" + vol_format = "raw" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" + + - defaultpool: + pool_name = "defaultpool" + pool_type = "dir" + pool_target = "/var/lib/libvirt/images/defaultpool" + variants: + - vol_qcow2: + vol_name = "def-vol1.qcow2" + vol_format = "qcow2" + - vol_raw: + vol_name = "def-vol1.raw" + vol_format = "raw" + - attach_disk_qcow2: + vol_name = "def-vol1.qcow2" + vol_format = "qcow2" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + - attach_disk_raw: + vol_name = "def-vol1.raw" + vol_format = "raw" + test_attach = "yes" + start_vm = yes + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdh" + + - attach_device_xml_qcow2: + vol_name = "def-vol1.qcow2" + vol_format = "qcow2" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" + + - attach_device_xml_raw: + vol_name = "def-vol1.raw" + vol_format = "raw" + test_attach_device = "yes" + start_vm = no + vms = avocado-vt-vm1 + main_vm = avocado-vt-vm1 + disk_target = "vdb" + disk_bus = "virtio" + disk_slot = "0x09" diff --git a/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_detach_disk.cfg b/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_detach_disk.cfg index 39e4a5035a0..a3e7ba7d40c 100644 --- a/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_detach_disk.cfg +++ b/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_detach_disk.cfg @@ -113,9 +113,14 @@ at_dt_disk_serial = "test" at_dt_disk_at_options = "--driver qemu --config" at_dt_disk_pre_vm_state = "shut off" + - vm_running_live: + at_dt_disk_at_options = "--driver qemu --subdriver raw --live" + at_dt_disk_dt_options = "--live" + at_dt_disk_detach_after_attach = "yes" - vm_running_config: at_dt_disk_at_options = "--driver qemu --subdriver raw --config" at_dt_disk_dt_options = "--config" + at_dt_disk_detach_after_attach = "yes" - vm_shutdown_persistent: at_dt_disk_at_options = "--driver qemu --subdriver raw --persistent --cache writeback" at_dt_disk_dt_options = "--persistent" @@ -123,6 +128,22 @@ - vm_running_persistent: at_dt_disk_at_options = "--driver qemu --subdriver raw --persistent --cache writethrough" at_dt_disk_dt_options = "--persistent" + at_dt_disk_detach_after_attach = "yes" + - vm_running_live_qcow2: + at_dt_disk_at_options = "--driver qemu --subdriver qcow2 --live" + at_dt_disk_dt_options = "--live" + at_dt_disk_device_source_format = "qcow2" + at_dt_disk_detach_after_attach = "yes" + - vm_running_config_qcow2: + at_dt_disk_at_options = "--driver qemu --subdriver qcow2 --config" + at_dt_disk_dt_options = "--config" + at_dt_disk_device_source_format = "qcow2" + at_dt_disk_detach_after_attach = "yes" + - vm_running_persistent_qcow2: + at_dt_disk_at_options = "--driver qemu --subdriver qcow2 --persistent" + at_dt_disk_dt_options = "--persistent" + at_dt_disk_device_source_format = "qcow2" + at_dt_disk_detach_after_attach = "yes" - twice_diff_target: at_dt_disk_test_twice = 'yes' at_dt_disk_device_target2 = vdx @@ -224,6 +245,10 @@ - file_disk_type: - block_disk_type: at_dt_disk_iscsi_device = "yes" + - block_disk_type_by_id: + at_dt_disk_use_disk_by_id = "yes" + at_dt_disk_skip_iscsi_setup = "yes" + at_dt_disk_detach_after_attach = "yes" - block_disk_type_lun: at_dt_disk_at_options = "--driver qemu --rawio --type lun" at_dt_disk_iscsi_device = "yes" diff --git a/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_device.cfg b/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_device.cfg index 9462d53bfe5..337fab776a7 100644 --- a/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_device.cfg +++ b/libvirt/tests/cfg/virsh_cmd/domain/virsh_attach_device.cfg @@ -46,6 +46,11 @@ variants: - persistent: vadu_extra = "--persistent" + - config: + # Test --config flag on running VM + vadu_config_option = "yes" + # Functional check should fail before rebooting + vadu_preboot_function_error = "yes" - live_config: # Avoid driver-dependant default behavior vadu_extra = "--live" @@ -250,6 +255,66 @@ vadu_dev_obj_alias_Controller = "ua-controller" - block: vadu_dev_objs = "VirtualDiskBasic" + variants: + - block_attach_detach_disk_qcow2: + only single_virtio_file + no cold_attach_hot_vm + no cold_attach_cold_vm + no with_alias + no id_ref + no uuid_ref + no error_test + no file_argument + no domain_argument + no hot_attach_hot_vm_current + #type = virsh_attach_device + #start_vm = "yes" + #status_error = "no" + vadu_preboot_function_error = "no" + vadu_pstboot_function_error = "no" + #vadu_dom_ref = "name" + #vadu_domain_positional = "yes" + #vadu_file_positional = "yes" + vadu_dev_objs = "VirtualDiskQcow2Raw" + vadu_dev_obj_testdetach_VirtualDiskQcow2Raw = "yes" + vadu_dev_obj_alias_VirtualDiskQcow2Raw = "ua-virtualdisk-fmt" + vadu_dev_obj_meg_VirtualDiskQcow2Raw = 100 + vadu_dev_obj_devidx_VirtualDiskQcow2Raw = 1 + vadu_dev_obj_targetbus_VirtualDiskQcow2Raw = "virtio" + vadu_dev_obj_count_VirtualDiskQcow2Raw = 1 + vadu_dev_obj_diskformat_VirtualDiskQcow2Raw = "qcow2" + config: + vadu_preboot_function_error = "yes" + - block_attach_detach_disk_raw: + only single_virtio_file + no cold_attach_hot_vm + no cold_attach_cold_vm + no with_alias + no id_ref + no uuid_ref + no error_test + no file_argument + no domain_argument + no hot_attach_hot_vm_current + #type = virsh_attach_device + #start_vm = "yes" + #status_error = "no" + vadu_preboot_function_error = "no" + vadu_pstboot_function_error = "no" + #vadu_config_option = "no" + #vadu_dom_ref = "name" + #vadu_domain_positional = "yes" + #vadu_file_positional = "yes" + vadu_dev_objs = "VirtualDiskQcow2Raw" + vadu_dev_obj_testdetach_VirtualDiskQcow2Raw = "yes" + vadu_dev_obj_alias_VirtualDiskQcow2Raw = "ua-virtualdisk-fmt" + vadu_dev_obj_meg_VirtualDiskQcow2Raw = 100 + vadu_dev_obj_devidx_VirtualDiskQcow2Raw = 1 + vadu_dev_obj_targetbus_VirtualDiskQcow2Raw = "virtio" + vadu_dev_obj_count_VirtualDiskQcow2Raw = 1 + vadu_dev_obj_diskformat_VirtualDiskQcow2Raw = "raw" + config: + vadu_preboot_function_error = "yes" variants: - without_alias: - with_alias: @@ -284,7 +349,39 @@ # Avoid running out of PCI slots on hot-attach vadu_dev_obj_count_VirtualDiskBasic = 16 vadu_dev_obj_meg_VirtualDiskBasic = 10 - vadu_dev_obj_targetbus_VirtualDiskBasic = "virtio" + - block_attach_detach_disk_formats: + # Test block device attach+detach with qcow2 and raw disk formats + # Uses join to bypass parent variant multiplication + join domain_positional file_positional normal_test name_ref + start_vm = "yes" + status_error = "no" + vadu_preboot_function_error = "no" + vadu_pstboot_function_error = "no" + vadu_config_option = "no" + vadu_dom_ref = "name" + vadu_domain_positional = "yes" + vadu_file_positional = "yes" + vadu_dev_objs = "VirtualDiskQcow2Raw" + vadu_dev_obj_testdetach_VirtualDiskQcow2Raw = "yes" + vadu_dev_obj_alias_VirtualDiskQcow2Raw = "ua-virtualdisk-fmt" + vadu_dev_obj_meg_VirtualDiskQcow2Raw = 100 + vadu_dev_obj_devidx_VirtualDiskQcow2Raw = 1 + vadu_dev_obj_targetbus_VirtualDiskQcow2Raw = "virtio" + vadu_dev_obj_count_VirtualDiskQcow2Raw = 1 + variants disk_format: + - qcow2: + vadu_dev_obj_diskformat_VirtualDiskQcow2Raw = "qcow2" + - raw: + vadu_dev_obj_diskformat_VirtualDiskQcow2Raw = "raw" + variants attach_flags: + - persistent: + vadu_extra = "--persistent" + - config: + vadu_config_option = "yes" + vadu_preboot_function_error = "yes" + - live_config: + vadu_extra = "--live" + vadu_config_option = "yes" - multiple: variants: - VirtualDiskBasic_SerialFile_SerialPipe: diff --git a/libvirt/tests/src/memory/memory_hotplug_virshdump.py b/libvirt/tests/src/memory/memory_hotplug_virshdump.py new file mode 100644 index 00000000000..16c4a844c17 --- /dev/null +++ b/libvirt/tests/src/memory/memory_hotplug_virshdump.py @@ -0,0 +1,289 @@ +import os +import time +import logging +import tempfile +from avocado.utils import process +from virttest import virsh +from virttest import utils_hotplug +from virttest.libvirt_xml import vm_xml +from virttest.libvirt_xml.vm_xml import VMXML, VMCPUXML + + +def run(test, params, env): + """ + Test memory hotplug with simultaneous virsh dump operations. + + :param test: QEMU test object + :param params: Dictionary with the test parameters + :param env: Dictionary with test environment + """ + vm_name = params.get("main_vm") + vm = env.get_vm(vm_name) + + mem_hotplug_iterations = int(params.get("mem_hotplug_iterations", "10")) + virshdump_iterations = int(params.get("virshdump_iterations", "5")) + virshdump_delay = int(params.get("virshdump_delay", "10")) + dump_path = params.get("dump_path", "./virsh_dumps") + dump_options = params.get("dump_options") + + tg_size = params.get("tg_size", "524288") # 512 MiB in KiB + tg_node = params.get("tg_node", "0") + mem_model = params.get("mem_model", "dimm") + + failures = [] + ops_log = "/tmp/memory_hotplug_operations.log" + + def run_iterations(vm_name, iterations, dump_delay, dump_dir): + """ + Both operations must succeed for the iteration to be counted. + If either fails the iteration is logged as an error and the loop stops. + + :param vm_name: libvirt domain name + :param iterations: number of attach+dump cycles to run + :param hotplug_delay: seconds to wait after a successful dump before + starting the next attach-device (mem_hotplug_delay) + :param dump_delay: seconds to wait after attach-device succeeds before + issuing virsh dump (virshdump_delay) + :param dump_dir: directory to write dump files into + """ + if not os.path.exists(dump_dir): + os.makedirs(dump_dir) + + with open(ops_log, "w") as f: + f.write("Operations started at %s\n" % time.strftime("%Y-%m-%d %H:%M:%S")) + f.write("=" * 80 + "\n") + + completed = 0 + for i in range(iterations): + ts = time.strftime("%Y-%m-%d %H:%M:%S") + logging.info("Iteration %d/%d starting at %s", i + 1, iterations, ts) + with open(ops_log, "a") as f: + f.write("\n[Iteration %d/%d] %s\n" % (i + 1, iterations, ts)) + if not virsh.is_alive(vm_name): + msg = " VM '%s' is not running – aborting iterations" % vm_name + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append(msg) + break + + tmp_file = None + attach_ok = False + try: + mem_xml = utils_hotplug.create_mem_xml( + tg_size=int(tg_size), + tg_sizeunit="KiB", + tg_node=int(tg_node), + mem_model=mem_model, + mem_addr={"slot": str(i)} + ) + tmp_file = tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".xml") + tmp_file.write(str(mem_xml)) + tmp_file.close() + + logging.info(" [attach-device] slot %d", i) + attach_result = virsh.attach_device( + vm_name, tmp_file.name, flagstr="--live", debug=True) + + if attach_result.exit_status == 0: + attach_ok = True + logging.info(" [attach-device] SUCCESS") + with open(ops_log, "a") as f: + f.write(" attach-device slot %d: SUCCESS\n" % i) + else: + stderr = attach_result.stderr.strip() + if "domain is not running" in stderr: + msg = " [attach-device] SKIP – domain is not running: %s" % stderr + logging.warning(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + break + else: + msg = " [attach-device] ERROR: %s" % stderr + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append("Iteration %d attach-device failed: %s" % (i + 1, stderr)) + break + except Exception as exc: + msg = " [attach-device] EXCEPTION: %s" % exc + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append("Iteration %d attach-device exception: %s" % (i + 1, exc)) + break + finally: + if tmp_file and os.path.exists(tmp_file.name): + os.unlink(tmp_file.name) + + if not attach_ok: + break + + if not virsh.is_alive(vm_name): + msg = " VM is not running before dump %d – aborting" % (i + 1) + logging.warning(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + break + + dump_file = os.path.join( + dump_dir, "dump_%d_%s" % (i + 1, time.strftime("%Y%m%d_%H%M%S")) + ) + dump_ok = False + try: + logging.info(" [virsh dump] %s", dump_file) + dump_result = virsh.dump(vm_name, dump_file, dump_options, debug=True) + + if dump_result.exit_status == 0: + if os.path.exists(dump_file): + dump_size = os.path.getsize(dump_file) + dump_ok = True + logging.info(" [virsh dump] SUCCESS (%d bytes)", dump_size) + with open(ops_log, "a") as f: + f.write(" virsh dump %d: SUCCESS (%d bytes)\n" % (i + 1, dump_size)) + else: + msg = " [virsh dump] ERROR – command succeeded but file not found" + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append("Iteration %d dump file missing after success" % (i + 1)) + break + else: + stderr = dump_result.stderr.strip() + if "domain is not running" in stderr: + msg = " [virsh dump] SKIP – domain is not running: %s" % stderr + logging.warning(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + break + else: + msg = " [virsh dump] ERROR: %s" % stderr + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append("Iteration %d virsh dump failed: %s" % (i + 1, stderr)) + break + except Exception as exc: + msg = " [virsh dump] EXCEPTION: %s" % exc + logging.error(msg) + with open(ops_log, "a") as f: + f.write(msg + "\n") + failures.append("Iteration %d dump exception: %s" % (i + 1, exc)) + break + + if not dump_ok: + break + + completed += 1 + with open(ops_log, "a") as f: + f.write(" Iteration %d: COMPLETE\n" % (i + 1)) + + with open(ops_log, "a") as f: + f.write("\n" + "=" * 80 + "\n") + f.write("Completed %d/%d iterations at %s\n" + % (completed, iterations, time.strftime("%Y-%m-%d %H:%M:%S"))) + + return completed + + try: + if mem_model == "dimm": + _was_running = vm.is_alive() + if _was_running: + vm.destroy(gracefully=True) + + _vmxml = VMXML.new_from_inactive_dumpxml(vm_name) + + _numa_present = False + try: + _cpuxml = _vmxml.cpu + if _cpuxml is not None and _cpuxml.numa_cell: + _numa_present = True + except Exception: + pass + + if not _numa_present: + logging.info("No NUMA nodes found on VM '%s' – configuring " + "prerequisites for DIMM hotplug", vm_name) + + _mem_kib = int(params.get("mem", "2097152")) + _max_mem_kib = int(params.get("max_mem", "20971520")) + _slots = int(params.get("slots", "40")) + _mem_unit = params.get("mem_unit", "KiB") + _smp = int(params.get("smp", "2")) + _numa_cells = int(params.get("numa_cells", "1")) + + _vmxml.max_mem_rt = _max_mem_kib + _vmxml.max_mem_rt_slots = _slots + _vmxml.max_mem_rt_unit = _mem_unit + _vmxml.max_mem = _mem_kib + _vmxml.current_mem = _mem_kib + + _cpuxml = _vmxml.cpu + if _cpuxml is None: + _cpuxml = VMCPUXML() + _cell_mem = _mem_kib // _numa_cells + _cpu_range = "0-%d" % (_smp - 1) if _smp > 1 else "0" + _cell_dicts = [ + {"id": str(i), "cpus": _cpu_range, + "memory": str(_cell_mem), "unit": _mem_unit} + for i in range(_numa_cells) + ] + _cpuxml.numa_cell = _cpuxml.dicts_to_cells(_cell_dicts) + _vmxml.cpu = _cpuxml + + _vmxml.sync() + logging.info("NUMA configured for VM '%s': %d cell(s), " + "mem=%d %s, maxMem=%d %s, slots=%d", + vm_name, _numa_cells, _mem_kib, _mem_unit, + _max_mem_kib, _mem_unit, _slots) + else: + logging.debug("NUMA already configured for VM '%s' – skipping", + vm_name) + + if _was_running: + vm.start() + + if not vm.is_alive(): + vm.start() + + vm.wait_for_login(timeout=240) + logging.info("VM is running and accessible") + + logging.info("Starting %d sequential iterations " + "(attach-device then virsh dump per cycle)", mem_hotplug_iterations) + completed = run_iterations( + vm_name, + mem_hotplug_iterations, + virshdump_delay, + dump_path + ) + + if os.path.exists(ops_log): + with open(ops_log, "r") as f: + content = f.read() + error_count = content.count("ERROR") + attach_ok = content.count("attach-device slot") - error_count + dump_ok = content.count("virsh dump") - error_count + logging.info("attach-device successes: %d | virsh dump successes: %d " + "| completed iterations: %d/%d", + attach_ok, dump_ok, completed, mem_hotplug_iterations) + else: + failures.append("Operations log not found: %s" % ops_log) + + if completed < mem_hotplug_iterations: + failures.append( + "Only %d/%d iterations completed successfully – check %s" + % (completed, mem_hotplug_iterations, ops_log) + ) + + if failures: + test.fail("Test failed with errors:\n" + "\n".join(failures)) + else: + logging.info("SUCCESS: All %d iterations completed " + "(attach-device + virsh dump each)", completed) + + except Exception as e: + test.error("Test execution failed: %s" % str(e)) + + finally: + logging.info("Cleaning up...") diff --git a/libvirt/tests/src/storage/virsh_pool_vol.py b/libvirt/tests/src/storage/virsh_pool_vol.py new file mode 100644 index 00000000000..962cdb40d8c --- /dev/null +++ b/libvirt/tests/src/storage/virsh_pool_vol.py @@ -0,0 +1,405 @@ +import os +import tempfile +import logging as log + +from avocado.utils import process + +from virttest import virsh +from virttest import libvirt_storage +from virttest.utils_test import libvirt as utlv +from virttest.staging import service + + +# Using as lower capital is not the best way to do, but this is just a +# workaround to avoid changing the entire file. +logging = log.getLogger('avocado.' + __name__) + + +def run(test, params, env): + """ + Test virsh storage pool and volume lifecycle for dir-type pools + (dirpool / fspool / defaultpool) covering two volume formats (qcow2, + raw) and two disk attach/detach methods. + + + :param test: Avocado-VT test object. + :param params: Cartesian-config parameter dictionary. + :param env: Avocado-VT environment object. + + """ + pool_name = params.get("pool_name", "dirpool") + pool_type = params.get("pool_type", "dir") + pool_target = params.get("pool_target", + "/var/lib/libvirt/images/dirpool") + pool_source_format = params.get("pool_source_format", "ext4") + vol_name = params.get("vol_name", "vol1.qcow2") + vol_capacity = params.get("vol_capacity", "10G") + vol_format = params.get("vol_format", "qcow2") + status_error = "yes" == params.get("status_error", "no") + test_attach = "yes" == params.get("test_attach", "no") + test_attach_device = "yes" == params.get("test_attach_device", "no") + disk_target = params.get("disk_target", "vdh") + disk_bus = params.get("disk_bus", "virtio") + disk_slot = params.get("disk_slot", "0x09") + at_options = params.get("at_options", "--live") + dt_options = params.get("dt_options", "--live") + ad_options = params.get("ad_options", "--persistent") + dd_options = params.get("dd_options", "") + vm_name = params.get("main_vm", "avocado-vt-vm1") + vol_path = os.path.join(pool_target, vol_name) + + def check_pool_list(expect_present=True): + """Return False only when pool_name is present but inactive.""" + result = virsh.pool_list("--all", ignore_status=True) + utlv.check_exit_status(result, False) + pool_line = next((line for line in result.stdout.strip().splitlines() + if pool_name in line), None) + found = pool_line is not None + pool_state = None + if pool_line: + pool_cols = pool_line.split() + if len(pool_cols) >= 2: + pool_state = pool_cols[1] + if expect_present and not found: + test.fail("Pool '%s' not found in 'virsh pool-list --all'" + % pool_name) + if expect_present and pool_state == "inactive": + logging.debug("pool-list check: pool='%s' present=%s state=%s (expected=%s)", + pool_name, found, pool_state, expect_present) + return False + if not expect_present and found: + test.fail("Pool '%s' still present in pool-list after cleanup" + % pool_name) + logging.debug("pool-list check: pool='%s' present=%s state=%s (expected=%s)", + pool_name, found, pool_state, expect_present) + return True + + def check_vol_list(expect_present=True): + """Assert vol_name is (or is not) visible in virsh vol-list .""" + result = virsh.vol_list(pool_name, ignore_status=True) + utlv.check_exit_status(result, False) + found = any(vol_name in line + for line in result.stdout.strip().splitlines()) + if expect_present and not found: + test.fail("Volume '%s' not found in pool '%s' vol-list" + % (vol_name, pool_name)) + if not expect_present and found: + test.fail("Volume '%s' still listed in pool '%s' after delete" + % (vol_name, pool_name)) + logging.debug("vol-list check: vol='%s' present=%s (expected=%s)", + vol_name, found, expect_present) + + def check_domblklist(expect_target, expect_source, expect_present=True): + """ + Verify target/source mapping in virsh domblklist output. + + :param expect_target: guest device target e.g. "vdh" + :param expect_source: host image path e.g. /var/.../vol1.qcow2 + :param expect_present: True -> fail if mapping not found + False -> fail if target is still listed + """ + result = virsh.domblklist(vm_name, debug=True) + if result.exit_status: + test.fail("virsh domblklist failed for '%s': %s" + % (vm_name, result.stderr.strip())) + output = result.stdout_text.strip() + logging.debug("domblklist output for %s:\n%s", vm_name, output) + found = False + for line in output.splitlines(): + cols = line.split() + if len(cols) < 2: + continue + if cols[0] == expect_target and cols[1] == expect_source: + found = True + break + if expect_present and not found: + test.fail("Disk target='%s' source='%s' not found in domblklist" + % (expect_target, expect_source)) + if not expect_present and any( + line.split()[0] == expect_target + for line in output.splitlines() if line.split()): + test.fail("Disk target='%s' still present in domblklist after detach" + % expect_target) + logging.debug("domblklist check: target='%s' present=%s (expected=%s)", + expect_target, found, expect_present) + + def build_disk_xml(source_path, driver_type, target_dev, bus, slot): + """ + Write a XML element to a temp file and return its path. + + :param source_path: absolute path to the image file + :param driver_type: 'raw' or 'qcow2' + :param target_dev: guest device name e.g. 'vdb' + :param bus: bus type e.g. 'virtio' + :param slot: PCI slot hex e.g. '0x09' + :returns: path to the temp XML file (caller is responsible for removal) + """ + xml_content = ( + "\n" + " \n" + " \n" + " \n" + "
\n" + "\n" + ).format(driver=driver_type, src=source_path, + dev=target_dev, bus=bus, slot=slot) + fd, xml_path = tempfile.mkstemp(suffix='.xml', prefix='disk_') + try: + os.write(fd, xml_content.encode()) + finally: + os.close(fd) + logging.debug("Disk XML written to %s:\n%s", xml_path, xml_content) + return xml_path + + multipathd = service.Factory.create_service("multipathd") + multipathd_status = multipathd.status() + if multipathd_status: + multipathd.stop() + + _existing = virsh.pool_info(pool_name, ignore_status=True) + if _existing.exit_status == 0: + logging.warning("Pre-flight: pool '%s' already exists — tearing " + "down before test start.", pool_name) + virsh.pool_destroy(pool_name, ignore_status=True) + virsh.pool_undefine(pool_name, ignore_status=True) + + # State flags used by the finally block to decide what needs cleanup. + pool_defined = False + pool_started = False + vol_created = False + fs_source_dev = None + + try: + if not os.path.isdir(pool_target): + logging.info("Creating pool target directory: %s", pool_target) + os.makedirs(pool_target) + pool_define_extra = "" + if pool_type == "fs": + root_src = process.run( + "findmnt -n -o SOURCE /", + shell=True, ignore_status=False).stdout_text.strip() + root_dev_name = os.path.basename(root_src) + root_pkname = process.run( + "lsblk -ndo PKNAME %s" % root_src, + shell=True, ignore_status=True).stdout_text.strip() + root_disk_name = root_pkname or root_dev_name + lsblk_cmd = "lsblk -dn -o NAME,TYPE,MOUNTPOINT,PKNAME" + lsblk_result = process.run(lsblk_cmd, shell=True, ignore_status=False) + raw_disk = None + for line in lsblk_result.stdout_text.strip().splitlines(): + cols = line.split(None, 3) + if len(cols) < 2: + continue + dev_name = cols[0] + dev_type = cols[1] + mountpoint = cols[2] if len(cols) > 2 else "" + parent_name = cols[3] if len(cols) > 3 else "" + if dev_type == "part": + if mountpoint: + continue + if parent_name == root_disk_name: + continue + fs_source_dev = "/dev/%s" % dev_name + break + if dev_type == "disk" and not mountpoint and dev_name != root_disk_name: + raw_disk = "/dev/%s" % dev_name + if not fs_source_dev and raw_disk: + fs_source_dev = raw_disk + if not fs_source_dev: + test.cancel("No safe unused partition or raw disk found for fs pool") + logging.info("Using fs pool source device: %s", fs_source_dev) + utlv.mkfs(fs_source_dev, pool_source_format) + pool_define_extra = "--source-dev %s --source-format %s" % ( + fs_source_dev, pool_source_format) + logging.info("Defining '%s' pool '%s' at '%s'", + pool_type, pool_name, pool_target) + result = virsh.pool_define_as( + pool_name, pool_type, pool_target, + extra=pool_define_extra, + ignore_status=True, debug=True) + utlv.check_exit_status(result, status_error) + if result.exit_status: + return # expected error path + pool_defined = True + _info = virsh.pool_info(pool_name, ignore_status=True) + if _info.exit_status != 0: + test.fail("pool-define-as reported success but pool '%s' is not " + "visible to libvirt (pool-info failed: %s)" + % (pool_name, _info.stderr.strip())) + logging.debug("pool-info confirmed pool '%s' is defined:\n%s", + pool_name, _info.stdout.strip()) + logging.info("Starting pool '%s'", pool_name) + result = virsh.pool_start(pool_name, ignore_status=True, debug=True) + if result.exit_status: + test.fail("virsh pool-start '%s' failed: %s" + % (pool_name, result.stderr.strip())) + pool_started = True + logging.debug("Refreshing pool '%s' after start", pool_name) + virsh.pool_refresh(pool_name, ignore_status=True, debug=True) + logging.info("Enabling autostart for pool '%s'", pool_name) + result = virsh.pool_autostart(pool_name, ignore_status=True, debug=True) + utlv.check_exit_status(result) + check_pool_list(expect_present=True) + logging.info("Creating %s volume '%s' (%s) in pool '%s'", + vol_format, vol_name, vol_capacity, pool_name) + result = virsh.vol_create_as( + vol_name, pool_name, + vol_capacity, "0", vol_format, + ignore_status=True, debug=True) + utlv.check_exit_status(result, status_error) + if result.exit_status: + return # expected error path + vol_created = True + check_pool_list(expect_present=True) + check_vol_list(expect_present=True) + + # Log the host path libvirt resolved for the new volume + vol_path_res = virsh.vol_path( + vol_name, pool_name, ignore_status=True, debug=True) + if not vol_path_res.exit_status: + logging.info("Volume host path: %s", vol_path_res.stdout.strip()) + + if test_attach or test_attach_device: + vm = env.get_vm(vm_name) + if vm is None: + # VM not yet registered in the env (e.g. vms param was empty + # during preprocess). Create the object and register it so + # subsequent env.get_vm() calls also work. + vm_type = params.get("vm_type", "libvirt") + target = params.get("target") + vm = env.create_vm(vm_type, target, vm_name, params, test.bindir) + if vm is None: + test.error("Cannot create VM object for '%s'; check that the " + "libvirt domain exists on the host." % vm_name) + if vm.is_dead(): + logging.info("Starting VM '%s' before attach step", vm_name) + vm.start() + vm.wait_for_login().close() + + if test_attach: + logging.info("Attaching '%s' to guest '%s' as target '%s' " + "with options '%s'", + vol_path, vm_name, disk_target, at_options) + result = virsh.attach_disk( + vm_name, vol_path, disk_target, + extra=at_options, + ignore_status=True, debug=True) + utlv.check_exit_status(result, status_error) + if result.exit_status: + return # expected error path + + check_domblklist(disk_target, vol_path, expect_present=True) + logging.info("Detaching target '%s' from guest '%s' " + "with options '%s'", + disk_target, vm_name, dt_options) + result = virsh.detach_disk( + vm_name, disk_target, + extra=dt_options, + ignore_status=True, debug=True) + utlv.check_exit_status(result) + check_domblklist(disk_target, vol_path, expect_present=False) + if test_attach_device: + xml_file = None + try: + xml_file = build_disk_xml( + vol_path, vol_format, disk_target, disk_bus, disk_slot) + logging.info("attach-device: xml='%s' -> guest '%s' " + "(source='%s', options='%s')", + xml_file, vm_name, vol_path, ad_options) + result = virsh.attach_device( + domainarg=vm_name, + filearg=xml_file, + flagstr=ad_options, + ignore_status=True, debug=True) + utlv.check_exit_status(result, status_error) + if result.exit_status: + return # expected error path + + check_domblklist(disk_target, vol_path, expect_present=True) + _dd_flags = dd_options + logging.info("detach-device: xml='%s' from guest '%s' " + "(target='%s', options='%s')", + xml_file, vm_name, disk_target, _dd_flags) + result = virsh.detach_device( + domainarg=vm_name, + filearg=xml_file, + flagstr=_dd_flags, + ignore_status=True, debug=True) + utlv.check_exit_status(result) + check_domblklist(disk_target, vol_path, expect_present=False) + + finally: + if xml_file and os.path.exists(xml_file): + os.remove(xml_file) + logging.debug("Removed temp XML file: %s", xml_file) + + logging.info("TEST PASSED -- pool='%s' format=%s vol='%s' " + "attach_tested=%s attach_device_tested=%s", + pool_name, vol_format, vol_name, + test_attach, test_attach_device) + + finally: + logging.debug("=== Cleanup start for pool '%s' ===", pool_name) + + if test_attach and vm_name: + _vm = env.get_vm(vm_name) + if _vm is None: + vm_type = params.get("vm_type", "libvirt") + _vm = env.create_vm(vm_type, params.get("target"), + vm_name, params, test.bindir) + if _vm is not None and not _vm.is_dead(): + bl = virsh.domblklist(vm_name, ignore_status=True) + if bl.exit_status == 0: + for _line in bl.stdout_text.strip().splitlines(): + _cols = _line.split() + if _cols and _cols[0] == disk_target: + logging.debug("Cleanup: detaching live target " + "'%s' from '%s'", + disk_target, vm_name) + virsh.detach_disk(vm_name, disk_target, + extra=dt_options, + ignore_status=True, debug=True) + break + + if vol_created: + logging.debug("Deleting volume '%s' from pool '%s'", + vol_name, pool_name) + res = virsh.vol_delete(vol_name, pool_name, + ignore_status=True, debug=True) + if res.exit_status: + logging.warning("vol-delete failed (may already be removed): %s", + res.stderr.strip()) + else: + check_vol_list(expect_present=False) + + # Destroy the pool + if pool_started: + logging.debug("Destroying pool '%s'", pool_name) + if not virsh.pool_destroy(pool_name): + logging.warning("pool-destroy returned non-zero for '%s'", + pool_name) + + if pool_defined: + logging.debug("Undefining pool '%s'", pool_name) + res = virsh.pool_undefine(pool_name, ignore_status=True, debug=True) + if res.exit_status: + logging.warning("pool-undefine failed for '%s': %s", + pool_name, res.stderr.strip()) + else: + check_pool_list(expect_present=False) + + if os.path.isdir(pool_target): + logging.debug("Removing pool target directory: %s", pool_target) + process.run("rm -rf %s" % pool_target, + shell=True, ignore_status=True) + + if pool_type == "fs" and fs_source_dev: + logging.debug("Cleaning filesystem signature on source device: %s", + fs_source_dev) + process.run("wipefs -a %s" % fs_source_dev, + shell=True, ignore_status=True) + if multipathd_status: + multipathd.start() + + logging.debug("Cleanup complete for pool '%s'", pool_name) diff --git a/libvirt/tests/src/virsh_cmd/domain/virsh_attach_detach_disk.py b/libvirt/tests/src/virsh_cmd/domain/virsh_attach_detach_disk.py index 52df542f751..42290e3feb2 100644 --- a/libvirt/tests/src/virsh_cmd/domain/virsh_attach_detach_disk.py +++ b/libvirt/tests/src/virsh_cmd/domain/virsh_attach_detach_disk.py @@ -1,4 +1,5 @@ import os +import re import time import logging as log @@ -157,12 +158,126 @@ def _check_disk(target): utils_misc.wait_for(lambda: _check_disk(target), 10, 3) + def check_domblklist(vm_name, target, source): + """ + Verify target/source mapping in virsh domblklist output. + + :param vm_name: domain name + :param target: disk target in guest xml + :param source: disk source path on host + :return: True if mapping is found + """ + result = virsh.domblklist(vm_name, debug=True) + if result.exit_status != 0: + logging.error("virsh domblklist failed for %s", vm_name) + return False + + output = result.stdout_text.strip() + logging.debug("domblklist output for %s:\n%s", vm_name, output) + for line in output.splitlines(): + columns = line.split() + if len(columns) < 2: + continue + if columns[0] == target and columns[1] == source: + return True + return False + + def check_domblklist_absent(vm_name, target): + """ + Verify target is absent in virsh domblklist output. + + :param vm_name: domain name + :param target: disk target in guest xml + :return: True if target is not found + """ + result = virsh.domblklist(vm_name, debug=True) + if result.exit_status != 0: + logging.error("virsh domblklist failed for %s", vm_name) + return False + + output = result.stdout_text.strip() + logging.debug("domblklist output for %s:\n%s", vm_name, output) + for line in output.splitlines(): + columns = line.split() + if len(columns) < 1: + continue + if columns[0] == target: + return False + return True + + def get_non_root_block_by_id(): + """ + Get a non-root host block device symlink from /dev/disk/by-id. + + The selected device must not back the host root filesystem. + """ + root_source = process.run( + "findmnt -n -o SOURCE /", + shell=True, + ignore_status=False + ).stdout_text.strip() + if not root_source: + test.cancel("Unable to determine host root source device") + + root_source = root_source.split("[", 1)[0].strip() + root_parent_result = process.run( + "lsblk -ndo PKNAME %s" % root_source, + shell=True, + ignore_status=True + ) + root_parent = root_parent_result.stdout_text.strip() + if root_parent: + root_disk = "/dev/%s" % root_parent + else: + root_disk = root_source + + lsblk_cmd = "lsblk -dn -o NAME,TYPE" + lsblk_result = process.run(lsblk_cmd, shell=True, ignore_status=False) + host_disks = [] + for line in lsblk_result.stdout_text.strip().splitlines(): + columns = line.split() + if len(columns) != 2: + continue + disk_name, disk_type = columns + if disk_type != "disk": + continue + disk_path = "/dev/%s" % disk_name + if disk_path == root_disk: + continue + host_disks.append(disk_path) + + if not host_disks: + test.cancel("No non-root host block device available for by-id attach-disk test") + + by_id_dir = "/dev/disk/by-id" + if not os.path.isdir(by_id_dir): + test.cancel("%s does not exist on host" % by_id_dir) + + for entry in sorted(os.listdir(by_id_dir)): + entry_path = os.path.join(by_id_dir, entry) + if not os.path.islink(entry_path): + continue + if re.search(r"-part\d+$", entry): + continue + try: + real_path = os.path.realpath(entry_path) + except OSError as err: + logging.debug("Skip broken by-id entry %s: %s", entry_path, err) + continue + if real_path in host_disks: + logging.info("Selected host by-id disk source %s -> %s", + entry_path, real_path) + return entry_path + + test.cancel("No /dev/disk/by-id symlink found for non-root host block device") + # Get test command. test_cmd = params.get("at_dt_disk_test_cmd", "attach-disk") vm_ref = params.get("at_dt_disk_vm_ref", "name") at_options = params.get("at_dt_disk_at_options", "") dt_options = params.get("at_dt_disk_dt_options", "") + at_options_twice = at_options at_with_shareable = "yes" == params.get("at_with_shareable", 'no') pre_vm_state = params.get("at_dt_disk_pre_vm_state", "running") status_error = "yes" == params.get("status_error", 'no') @@ -194,9 +309,12 @@ def _check_disk(target): test_type = "yes" == params.get("at_dt_disk_check_type", "no") test_audit = "yes" == params.get("at_dt_disk_check_audit", "no") test_block_dev = "yes" == params.get("at_dt_disk_iscsi_device", "no") + skip_iscsi_setup = "yes" == params.get("at_dt_disk_skip_iscsi_setup", "no") test_logcial_dev = "yes" == params.get("at_dt_disk_logical_device", "no") restart_libvirtd = "yes" == params.get("at_dt_disk_restart_libvirtd", "no") detach_disk_with_print_xml = "yes" == params.get("detach_disk_with_print_xml", "no") + use_disk_by_id = "yes" == params.get("at_dt_disk_use_disk_by_id", "no") + detach_after_attach = "yes" == params.get("at_dt_disk_detach_after_attach", "no") vg_name = params.get("at_dt_disk_vg", "vg_test_0") lv_name = params.get("at_dt_disk_lv", "lv_test_0") # Get additional lvm item names. @@ -238,11 +356,19 @@ def _check_disk(target): # Create virtual device file. device_source_path = os.path.join(data_dir.get_data_dir(), device_source_name) - if test_block_dev: - device_source = libvirt.setup_or_cleanup_iscsi(True) - if not device_source: - # We should skip this case - test.cancel("Can not get iscsi device name in host") + if use_disk_by_id: + device_source = get_non_root_block_by_id() + source_path = False + create_img = False + test_block_dev = True + elif test_block_dev: + if skip_iscsi_setup: + device_source = device_source_name + else: + device_source = libvirt.setup_or_cleanup_iscsi(True) + if not device_source: + # We should skip this case + test.cancel("Can not get iscsi device name in host") if test_logcial_dev: if lv_utils.vg_check(vg_name): lv_utils.vg_remove(vg_name) @@ -349,6 +475,8 @@ def _check_disk(target): disk_count_before_cmd = vm_xml.VMXML.get_disk_count(vm_name) # Test. + status = None + device_target2 = device_target domid = vm.get_id() domuuid = vm.get_uuid() @@ -366,9 +494,48 @@ def _check_disk(target): else: vm_ref = "" + detach_status = 0 + check_domblklist_after_attach = True if test_cmd == "attach-disk": status = virsh.attach_disk(vm_ref, device_source, device_target, at_options, debug=True).exit_status + if not status: + + if at_options.count("config") and not at_options.count("live") \ + and pre_vm_state != "shut off": + logging.info("--config attach on running VM: restarting to " + "bring disk live before verification.") + vm.destroy(gracefully=False) + vm.start() + vm.wait_for_login() + # Now the disk is live – verify host-side domblklist. + check_domblklist_after_attach = check_domblklist(vm_name, + device_target, + device_source) + if check_domblklist_after_attach: + logging.info("domblklist confirms '%s' present after " + "restart (--config attach).", device_target) + else: + logging.error("domblklist did NOT show '%s' after " + "restart (--config attach).", device_target) + + else: + # For --live or --persistent, check domblklist immediately + check_domblklist_after_attach = check_domblklist(vm_name, + device_target, + device_source) + if detach_after_attach and not status: + wait_for_disk(vm, device_target) + detach_status = virsh.detach_disk(vm_ref, device_target, dt_options, + debug=True).exit_status + if not detach_status: + if at_options.count("config") and not at_options.count("live") \ + and pre_vm_state != "shut off": + vm.destroy(gracefully=False) + vm.start() + vm.wait_for_login() + + elif test_cmd == "detach-disk": # For detach disk with print-xml option, it only print information,and not actual disk detachment. if detach_disk_with_print_xml and libvirt_version.version_compare(4, 5, 0): @@ -462,12 +629,23 @@ def _check_disk(target): # Recover VM state. if pre_vm_state == "shut off": vm.start() - # Check in VM after command. check_vm_after_cmd = True check_vm_after_cmd = check_vm_partition(vm, device, os_type, device_target) + # Check host side domblklist after command. + check_domblklist_after_cmd = True + check_domblklist_after_detach = True + if test_cmd == "attach-disk": + check_domblklist_after_cmd = check_domblklist_after_attach + if detach_after_attach: + check_domblklist_after_detach = check_domblklist_absent(vm_name, + device_target) + elif test_cmd == "detach-disk": + check_domblklist_after_cmd = check_domblklist_absent(vm_name, + device_target) + # Check disk type after attach. check_disk_type = True if test_type: @@ -552,12 +730,26 @@ def _check_disk(target): inactive_vmxml = vm_xml.VMXML.new_from_inactive_dumpxml(vm_name) disk_count_after_shutdown = len(inactive_vmxml.get_disk_all()) if test_cmd == "attach-disk": - if disk_count_after_shutdown == disk_count_before_cmd: + if detach_after_attach and detach_status == 0: + pass + elif disk_count_after_shutdown == disk_count_before_cmd: check_count_after_shutdown = False elif test_cmd == "detach-disk": if disk_count_after_shutdown < disk_count_before_cmd: check_count_after_shutdown = False + # For --config flag, verify disk appears after VM restart + check_domblklist_after_restart = True + if test_cmd == "attach-disk" and at_options.count("config"): + # Restart VM to verify --config changes take effect + vm.start() + vm.wait_for_login() + check_domblklist_after_restart = check_domblklist(vm_name, + device_target, + device_source) + # Destroy VM after validation + vm.destroy(gracefully=False) + finally: # Recover VM. if vm.is_alive(): @@ -574,7 +766,8 @@ def _check_disk(target): libvirt.delete_local_disk("lvm", vgname=vg_name, lvname=lv_item_name) lv_utils.vg_remove(vg_name) process.run("pvremove %s" % device_source, shell=True, ignore_status=True) - libvirt.setup_or_cleanup_iscsi(False) + if not skip_iscsi_setup: + libvirt.setup_or_cleanup_iscsi(False) else: libvirt.delete_local_disk("file", device_source) @@ -588,7 +781,13 @@ def _check_disk(target): return if status: test.fail("virsh %s failed." % test_cmd) + if detach_after_attach and detach_status: + test.fail("virsh detach-disk failed after attach-disk.") if test_cmd == "attach-disk": + if not check_domblklist_after_cmd: + test.fail("Cannot find attached disk in virsh domblklist output.") + if detach_after_attach and not check_domblklist_after_detach: + test.fail("Detached disk still exists in virsh domblklist output.") if at_options.count("config"): if not check_count_after_shutdown: test.fail("Cannot see config attached device " @@ -601,34 +800,54 @@ def _check_disk(target): test.fail("Address(multifunction) set failed" " after attach") else: - if not check_count_after_cmd: - test.fail("Cannot see device in xml file" - " after attach.") - if not check_vm_after_cmd: - test.fail("Cannot see device in VM after" - " attach.") - if not check_disk_type: - test.fail("Check disk type failed after" - " attach.") - if not check_audit_after_cmd: - test.fail("Audit hotplug failure after attach") - if not check_cache_after_cmd: - test.fail("Check cache failure after attach") - if at_options.count("persistent"): - if not check_count_after_shutdown: - test.fail("Cannot see device attached " - "with persistent after " - "VM shutdown.") - else: - if check_count_after_shutdown: - test.fail("See non-config attached device " - "in xml file after VM shutdown.") + if not detach_after_attach: + if not check_count_after_cmd: + test.fail("Cannot see device in xml file" + " after attach.") + if not check_vm_after_cmd: + test.fail("Cannot see device in VM after" + " attach.") + if not check_domblklist_after_cmd: + test.fail("Cannot verify attached disk in host " + "domblklist output after attach.") + if not check_disk_type: + test.fail("Check disk type failed after" + " attach.") + if not check_audit_after_cmd: + test.fail("Audit hotplug failure after attach") + if not check_cache_after_cmd: + test.fail("Check cache failure after attach") + if at_options.count("persistent"): + if not check_count_after_shutdown: + test.fail("Cannot see device attached " + "with persistent after " + "VM shutdown.") + else: + if check_count_after_shutdown: + test.fail("See non-config attached device " + "in xml file after VM shutdown.") elif test_cmd == "detach-disk": - if dt_options.count("config"): + if dt_options.count("config"): + # Persistent XML must no longer contain the disk. if check_count_after_shutdown: test.fail("See config detached device in " "xml file after VM shutdown.") - else: + # After the VM was restarted (see restart block above), the + # active domain and guest must also not see the disk. + if pre_vm_state != "shut off": + if check_domblklist_after_cmd is False: + test.fail("Disk '%s' still in active domblklist after " + "--config detach-disk and VM restart." + % device_target) + if check_vm_after_cmd: + test.fail("Disk '%s' still visible in guest after " + "--config detach-disk and VM restart." + % device_target) + # if dt_options.count("config"): + # if check_count_after_shutdown: + # test.fail("See config detached device in " + # "xml file after VM shutdown.") + else: if check_count_after_cmd: test.fail("See device in xml file " "after detach.") diff --git a/libvirt/tests/src/virsh_cmd/domain/virsh_attach_device.py b/libvirt/tests/src/virsh_cmd/domain/virsh_attach_device.py index c3288cb62e9..2743a64ec1c 100644 --- a/libvirt/tests/src/virsh_cmd/domain/virsh_attach_device.py +++ b/libvirt/tests/src/virsh_cmd/domain/virsh_attach_device.py @@ -795,6 +795,290 @@ def function(self, index): else: return True +class VirtualDiskQcow2Raw(VirtualDiskBasic): + """ + File-backed virtio/usb/scsi disk device with qcow2 or raw format + + Extends VirtualDiskBasic to support both qcow2 and raw disk formats + using qemu-img for proper disk image creation. + + Consumes Cartesian object parameters: + count - number of devices to make + meg - size of device in megabytes + devidx - device index to start at + targetbus - bus type (virtio, scsi, usb, etc.) + diskformat - disk format (qcow2 or raw), defaults to raw + testdetach - if 'yes', test detach operation after attach (default: 'no') + """ + + identifier = None + diskformat = 'raw' # Default format, can be overridden by config + testdetach = 'no' # Default: don't test detach, can be overridden by config + + def make_image_file_path(self, index): + """Override to use diskformat as the file extension.""" + return os.path.join(data_dir.get_data_dir(), + 'disk_%s_%s_%d.%s' + % (self.__class__.__name__, + self.identifier, + index, + self.diskformat)) + + def make_image_file(self, index): + """Override to create a properly formatted image via create_local_disk.""" + from virttest.utils_test import libvirt as utlv + utlv.create_local_disk("file", + path=self.make_image_file_path(index), + size=str(self.meg / 1024.0), + disk_format=self.diskformat) + + def init_device(self, index): + """ + Initialize and return instance of device xml for index + """ + self.make_image_file(index) + disk_class = self.test_params.vmxml.get_device_class('disk') + disk_device = disk_class(type_name=self.devtype, + virsh_instance=self.test_params.virsh) + + disk_device.driver = {'name': 'qemu', 'type': self.diskformat} + source_properties = {'attrs': + {'file': self.make_image_file_path(index)}} + source = disk_device.new_disk_source(**source_properties) + disk_device.source = source + dev_name = self.devname(index) + disk_device.target = {'dev': dev_name, 'bus': self.targetbus} + if hasattr(self, 'alias') and libvirt_version.version_compare(3, 9, 0): + disk_device.alias = {'name': self.alias + str(index)} + logging.info("Created %s format disk device: %s (target: %s)", + self.diskformat, self.make_image_file_path(index), dev_name) + + return disk_device + + def function(self, index): + """ + Override function() to verify disk functionality then perform detach + + This method: + 1. Verifies disk in domblklist (for storage tests) + 2. Calls parent's function() to verify disk is accessible in guest + 3. Only performs detach AFTER post-boot tests (when self.booted=True) + 4. Returns True if all verifications and detach succeed + + Test flow: + - Pre-boot: booted=False, verify disk in domblklist and works, skip detach + - Post-boot: booted=True, verify disk in domblklist and works, then detach if enabled + """ + # Verify disk appears in domblklist (storage-specific validation) + dev_name = self.devname(index) + vm_name = self.test_params.main_vm.name + + logging.info("Verifying device %s in domblklist (booted=%s)", dev_name, self.booted) + domblklist_result = virsh.domblklist(vm_name, debug=True) + + if domblklist_result.exit_status != 0: + logging.error("Failed to get domblklist for %s", vm_name) + return False + + output = domblklist_result.stdout_text.strip() + logging.debug("domblklist output:\n%s", output) + + device_found = False + for line in output.splitlines(): + columns = line.split() + if len(columns) < 1: + continue + if columns[0] == dev_name: + device_found = True + break + + if not device_found: + logging.error("Device %s NOT found in domblklist", dev_name) + return False + + logging.info("Device %s found in domblklist", dev_name) + + # Now verify the disk functionality using parent class method + function_result = super(VirtualDiskQcow2Raw, self).function(index) + + if not function_result: + logging.error("Disk functionality verification failed for %s format disk", + self.diskformat) + return False + + logging.info("Disk functionality verified successfully (booted=%s)", self.booted) + + # Check if detach testing is enabled for this test variant + testdetach = getattr(self, 'testdetach', 'no') + if testdetach != 'yes': + logging.info("Detach testing disabled for this variant, skipping detach") + return True + + # Only perform detach after post-boot tests (when booted=True) + # This ensures disk is available for both pre-boot and post-boot functionality tests + if not self.booted: + logging.info("Pre-boot phase: skipping detach, will detach after post-boot tests") + return True + + logging.info("Post-boot phase: functionality verified, now testing detach operation") + + # Now perform detach operation with same flags as attach + vadu_dargs = make_vadu_dargs(self.test_params, + self.device_xmls[index].xml, + self.test) + + self.test_params.virsh['debug'] = True + vadu_dargs.update(self.test_params.virsh) + + logging.info("Detaching %s format disk device: %s", + self.diskformat, self.make_image_file_path(index)) + + # Call virsh detach-device with same flags as attach + cmdresult = self.test_params.virsh.detach_device(**vadu_dargs) + self.test_params.virsh['debug'] = False + + # Verify detach command was successful + if cmdresult.exit_status != 0: + if self.test_params.status_error: + return True + else: + logging.error("Failed to detach disk device: %s", cmdresult.stderr) + return False + + # libvirt outputs "Device detached successfully"; exit 0 is the real + # signal but warn if the expected message is unexpectedly absent. + combined = cmdresult.stdout_text + cmdresult.stderr_text + if 'detached successfully' not in combined: + logging.warning("detach-device exited 0 but success message absent; " + "stdout=%s stderr=%s", + cmdresult.stdout_text.strip(), + cmdresult.stderr_text.strip()) + + logging.info("Successfully detached %s format disk device", + self.diskformat) + time.sleep(2) + + # Bug fix: test_params.extra returns None when vadu_extra is unset; + # guard against TypeError before doing '--live' not in None. + extra = self.test_params.extra # str or None + # Check if --config flag is used (without --live) + # For --config, device removal only takes effect after VM restart + config_only = (self.test_params.mmconfig and + (extra is None or '--live' not in extra)) + + dev_name = self.devname(index) + vm_name = self.test_params.main_vm.name + + if config_only: + logging.info("--config flag used for detach: device will be removed after VM restart") + + # For --config, device should still be present in running VM + logging.info("Verifying device %s still present in running VM", dev_name) + domblklist_result = virsh.domblklist(vm_name, debug=True) + + if domblklist_result.exit_status != 0: + logging.error("Failed to get domblklist for %s", vm_name) + return False + + output = domblklist_result.stdout_text.strip() + logging.debug("domblklist output before restart:\n%s", output) + + device_found = False + for line in output.splitlines(): + columns = line.split() + if len(columns) < 1: + continue + if columns[0] == dev_name: + device_found = True + break + + if not device_found: + logging.error("Device %s not found in running VM (should still be present with --config)", + dev_name) + return False + + logging.info("Confirmed: Device %s still present in running VM before restart", dev_name) + + # Now restart the VM to apply --config changes + logging.info("Restarting VM to apply --config detach changes") + try: + vm = self.test_params.main_vm + vm.destroy(gracefully=True) + time.sleep(2) + vm.start() + vm.wait_for_login() + logging.info("VM restarted successfully") + except Exception as e: + logging.error("Failed to restart VM: %s", str(e)) + return False + + time.sleep(2) + + # Verify device is removed from domblklist (after restart for --config) + logging.info("Verifying device %s is removed from domblklist", dev_name) + domblklist_result = virsh.domblklist(vm_name, debug=True) + + if domblklist_result.exit_status != 0: + logging.error("Failed to get domblklist for %s", vm_name) + return False + + output = domblklist_result.stdout_text.strip() + logging.debug("domblklist output after detach%s:\n%s", + " and restart" if config_only else "", output) + + device_found = False + for line in output.splitlines(): + columns = line.split() + if len(columns) < 1: + continue + if columns[0] == dev_name: + device_found = True + break + + if device_found: + logging.error("Device %s still present in domblklist after detach%s", + dev_name, " and restart" if config_only else "") + return False + + logging.info("Verified: Device %s successfully removed from domblklist%s", + dev_name, " after restart" if config_only else "") + + # Guest-side lsblk: confirm the kernel no longer lists the block device. + # Mirror the same pattern used after attach (parent function()) — + # open a session, wait for /dev/ to disappear, then run lsblk + # and assert the device name is absent from its NAME column. + guest_dev = '/dev/' + dev_name + logging.info("Verifying %s absent on guest via lsblk", guest_dev) + session = None + try: + session = self.test_params.main_vm.login() + # Wait up to 10 s for udev to remove the node (mirrors the 5 s + # wait-for-appear used after attach in VirtualDiskBasic.function()) + utils_misc.wait_for( + lambda: session.cmd_status('ls %s' % guest_dev) != 0, 10) + lsblk_out = session.cmd_output( + 'lsblk -o NAME,SIZE,TYPE,MOUNTPOINT 2>/dev/null') + session.close() + logging.info("Guest lsblk output after detach:\n%s", lsblk_out) + # lsblk NAME column contains bare names (e.g. "vdb"), not /dev/ paths + if dev_name in lsblk_out.split(): + logging.error("Guest lsblk still lists %s after detach", dev_name) + return False + logging.info("Guest lsblk confirms %s absent after detach", dev_name) + except (virt_vm.VMAddressError, remote.LoginError, + aexpect.ExpectError, aexpect.ShellError): + try: + session.close() + except AttributeError: + pass # session == None + # Non-fatal: host-side domblklist already confirmed removal + logging.warning("Could not verify lsblk on guest (non-fatal); " + "host domblklist already confirmed removal of %s", + dev_name) + + return True + + def operational_action(test_params, test_devices, operational_results): """ @@ -956,7 +1240,9 @@ def update_controllers_ppc(vm_name, vmxml): device_bus = 'scsi' if not vmxml.get_controllers(device_bus, 'virtio-scsi'): vmxml.del_controller(device_bus) - ppc_controller = Controller('controller') + # Use vmxml.get_device_class to create controller device properly + controller_class = vmxml.get_device_class('controller') + ppc_controller = controller_class(type_name='controller') ppc_controller.type = device_bus ppc_controller.index = '0' ppc_controller.model = 'virtio-scsi'