From cc58f34f6df3dea0b7901a1db236f4a7de05c600 Mon Sep 17 00:00:00 2001 From: Anushree-Mathur Date: Tue, 11 Aug 2026 23:13:50 +0530 Subject: [PATCH] Add test CPU hotplug/unplug with explicit sockets/cores/threads topology! What the test does: 1. Defines the VM with an explicit CPU topology (topology_sockets/cores/threads) and a capped vcpu_max_num before the VM starts, instead of relying on set_vm_vcpus() topology correction. 2. If numa=yes is also set, redistributes the pre-existing NUMA cells to match the new vcpu_max_num: CPU ranges are recalculated (cpus_per_cell = vcpu_max / num_cells) and each cell receives an equal share of the total guest memory (cell_memory_kib = mem_MiB * 1024 / num_cells). 3. Starts the VM and verifies the initial vcpu count matches vcpu_current_num. 4. Hotplugs vCPUs live (virsh setvcpus --live) to vcpu_plug_num, waits for the new CPUs to come online in the guest, and verifies the count. 5. Unplugs back to vcpu_unplug_num (first bumps to vcpu_max_num to ensure all added vCPUs are hotpluggable, then reduces), and verifies the final count. Steps 3-5 are repeated test_itr times (default 1). libvirt/tests/cfg/libvirt_vcpu_plug_unplug.cfg: Add with_topology variant with three sub-variants for ppc64le/ppc64: - threads_2: sockets=1, cores=4, threads=2, vcpu_max=8 - threads_4: sockets=1, cores=2, threads=4, vcpu_max=8 - threads_8: sockets=1, cores=2, threads=8, vcpu_max=16 Parameters match subtests.cfg authoritative definitions exactly. libvirt/tests/src/libvirt_vcpu_plug_unplug.py: Read topology_required/topology_sockets/cores/threads params. When topology_required=yes, set vcpu/current_vcpu directly and inject into the element via VMCPUXML, then sync. When numa=yes is also set, re-read the inactive XML, recalculate all NUMA cell CPU ranges and per-cell memory, and sync again. This avoids the stale per-cell copy bug (inflating total guest RAM by num_cells x) that caused QEMU OOM on start. All non-topology variants reach set_vm_vcpus() unchanged via the else branch. Signed-off-by: Anushree-Mathur Signed-off-by: Anushree-Mathur --- .../tests/cfg/libvirt_vcpu_plug_unplug.cfg | 29 +++++++ libvirt/tests/src/libvirt_vcpu_plug_unplug.py | 79 ++++++++++++++++++- 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/libvirt/tests/cfg/libvirt_vcpu_plug_unplug.cfg b/libvirt/tests/cfg/libvirt_vcpu_plug_unplug.cfg index c16e88d4bb3..1f73333f537 100644 --- a/libvirt/tests/cfg/libvirt_vcpu_plug_unplug.cfg +++ b/libvirt/tests/cfg/libvirt_vcpu_plug_unplug.cfg @@ -94,6 +94,35 @@ vcpu_plug_num = 240 vcpu_unplug_num = 1 vcpu_max_num = 240 + - with_topology: + only live + only ppc64le,ppc64 + topology_correction = "no" + topology_required = "yes" + topology_sockets = "1" + test_itr = 1 + variants: + - threads_2: + topology_threads = "2" + topology_cores = "4" + vcpu_max_num = "8" + vcpu_current_num = "2" + vcpu_plug_num = "8" + vcpu_unplug_num = "2" + - threads_4: + topology_threads = "4" + topology_cores = "2" + vcpu_max_num = "8" + vcpu_current_num = "4" + vcpu_plug_num = "8" + vcpu_unplug_num = "4" + - threads_8: + topology_threads = "8" + topology_cores = "2" + vcpu_max_num = "16" + vcpu_current_num = "8" + vcpu_plug_num = "16" + vcpu_unplug_num = "8" variants: - live: diff --git a/libvirt/tests/src/libvirt_vcpu_plug_unplug.py b/libvirt/tests/src/libvirt_vcpu_plug_unplug.py index d6404bd72f8..70b03b149cf 100644 --- a/libvirt/tests/src/libvirt_vcpu_plug_unplug.py +++ b/libvirt/tests/src/libvirt_vcpu_plug_unplug.py @@ -4,6 +4,7 @@ import time from avocado.utils import cpu as cpu_util +from virttest.libvirt_xml.vm_xml import VMCPUXML from virttest import virsh from virttest import data_dir @@ -240,6 +241,10 @@ def check_setvcpus_result(cmd_result, expect_error): with_stress = "yes" == params.get("run_stress", "no") iterations = int(params.get("test_itr", 1)) topology_correction = "yes" == params.get("topology_correction", "no") + topology_diff = "yes" == params.get("topology_required", "no") + topology_threads = params.get("topology_threads") + topology_cores = params.get("topology_cores") + topology_sockets = params.get("topology_sockets") # Init expect vcpu count values expect_vcpu_num = {'max_config': vcpu_max_num, 'max_live': vcpu_max_num, 'cur_config': vcpu_current_num, @@ -300,8 +305,78 @@ def check_setvcpus_result(cmd_result, expect_error): vmxml.remove_agent_channels() vmxml.sync() - vmxml.set_vm_vcpus(vm_name, vcpu_max_num, vcpu_current_num, - topology_correction=topology_correction) + if topology_diff: + # Set explicit CPU topology (sockets/cores/threads) then + # redistribute any existing NUMA cells to match the new vcpu count. + vmxml.vcpu = vcpu_max_num + vmxml.current_vcpu = vcpu_current_num + + try: + cpu_xml = vmxml.cpu + except Exception: + cpu_xml = VMCPUXML() + + topology = {} + if topology_sockets: + topology['sockets'] = int(topology_sockets) + if topology_cores: + topology['cores'] = int(topology_cores) + if topology_threads: + topology['threads'] = int(topology_threads) + if topology: + cpu_xml.topology = topology + vmxml.cpu = cpu_xml + logging.info("CPU topology set: %s", topology) + + vmxml.sync() + + # Redistribute NUMA cells to match the new vcpu count + numa_enabled = params.get("numa") == "yes" + if numa_enabled: + logging.info("Fixing NUMA cells to match topology") + vmxml = VMXML.new_from_inactive_dumpxml(vm_name) + try: + cpu_xml = vmxml.cpu + if hasattr(cpu_xml, 'numa_cell') and cpu_xml.numa_cell: + num_cells = len(cpu_xml.numa_cell) + cpus_per_cell = vcpu_max_num // num_cells + remaining = vcpu_max_num % num_cells + # Divide total guest memory equally — do NOT copy the + # stale per-cell value which would multiply total RAM + # by num_cells. + total_mem_mib = int(params.get("mem", "16384")) + cell_memory_kib = (total_mem_mib * 1024) // num_cells + + new_cells = [] + cpu_start = 0 + for i in range(num_cells): + cell_cpus = cpus_per_cell + (1 if i < remaining else 0) + cpu_end = cpu_start + cell_cpus - 1 + cell_dict = { + 'id': str(i), + 'cpus': ("%d-%d" % (cpu_start, cpu_end) + if cell_cpus > 1 else str(cpu_start)), + 'memory': str(cell_memory_kib), + 'unit': 'KiB', + } + new_cells.append(cell_dict) + logging.info("NUMA cell %d: CPUs %s, Memory %d KiB", + i, cell_dict['cpus'], cell_memory_kib) + cpu_start = cpu_end + 1 + + cpu_xml.numa_cell = cpu_xml.dicts_to_cells(new_cells) + vmxml.cpu = cpu_xml + vmxml.sync() + logging.info("NUMA cells redistributed successfully") + else: + test.fail("NUMA enabled but no NUMA cells found " + "after topology sync") + except Exception as e: + test.fail("Failed to redistribute NUMA cells: %s" % str(e)) + else: + vmxml.set_vm_vcpus(vm_name, vcpu_max_num, vcpu_current_num, + topology_correction=topology_correction) + vm.start() vm_uptime_init = vm.uptime() if with_stress: