[PATCH 0/7] virsh: Fix cleanup in 'event' command and introduce 'await' command

'virsh await' is a helper/syntax-sugar command which allows you to wait until a domain reaches a certain state. Currently this series implements conditions named 'domain-inactive' and 'guest-agent-available'. Peter Krempa (7): virthread: Register auto cleanup function for virMutex virthread: Add infrastructure for static virCond definitions virsh: cmdEvent: Ensure that event callbacks are unregistered before returning vshCommandRun: Convert to directly return the exit code vsh: Add support for commands with more return values virsh: Introduce 'await' command for waiting until target domain state is reached virsh: await: Introduce 'guest-agent-available' condition docs/manpages/virsh.rst | 30 ++++ src/util/virthread.h | 7 + tools/virsh-domain-event.c | 321 ++++++++++++++++++++++++++++++++++++- tools/virsh.c | 4 +- tools/virt-admin.c | 4 +- tools/vsh.c | 46 ++++-- tools/vsh.h | 3 +- 7 files changed, 398 insertions(+), 17 deletions(-) -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> This makes it very convenient to declare a mutex on stack along with the VIR_MUTEX_INITIALIZE macro without the need to have complex cleanup. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- src/util/virthread.h | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/virthread.h b/src/util/virthread.h index 23abe0b6c9..a7ec6bf32c 100644 --- a/src/util/virthread.h +++ b/src/util/virthread.h @@ -121,6 +121,7 @@ int virOnce(virOnceControl *once, virOnceFunc init) int virMutexInit(virMutex *m) G_GNUC_WARN_UNUSED_RESULT; int virMutexInitRecursive(virMutex *m) G_GNUC_WARN_UNUSED_RESULT; void virMutexDestroy(virMutex *m); +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(virMutex, virMutexDestroy); void virMutexLock(virMutex *m); void virMutexUnlock(virMutex *m); -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> Add the automatic cleanup handler as well as static initializer to allow simple stack allocated conditions. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- src/util/virthread.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/util/virthread.h b/src/util/virthread.h index a7ec6bf32c..9e4e8cdafe 100644 --- a/src/util/virthread.h +++ b/src/util/virthread.h @@ -73,6 +73,11 @@ struct virOnceControl { .once = PTHREAD_ONCE_INIT \ } +#define VIR_COND_INITIALIZER \ + { \ + .cond = PTHREAD_COND_INITIALIZER \ + } + typedef void (*virOnceFunc)(void); typedef void (*virThreadFunc)(void *opaque); @@ -142,6 +147,7 @@ void virRWLockUnlock(virRWLock *m); int virCondInit(virCond *c) G_GNUC_WARN_UNUSED_RESULT; int virCondDestroy(virCond *c); +G_DEFINE_AUTO_CLEANUP_CLEAR_FUNC(virCond, virCondDestroy); /* virCondWait, virCondWaitUntil: * These functions can return without the associated predicate -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> Successful return from 'virConnectDomainEventDeregisterAny' does not guarantee that there aren't still in-progress events being handled by the callbacks. Since 'cmdEvent' passes in a slice from an array as the private data of the callbacks, we must ensure that the array stays in scope (it's auto-freed) for the whole time there are possible callbacks being executed. While in practice this doesn't happen as the callbacks are usually quick enough to finish while unregistering stuff, placing a 'sleep(1)' into e.g. 'virshEventLifecyclePrint' and starting a domain results in crash of virsh with the following backtrace: Program terminated with signal SIGSEGV, Segmentation fault. #0 0x00005557b5cfd343 in virshEventPrintf (data=data@entry=0x5557db9619b0, fmt=fmt@entry=0x5557b5d5e527 "%s") at ../../../libvirt/tools/virsh-domain-event.c:252 Thread 2 (Thread 0x7f59a54b7d00 (LWP 2097121)): #0 0x00007f59a6cadbf9 in __futex_abstimed_wait_common () at /lib64/libc.so.6 #1 0x00007f59a6cb2cf3 in __pthread_clockjoin_ex () at /lib64/libc.so.6 #2 0x00005557b5cd57f6 in virshDeinit (ctl=0x7ffc7b615140) at ../../../libvirt/tools/virsh.c:408 #3 0x00005557b5cd5391 in main (argc=<optimized out>, argv=<optimized out>) at ../../../libvirt/tools/virsh.c:932 Thread 1 (Thread 0x7f59a51a66c0 (LWP 2097122)): #0 0x00005557b5cfd343 in virshEventPrintf (data=data@entry=0x5557db9619b0, fmt=fmt@entry=0x5557b5d5e527 "%s") at ../../../libvirt/tools/virsh-domain-event.c:252 #1 0x00005557b5cffa10 in virshEventPrint (data=0x5557db9619b0, buf=0x7f59a51a55c0) at ../../../libvirt/tools/virsh-domain-event.c:290 #2 virshEventLifecyclePrint (conn=<optimized out>, dom=<optimized out>, event=<optimized out>, detail=<optimized out>, opaque=0x5557db9619b0) at ../../../libvirt/ [snipped] From the backtrace you can see that the 'main()' thread is already shutting down virsh, which means that 'cmdEvent' terminated and the private data was freed. The event loop thread is still execing the callback which accesses the data. To fix this add a condition and wait on all of the callbacks to be unregistered first (their private data freeing function will be called). This bug was observed when I've copied the event code for a new virsh command which had a bit more involved callbacks. Fixes: 99fa96c3907 Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- tools/virsh-domain-event.c | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/tools/virsh-domain-event.c b/tools/virsh-domain-event.c index 69a68d857d..9aa21b2e78 100644 --- a/tools/virsh-domain-event.c +++ b/tools/virsh-domain-event.c @@ -237,10 +237,23 @@ struct virshDomEventData { bool timestamp; virshDomainEventCallback *cb; int id; + + virMutex *m; /* needed to signal that handler was unregistered for clean shutdown */ + virCond *c; }; typedef struct virshDomEventData virshDomEventData; +static void +virshDomEventDataUnregistered(virshDomEventData *d) +{ + g_auto(virLockGuard) name = virLockGuardLock(d->m); + /* signal that the handler was unregistered */ + d->id = -1; + virCondSignal(d->c); +} + + static void G_GNUC_PRINTF(2, 3) virshEventPrintf(virshDomEventData *data, const char *fmt, @@ -936,6 +949,8 @@ cmdEvent(vshControl *ctl, const vshCmd *cmd) bool timestamp = vshCommandOptBool(cmd, "timestamp"); int count = 0; virshControl *priv = ctl->privData; + g_auto(virMutex) m = VIR_MUTEX_INITIALIZER; + g_auto(virCond) c = VIR_COND_INITIALIZER; VSH_EXCLUSIVE_OPTIONS("all", "event"); VSH_EXCLUSIVE_OPTIONS("list", "all"); @@ -969,6 +984,8 @@ cmdEvent(vshControl *ctl, const vshCmd *cmd) data[ndata].count = &count; data[ndata].timestamp = timestamp; data[ndata].cb = &virshDomainEventCallbacks[i]; + data[ndata].m = &m; + data[ndata].c = &c; data[ndata].id = -1; ndata++; } @@ -994,7 +1011,7 @@ cmdEvent(vshControl *ctl, const vshCmd *cmd) data[i].event, data[i].cb->cb, &data[i], - NULL)) < 0) { + (virFreeCallback) virshDomEventDataUnregistered)) < 0) { /* When registering for all events: if the first * registration succeeds, silently ignore failures on all * later registrations on the assumption that the server @@ -1022,14 +1039,27 @@ cmdEvent(vshControl *ctl, const vshCmd *cmd) ret = true; cleanup: - vshEventCleanup(ctl); if (data) { for (i = 0; i < ndata; i++) { if (data[i].id >= 0 && virConnectDomainEventDeregisterAny(priv->conn, data[i].id) < 0) ret = false; } + + virMutexLock(&m); + while (true) { + for (i = 0; i < ndata; i++) { + if (data[i].id >= 0) + break; + } + + if (i == ndata || + virCondWait(&c, &m) < 0) + break; + } + virMutexUnlock(&m); } + vshEventCleanup(ctl); return ret; } -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> Currently the handler functions in the virt shells return only a boolean signalling if the command was successful or not. In preparation for a command which will want to return another value (timeout) convert vshCommand run to actually return the requested exit code instead and document the conversion from boolean. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- tools/virsh.c | 4 ++-- tools/virt-admin.c | 4 ++-- tools/vsh.c | 29 ++++++++++++++++++++--------- tools/vsh.h | 2 +- 4 files changed, 25 insertions(+), 14 deletions(-) diff --git a/tools/virsh.c b/tools/virsh.c index 244ca655ee..643ef6b453 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -836,7 +836,7 @@ main(int argc, char **argv) vshControl _ctl = { 0 }; vshControl *ctl = &_ctl; virshControl virshCtl = { 0 }; - bool ret = true; + int ret = EXIT_SUCCESS; ctl->name = "virsh"; /* hardcoded name of the binary */ ctl->env_prefix = "VIRSH"; @@ -930,5 +930,5 @@ main(int argc, char **argv) } virshDeinit(ctl); - exit(ret ? EXIT_SUCCESS : EXIT_FAILURE); + exit(ret); } diff --git a/tools/virt-admin.c b/tools/virt-admin.c index b701ed1fe4..ac8f3202b6 100644 --- a/tools/virt-admin.c +++ b/tools/virt-admin.c @@ -1530,7 +1530,7 @@ main(int argc, char **argv) vshControl _ctl = { 0 }; vshControl *ctl = &_ctl; vshAdmControl virtAdminCtl = { 0 }; - bool ret = true; + int ret = EXIT_SUCCESS; ctl->name = "virt-admin"; /* hardcoded name of the binary */ ctl->env_prefix = "VIRT_ADMIN"; @@ -1612,5 +1612,5 @@ main(int argc, char **argv) } vshAdmDeinit(ctl); - exit(ret ? EXIT_SUCCESS : EXIT_FAILURE); + exit(ret); } diff --git a/tools/vsh.c b/tools/vsh.c index e892cbca22..497b7ec631 100644 --- a/tools/vsh.c +++ b/tools/vsh.c @@ -1341,14 +1341,22 @@ vshBlockJobOptionBandwidth(vshControl *ctl, } -/* - * Executes command(s) and returns return code from last command +/** + * vshCommandRun: + * @ctl: virt shell data + * @cmd: command to execute + * + * Returns return code from last command. Return values from command handlers + * which return boolean are converted as: + * true -> EXIT_SUCCESS + * false -> EXIT_FAILURE */ -bool -vshCommandRun(vshControl *ctl, const vshCmd *cmd) +int +vshCommandRun(vshControl *ctl, + const vshCmd *cmd) { const vshClientHooks *hooks = ctl->hooks; - bool ret = true; + int ret = EXIT_SUCCESS; while (cmd) { gint64 before, after; @@ -1358,16 +1366,19 @@ vshCommandRun(vshControl *ctl, const vshCmd *cmd) if ((cmd->def->flags & VSH_CMD_FLAG_NOCONNECT) || (hooks && hooks->connHandler && hooks->connHandler(ctl))) { - ret = cmd->def->handler(ctl, cmd); + if (cmd->def->handler(ctl, cmd)) + ret = EXIT_SUCCESS; + else + ret = EXIT_FAILURE; } else { /* connection is not usable, return error */ - ret = false; + ret = EXIT_FAILURE; } after = g_get_real_time(); /* try to automatically catch disconnections */ - if (!ret && + if (ret != EXIT_SUCCESS && ((last_error != NULL) && (((last_error->code == VIR_ERR_SYSTEM_ERROR) && (last_error->domain == VIR_FROM_REMOTE)) || @@ -1376,7 +1387,7 @@ vshCommandRun(vshControl *ctl, const vshCmd *cmd) (last_error->code == VIR_ERR_INVALID_CONN)))) disconnected++; - if (!ret) + if (ret != EXIT_SUCCESS) vshReportError(ctl); if (STREQ(cmd->def->name, "quit") || diff --git a/tools/vsh.h b/tools/vsh.h index 3b75216e11..284da36e32 100644 --- a/tools/vsh.h +++ b/tools/vsh.h @@ -287,7 +287,7 @@ int vshBlockJobOptionBandwidth(vshControl *ctl, bool bytes, unsigned long *bandwidth); bool vshCommandOptBool(const vshCmd *cmd, const char *name); -bool vshCommandRun(vshControl *ctl, const vshCmd *cmd); +int vshCommandRun(vshControl *ctl, const vshCmd *cmd); bool vshCommandStringParse(vshControl *ctl, char *cmdstr, vshCmd **partial); -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> Add a new handler callback for command handlers which will want to return more than just EXIT_SUCCESS/EXIT_FAILURE. The new handler allows returning integers. Any negative values are converted to EXIT_FAILURE, other values are returned as reported in cases where we forward the command state (non-interactive usage) as return value of the virt shell program. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- tools/vsh.c | 25 ++++++++++++++++++++----- tools/vsh.h | 1 + 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/tools/vsh.c b/tools/vsh.c index 497b7ec631..4aacc5feac 100644 --- a/tools/vsh.c +++ b/tools/vsh.c @@ -293,7 +293,7 @@ vshCmddefCheckInternals(vshControl *ctl, return -1; } - if (cmd->handler) { + if (cmd->handler || cmd->handler_rv) { vshError(ctl, _("command '%1$s' has handler set"), cmd->name); return -1; } @@ -323,6 +323,11 @@ vshCmddefCheckInternals(vshControl *ctl, return -1; } + if (!!cmd->handler + !!cmd->handler_rv != 1) { + vshError(ctl, _("command '%1$s' must have exactly one of the handler callbacks set"), cmd->name); + return -1; + } + if (!cmd->opts) return 0; @@ -1350,6 +1355,9 @@ vshBlockJobOptionBandwidth(vshControl *ctl, * which return boolean are converted as: * true -> EXIT_SUCCESS * false -> EXIT_FAILURE + * Return values from command handlers returning integers are converted as: + * '< 0' -> EXIT_FAILURE + * others -> use value returned by handler callback. */ int vshCommandRun(vshControl *ctl, @@ -1366,10 +1374,17 @@ vshCommandRun(vshControl *ctl, if ((cmd->def->flags & VSH_CMD_FLAG_NOCONNECT) || (hooks && hooks->connHandler && hooks->connHandler(ctl))) { - if (cmd->def->handler(ctl, cmd)) - ret = EXIT_SUCCESS; - else - ret = EXIT_FAILURE; + if (cmd->def->handler_rv) { + ret = cmd->def->handler_rv(ctl, cmd); + + if (ret < 0) + ret = EXIT_FAILURE; + } else { + if (cmd->def->handler(ctl, cmd)) + ret = EXIT_SUCCESS; + else + ret = EXIT_FAILURE; + } } else { /* connection is not usable, return error */ ret = EXIT_FAILURE; diff --git a/tools/vsh.h b/tools/vsh.h index 284da36e32..bd2494e899 100644 --- a/tools/vsh.h +++ b/tools/vsh.h @@ -167,6 +167,7 @@ enum { struct _vshCmdDef { const char *name; /* name of command, or NULL for list end */ bool (*handler) (vshControl *, const vshCmd *); /* command handler */ + int (*handler_rv) (vshControl *, const vshCmd *); /* command handler - allows return codes */ const vshCmdOptDef *opts; /* definition of command options */ const vshCmdInfo *info; /* details about command */ unsigned int flags; /* bitwise OR of VSH_CMD_FLAG */ -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> The new command is meant as syntax sugar for event handling which blocks virsh until the requested state condition is reached. The initial implementation adds a condition 'domain-inactive' returning if the domain is/becomes inactive for whatever reason. This command is useful for simple scripts e.g. for debugging libvirt when it allows responding to target state in shell without the need to fuss too much with polling or writing handlers around 'virsh event'. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- docs/manpages/virsh.rst | 25 ++++ tools/virsh-domain-event.c | 245 +++++++++++++++++++++++++++++++++++++ 2 files changed, 270 insertions(+) diff --git a/docs/manpages/virsh.rst b/docs/manpages/virsh.rst index 895a905b08..1515f84063 100644 --- a/docs/manpages/virsh.rst +++ b/docs/manpages/virsh.rst @@ -3062,6 +3062,31 @@ When *--timestamp* is used, a human-readable timestamp will be printed before the event. +await +----- + +**Syntax:** + +:: + + await <domain> --condition <string> [--timeout seconds] + +Wait until the *--condition* for <domain> is satisfied. Uses events for +efficient state updates. + +Supported conditions: + + *domain-inactive* + + domain is or becomes inactive + +If *--timeout* is specified, the command gives up waiting for the condition to +satisfy after *seconds* have elapsed. If SIGINT is delivered to virsh +(usually via ``Ctrl-C``) the wait is given up immediately. In non-interactive +mode virsh will return '2' if either of those cases instead of '1' which means +an error happened. + + get-user-sshkeys ---------------- diff --git a/tools/virsh-domain-event.c b/tools/virsh-domain-event.c index 9aa21b2e78..2cd52933c7 100644 --- a/tools/virsh-domain-event.c +++ b/tools/virsh-domain-event.c @@ -1064,6 +1064,245 @@ cmdEvent(vshControl *ctl, const vshCmd *cmd) } +/* virsh event-await */ + +struct virshDomEventAwaitConditionData; + +struct virshDomainEventAwaitCondition { + const char *name; + int event; + int (*handler)(struct virshDomEventAwaitConditionData *data); +}; + +struct virshDomEventAwaitConditionData { + vshControl *ctl; + virshDomain *dom; + const struct virshDomainEventAwaitCondition *cond; + + virMutex *m; /* synchronization to ensure clean shutdown */ + virCond *c; + bool done; +}; + + +static void +virshDomEventAwaitConditionDataUnregistered(struct virshDomEventAwaitConditionData *data) +{ + g_auto(virLockGuard) name = virLockGuardLock(data->m); + /* signal that the handler was unregistered */ + data->done = true; + virCondSignal(data->c); +} + + +static void +virshDomainEventAwaitCallbackLifecycle(virConnectPtr conn G_GNUC_UNUSED, + virDomainPtr dom G_GNUC_UNUSED, + int event G_GNUC_UNUSED, + int detail G_GNUC_UNUSED, + void *opaque) +{ + struct virshDomEventAwaitConditionData *data = opaque; + + if (data->cond->handler(data) < 1) + vshEventDone(data->ctl); +} + + +struct virshDomainEventAwaitCallbackTuple { + int event; + virConnectDomainEventGenericCallback eventCB; +}; + + +static const struct virshDomainEventAwaitCallbackTuple callbacks[] = +{ + { .event = VIR_DOMAIN_EVENT_ID_LIFECYCLE, + .eventCB = VIR_DOMAIN_EVENT_CALLBACK(virshDomainEventAwaitCallbackLifecycle), + }, +}; + + +static int +virshDomainEventAwaitConditionDomainInactive(struct virshDomEventAwaitConditionData *data) +{ + int state = -1; + + if (virDomainGetState(data->dom, &state, NULL, 0) < 0) { + vshError(data->ctl, "%s", _("failed to update domain state")); + return -1; + } + + switch ((virDomainState) state) { + case VIR_DOMAIN_SHUTOFF: + case VIR_DOMAIN_CRASHED: + return 0; + + case VIR_DOMAIN_NOSTATE: + case VIR_DOMAIN_RUNNING: + case VIR_DOMAIN_BLOCKED: + case VIR_DOMAIN_PAUSED: + case VIR_DOMAIN_SHUTDOWN: + case VIR_DOMAIN_PMSUSPENDED: + case VIR_DOMAIN_LAST: + break; + } + + return 1; +} + + +static const struct virshDomainEventAwaitCondition conditions[] = { + { .name = "domain-inactive", + .event = VIR_DOMAIN_EVENT_ID_LIFECYCLE, + .handler = virshDomainEventAwaitConditionDomainInactive, + }, +}; + + +static char ** +virshDomainAwaitConditionNameCompleter(vshControl *ctl G_GNUC_UNUSED, + const vshCmd *cmd G_GNUC_UNUSED, + unsigned int flags) +{ + size_t i = 0; + GStrv ret = NULL; + + virCheckFlags(0, NULL); + + ret = g_new0(char *, G_N_ELEMENTS(conditions) + 1); + + for (i = 0; i < G_N_ELEMENTS(conditions); i++) + ret[i] = g_strdup(conditions[i].name); + + return ret; +} + + +static const vshCmdInfo info_await = { + .help = N_("await a domain event"), + .desc = N_("waits for a certain domain event to happen and then terminates"), +}; + +static const vshCmdOptDef opts_await[] = { + VIRSH_COMMON_OPT_DOMAIN_FULL(0), + {.name = "condition", + .type = VSH_OT_STRING, + .required = true, + .completer = virshDomainAwaitConditionNameCompleter, + .help = N_("which condition to wait until") + }, + {.name = "timeout", + .type = VSH_OT_INT, + .help = N_("timeout seconds") + }, + {.name = NULL} +}; + + +static int +cmdAwait(vshControl *ctl, + const vshCmd *cmd) +{ + g_autoptr(virshDomain) dom = NULL; + int timeout = 0; + size_t i; + const char *conditionName = NULL; + virshControl *priv = ctl->privData; + const struct virshDomainEventAwaitCallbackTuple *callback = NULL; + int evid = -1; + g_auto(virMutex) m = VIR_MUTEX_INITIALIZER; + g_auto(virCond) c = VIR_COND_INITIALIZER; + struct virshDomEventAwaitConditionData data = { + .ctl = ctl, + .m = &m, + .c = &c, + }; + int ret = -1; + + if (vshCommandOptString(ctl, cmd, "condition", &conditionName) < 0) + return -1; + + for (i = 0; i < G_N_ELEMENTS(conditions); i++) { + if (STREQ(conditionName, conditions[i].name)) { + data.cond = conditions + i; + break; + } + } + + if (!data.cond) { + vshError(ctl, _("Unsupported await condition name '%1$s'"), conditionName); + return -1; + } + + for (i = 0; i < G_N_ELEMENTS(callbacks); i++) { + if (data.cond->event == callbacks[i].event) { + callback = callbacks + i; + break; + } + } + + if (!callback) { + vshError(ctl, _("Missing callback definition for event type '%1$d'"), data.cond->event); + return -1; + } + + if (vshCommandOptTimeoutToMs(ctl, cmd, &timeout) < 0) + return -1; + + if (!(data.dom = dom = virshCommandOptDomain(ctl, cmd, NULL))) + return -1; + + if (vshEventStart(ctl, timeout) < 0) + goto cleanup; + + if ((evid = virConnectDomainEventRegisterAny(priv->conn, dom, + callback->event, + callback->eventCB, + &data, + (virFreeCallback) virshDomEventAwaitConditionDataUnregistered)) < 0) + goto cleanup; + + /* invoke the handler to ensure initial state update */ + if ((ret = data.cond->handler(&data)) <= 0) + goto cleanup; + + switch (vshEventWait(ctl)) { + case VSH_EVENT_INTERRUPT: + vshPrintExtra(ctl, "%s", _("event loop interrupted\n")); + ret = 2; + break; + + case VSH_EVENT_TIMEOUT: + vshPrintExtra(ctl, "%s", _("event loop timed out\n")); + ret = 2; + break; + + case VSH_EVENT_DONE: + ret = 0; + break; + + default: + ret = -1; + goto cleanup; + } + + cleanup: + if (evid >= 0) { + virConnectDomainEventDeregisterAny(priv->conn, evid); + + virMutexLock(&m); + while (!data.done) { + if (virCondWait(&c, &m) < 0) + break; + } + virMutexUnlock(&m); + } + vshEventCleanup(ctl); + + return ret; +} + const vshCmdDef domEventCmds[] = { {.name = "event", .handler = cmdEvent, @@ -1071,5 +1310,11 @@ const vshCmdDef domEventCmds[] = { .info = &info_event, .flags = 0 }, + {.name = "await", + .handler_rv = cmdAwait, + .opts = opts_await, + .info = &info_await, + .flags = 0 + }, {.name = NULL} }; -- 2.49.0

