Add async CPU hotplug harness support so tests can observe the state between device_del and DEVICE_DELETED, and then complete the unplug through the QEMU driver's normal process-event path.
The async path compares the pending live XML, emits DEVICE_DELETED events via the monitor test helper, waits for the driver worker to process them, and validates the vcpu-removed events before running the existing final XML checks. Signed-off-by: Akash Kulhalli <[email protected]> --- tests/qemuhotplugtest.c | 350 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 350 insertions(+) diff --git a/tests/qemuhotplugtest.c b/tests/qemuhotplugtest.c index 7e49b9ad3661..9696448e5b4e 100644 --- a/tests/qemuhotplugtest.c +++ b/tests/qemuhotplugtest.c @@ -19,8 +19,11 @@ #include <config.h> +#include "domain_event.h" #include "qemu/qemu_alias.h" #include "qemu/qemu_conf.h" +#define LIBVIRT_QEMU_DRIVERPRIV_H_ALLOW +#include "qemu/qemu_driverpriv.h" #include "qemu/qemu_hotplug.h" #include "qemumonitortestutils.h" #include "testutils.h" @@ -28,6 +31,9 @@ #include "testutilsqemuschema.h" #include "virhostdev.h" #include "virfile.h" +#include "virthread.h" +#include "virthreadpool.h" +#include "virtime.h" #include "qemufakedrivers.h" #define LIBVIRT_QEMU_CAPSPRIV_H_ALLOW @@ -44,6 +50,7 @@ enum { }; #define QEMU_HOTPLUG_TEST_DOMAIN_ID 7 +#define QEMU_HOTPLUG_TEST_DOMAIN_PID 12345 struct qemuHotplugTestData { const char *domain_filename; @@ -325,6 +332,146 @@ struct testQemuHotplugCpuParams { }; +struct testQemuHotplugCpuAsyncParams { + const char *test; + int newcpus; + const char *cpumap; + virConnectPtr conn; + const char **deviceDeletedAliases; + size_t ndeviceDeletedAliases; + const char *removedVcpus; + const char *arch; + GHashTable *capsLatestFiles; + GHashTable *capsCache; + GHashTable *schemaCache; +}; + + +struct testQemuHotplugCpuAsyncData { + int eventCallbackID; + virMutex lock; + virCond cond; + bool lockInitialized; + bool condInitialized; + virBitmap *removedVcpusExpected; + virBitmap *removedVcpusActual; + bool removedVcpusInvalid; + bool removedVcpusDuplicate; + bool ownsWorkerPool; +}; + + +static int +testQemuHotplugCpuAsyncDataInit(struct testQemuHotplugCpuAsyncData *asyncData, + const char *test) +{ + asyncData->eventCallbackID = -1; + + if (driver.workerPool) { + VIR_TEST_VERBOSE("stale qemu worker pool in async cpu test '%s'", + test); + return -1; + } + + if (virMutexInit(&asyncData->lock) < 0) + return -1; + asyncData->lockInitialized = true; + + if (virCondInit(&asyncData->cond) < 0) + return -1; + asyncData->condInitialized = true; + + if (!(driver.workerPool = virThreadPoolNewFull(0, 1, 0, + qemuProcessEventHandler, + "qemuProcessEventHandler", + NULL, &driver))) + return -1; + + asyncData->ownsWorkerPool = true; + + return 0; +} + + +static void +testQemuHotplugCpuAsyncDataClear(struct testQemuHotplugCpuAsyncData *asyncData, + virConnectPtr conn) +{ + if (asyncData->eventCallbackID >= 0) + virObjectEventStateDeregisterID(conn, driver.domainEventState, + asyncData->eventCallbackID, true); + + if (asyncData->ownsWorkerPool) + g_clear_pointer(&driver.workerPool, virThreadPoolFree); + + if (asyncData->condInitialized) + virCondDestroy(&asyncData->cond); + if (asyncData->lockInitialized) + virMutexDestroy(&asyncData->lock); + + g_clear_pointer(&asyncData->removedVcpusExpected, virBitmapFree); + g_clear_pointer(&asyncData->removedVcpusActual, virBitmapFree); +} + + +static int +testQemuHotplugCpuWaitVcpuRemoved(struct testQemuHotplugCpuAsyncData *asyncData) +{ + g_autofree char *expected = NULL; + g_autofree char *actual = NULL; + unsigned long long now; + unsigned long long deadline; + size_t expectedCount = virBitmapCountBits(asyncData->removedVcpusExpected); + size_t actualCount; + int ret = -1; + + if (virTimeMillisNow(&now) < 0) + return -1; + deadline = now + 1000; + + virMutexLock(&asyncData->lock); + while (virBitmapCountBits(asyncData->removedVcpusActual) < expectedCount) { + if (virCondWaitUntil(&asyncData->cond, &asyncData->lock, deadline) < 0) + break; + } + + actualCount = virBitmapCountBits(asyncData->removedVcpusActual); + + if (asyncData->removedVcpusInvalid) { + VIR_TEST_VERBOSE("async cpu test reported an unexpected vCPU id"); + goto cleanup; + } + + if (asyncData->removedVcpusDuplicate) { + VIR_TEST_VERBOSE("async cpu test received duplicate vcpu-removed events"); + goto cleanup; + } + + if (actualCount < expectedCount) { + VIR_TEST_VERBOSE("timed out waiting for vcpu-removed events" + " (%zu/%zu received)", + actualCount, expectedCount); + goto cleanup; + } + + if (!virBitmapEqual(asyncData->removedVcpusExpected, + asyncData->removedVcpusActual)) { + expected = virBitmapFormat(asyncData->removedVcpusExpected); + actual = virBitmapFormat(asyncData->removedVcpusActual); + VIR_TEST_VERBOSE("expected removed vCPUs '%s', got '%s'", + NULLSTR(expected), NULLSTR(actual)); + goto cleanup; + } + + ret = 0; + + cleanup: + virMutexUnlock(&asyncData->lock); + + return ret; +} + + static struct testQemuHotplugCpuData * testQemuHotplugCpuPrepare(const struct testQemuHotplugCpuParams *params) { @@ -407,6 +554,124 @@ testQemuHotplugCpuFinalize(struct testQemuHotplugCpuData *data) } +static void +testQemuHotplugCpuVcpuRemoved(virConnectPtr conn G_GNUC_UNUSED, + virDomainPtr dom G_GNUC_UNUSED, + unsigned int vcpuid, + void *opaque) +{ + struct testQemuHotplugCpuAsyncData *asyncData = opaque; + + virMutexLock(&asyncData->lock); + + if (virBitmapIsBitSet(asyncData->removedVcpusActual, vcpuid)) + asyncData->removedVcpusDuplicate = true; + + if (virBitmapSetBit(asyncData->removedVcpusActual, vcpuid) < 0) + asyncData->removedVcpusInvalid = true; + + virCondBroadcast(&asyncData->cond); + virMutexUnlock(&asyncData->lock); +} + + +static int +testQemuHotplugCpuRegisterVcpuRemoved(struct testQemuHotplugCpuData *data, + const struct testQemuHotplugCpuAsyncParams *async, + struct testQemuHotplugCpuAsyncData *asyncData) +{ + g_autoptr(virBitmap) expected = NULL; + size_t maxvcpus = virDomainDefGetVcpusMax(data->vm->def); + + if (!async->removedVcpus) { + VIR_TEST_VERBOSE("async cpu test '%s' is missing expected removed vCPUs", + async->test); + return -1; + } + + if (virBitmapParse(async->removedVcpus, &expected, maxvcpus) < 0) + return -1; + + if (!(asyncData->removedVcpusActual = virBitmapNew(maxvcpus))) + return -1; + + asyncData->removedVcpusExpected = g_steal_pointer(&expected); + + if (virDomainEventStateRegisterID(async->conn, + driver.domainEventState, + NULL, + VIR_DOMAIN_EVENT_ID_VCPU_REMOVED, + VIR_DOMAIN_EVENT_CALLBACK(testQemuHotplugCpuVcpuRemoved), + asyncData, + NULL, + &asyncData->eventCallbackID) < 0) + return -1; + + return 0; +} + + +static int +testQemuHotplugCpuCompleteAsync(struct testQemuHotplugCpuData *data, + const struct testQemuHotplugCpuAsyncParams *async, + struct testQemuHotplugCpuAsyncData *asyncData) +{ + g_autofree char *activeXML = NULL; + g_autofree char *pendingLiveFile = NULL; + size_t i; + bool vmUnlocked = false; + int ret = -1; + + if (!async->deviceDeletedAliases || + async->ndeviceDeletedAliases == 0) { + VIR_TEST_VERBOSE("async cpu test '%s' is missing DEVICE_DELETED aliases", + async->test); + return -1; + } + + if (!(activeXML = virDomainDefFormat(data->vm->def, driver.xmlopt, + VIR_DOMAIN_DEF_FORMAT_SECURE))) + return -1; + + pendingLiveFile = g_strdup_printf("%s/qemuhotplugtestcpus/%s-result-pending-live.xml", + abs_srcdir, async->test); + + if (virTestCompareToFile(activeXML, pendingLiveFile) < 0) + return -1; + + if (testQemuHotplugCpuRegisterVcpuRemoved(data, async, asyncData) < 0) + return -1; + + /* qemuDomainRefreshVcpuInfo() validates vCPU TIDs against the emulator + * PID. A running QEMU process always has a non-zero PID, so mirror that + * before DEVICE_DELETED processing reports unplugged vCPUs with tid == 0. */ + data->vm->pid = QEMU_HOTPLUG_TEST_DOMAIN_PID; + + virObjectUnlock(data->vm); + vmUnlocked = true; + + for (i = 0; i < async->ndeviceDeletedAliases; i++) + qemuMonitorTestEmitDeviceDeleted(data->mon, + async->deviceDeletedAliases[i]); + + if (testQemuHotplugCpuWaitVcpuRemoved(asyncData) < 0) + goto cleanup; + + virThreadPoolDrain(driver.workerPool); + + virObjectLock(data->vm); + vmUnlocked = false; + + ret = 0; + + cleanup: + if (vmUnlocked) + virObjectLock(data->vm); + + return ret; +} + + static int testQemuHotplugCpuGroup(const void *opaque) { @@ -442,6 +707,71 @@ testQemuHotplugCpuGroup(const void *opaque) } +static int +testQemuHotplugCpuAsync(const struct testQemuHotplugCpuAsyncParams *async, + bool group) +{ + struct testQemuHotplugCpuData *data = NULL; + struct testQemuHotplugCpuAsyncData asyncData = { .eventCallbackID = -1 }; + struct testQemuHotplugCpuParams params = { + .test = async->test, + .arch = async->arch, + .capsLatestFiles = async->capsLatestFiles, + .capsCache = async->capsCache, + .schemaCache = async->schemaCache, + }; + g_autoptr(virBitmap) map = NULL; + int ret = -1; + int rc; + + if (!(data = testQemuHotplugCpuPrepare(¶ms))) + return -1; + + if (testQemuHotplugCpuAsyncDataInit(&asyncData, async->test) < 0) + goto cleanup; + + if (group) { + rc = qemuDomainSetVcpusInternal(&driver, data->vm, data->vm->def, + data->vm->newDef, async->newcpus, + true, true); + } else { + if (virBitmapParse(async->cpumap, &map, 128) < 0) + goto cleanup; + + rc = qemuDomainSetVcpuInternal(&driver, data->vm, data->vm->def, + data->vm->newDef, map, false, + true); + } + + if (rc < 0) + goto cleanup; + + if (testQemuHotplugCpuCompleteAsync(data, async, &asyncData) < 0) + goto cleanup; + + ret = testQemuHotplugCpuFinalize(data); + + cleanup: + testQemuHotplugCpuAsyncDataClear(&asyncData, async->conn); + testQemuHotplugCpuDataFree(data); + return ret; +} + + +static int +testQemuHotplugCpuGroupAsync(const void *opaque) +{ + return testQemuHotplugCpuAsync(opaque, true); +} + + +static int +testQemuHotplugCpuIndividualAsync(const void *opaque) +{ + return testQemuHotplugCpuAsync(opaque, false); +} + + static int testQemuHotplugCpuIndividual(const void *opaque) { @@ -818,6 +1148,26 @@ mymain(void) ret = -1; \ } while (0) +#define DO_TEST_CPU_INDIVIDUAL_ASYNC(archname, prefix, mapstr, alias, removed) \ + do { \ + const char *aliases[] = { alias }; \ + const struct testQemuHotplugCpuAsyncParams async = { \ + .test = prefix, \ + .cpumap = mapstr, \ + .conn = conn, \ + .deviceDeletedAliases = aliases, \ + .ndeviceDeletedAliases = G_N_ELEMENTS(aliases), \ + .removedVcpus = removed, \ + .arch = archname, \ + .capsLatestFiles = capsLatestFiles, \ + .capsCache = capsCache, \ + .schemaCache = schemaCache, \ + }; \ + if (virTestRun("hotplug vcpus individual async " prefix, \ + testQemuHotplugCpuIndividualAsync, &async) < 0) \ + ret = -1; \ + } while (0) + DO_TEST_CPU_INDIVIDUAL("x86_64", "x86-modern-individual-add", "7", true, false); DO_TEST_CPU_INDIVIDUAL("x86_64", "x86-modern-individual-add", "6,7", true, true); DO_TEST_CPU_INDIVIDUAL("x86_64", "x86-modern-individual-add", "7", false, true); -- 2.47.3