From: Peter Krempa <pkrempa@redhat.com> The new condition allows waiting for the guest agent to show up, which usually means that the guest has booted enough to respond to external stimuli. Signed-off-by: Peter Krempa <pkrempa@redhat.com> --- docs/manpages/virsh.rst | 5 +++++ tools/virsh-domain-event.c | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/docs/manpages/virsh.rst b/docs/manpages/virsh.rst index 1515f84063..0259c21065 100644 --- a/docs/manpages/virsh.rst +++ b/docs/manpages/virsh.rst @@ -3080,6 +3080,11 @@ Supported conditions: domain is or becomes inactive + *guest-agent-available* + + the guest agent inside the guest connects and becomes available for commands + (usually means that the guest has booted) + If *--timeout* is specified, the command gives up waiting for the condition to satisfy after *seconds* have elapsed. If SIGINT is delivered to virsh (usually via ``Ctrl-C``) the wait is given up immediately. In non-interactive diff --git a/tools/virsh-domain-event.c b/tools/virsh-domain-event.c index 2cd52933c7..de33ed9d74 100644 --- a/tools/virsh-domain-event.c +++ b/tools/virsh-domain-event.c @@ -24,6 +24,7 @@ #include "virenum.h" #include "virtime.h" #include "virtypedparam.h" +#include "virxml.h" /* * "event" command @@ -1109,6 +1110,20 @@ virshDomainEventAwaitCallbackLifecycle(virConnectPtr conn G_GNUC_UNUSED, } +static void +virshDomainEventAwaitAgentLifecycle(virConnectPtr conn G_GNUC_UNUSED, + virDomainPtr dom G_GNUC_UNUSED, + int state G_GNUC_UNUSED, + int reason G_GNUC_UNUSED, + void *opaque G_GNUC_UNUSED) +{ + struct virshDomEventAwaitConditionData *data = opaque; + + if (data->cond->handler(data) < 1) + vshEventDone(data->ctl); +} + + struct virshDomainEventAwaitCallbackTuple { int event; virConnectDomainEventGenericCallback eventCB; @@ -1120,6 +1135,9 @@ static const struct virshDomainEventAwaitCallbackTuple callbacks[] = { .event = VIR_DOMAIN_EVENT_ID_LIFECYCLE, .eventCB = VIR_DOMAIN_EVENT_CALLBACK(virshDomainEventAwaitCallbackLifecycle), }, + { .event = VIR_DOMAIN_EVENT_ID_AGENT_LIFECYCLE, + .eventCB = VIR_DOMAIN_EVENT_CALLBACK(virshDomainEventAwaitAgentLifecycle), + }, }; @@ -1152,11 +1170,35 @@ virshDomainEventAwaitConditionDomainInactive(struct virshDomEventAwaitConditionD } +static int +virshDomainEventAwaitConditionGuestAgentAvailable(struct virshDomEventAwaitConditionData *data) +{ + g_autoptr(xmlDoc) xml = NULL; + g_autoptr(xmlXPathContext) ctxt = NULL; + g_autofree char *state = NULL; + + if (virshDomainGetXMLFromDom(data->ctl, data->dom, 0, &xml, &ctxt) < 0) + return -1; + + if ((state = virXPathString("string(//devices/channel/target[@name = 'org.qemu.guest_agent.0']/@state)", + ctxt))) { + if (STREQ(state, "connected")) + return 0; + } + + return 1; +} + + static const struct virshDomainEventAwaitCondition conditions[] = { { .name = "domain-inactive", .event = VIR_DOMAIN_EVENT_ID_LIFECYCLE, .handler = virshDomainEventAwaitConditionDomainInactive, }, + { .name = "guest-agent-available", + .event = VIR_DOMAIN_EVENT_ID_AGENT_LIFECYCLE, + .handler = virshDomainEventAwaitConditionGuestAgentAvailable, + }, }; -- 2.49.0

On a Thursday in 2025, Peter Krempa via Devel wrote:
'virsh await' is a helper/syntax-sugar command which allows you to wait until a domain reaches a certain state. Currently this series implements conditions named 'domain-inactive' and 'guest-agent-available'.
Peter Krempa (7): virthread: Register auto cleanup function for virMutex virthread: Add infrastructure for static virCond definitions virsh: cmdEvent: Ensure that event callbacks are unregistered before returning vshCommandRun: Convert to directly return the exit code vsh: Add support for commands with more return values virsh: Introduce 'await' command for waiting until target domain state is reached virsh: await: Introduce 'guest-agent-available' condition
docs/manpages/virsh.rst | 30 ++++ src/util/virthread.h | 7 + tools/virsh-domain-event.c | 321 ++++++++++++++++++++++++++++++++++++- tools/virsh.c | 4 +- tools/virt-admin.c | 4 +- tools/vsh.c | 46 ++++-- tools/vsh.h | 3 +- 7 files changed, 398 insertions(+), 17 deletions(-)
Reviewed-by: Ján Tomko <jtomko@redhat.com> Jano
participants (2)
-
Ján Tomko
-
Peter Krempa