[PATCH 0/4] qemu: add API to ammounce guest interfaces to the network
This provides a supported method of executing QEMU's "self-announce" command, which injects gratuitous ARP packets into the outgoing data stream of guest interfaces. Resolves: https://redhat.atlassian.net/browse/RHEL-7047 Laine Stump (4): qemu: new monitor function qemuMonitorAnnounceSelf() API: Introduce virDomainAnnounceInterface qemu: implement virDomainAnnounceInterface() API virsh: new command "domifannounce" docs/manpages/virsh.rst | 41 +++++++++++++++ include/libvirt/libvirt-domain.h | 46 +++++++++++++++++ src/driver-hypervisor.h | 8 +++ src/libvirt-domain.c | 65 +++++++++++++++++++++++ src/libvirt_public.syms | 5 ++ src/qemu/qemu_driver.c | 81 ++++++++++++++++++++++++++++- src/qemu/qemu_monitor.c | 13 +++++ src/qemu/qemu_monitor.h | 8 +++ src/qemu/qemu_monitor_json.c | 55 ++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 8 +++ src/remote/remote_driver.c | 1 + src/remote/remote_protocol.x | 19 ++++++- src/remote_protocol-structs | 10 ++++ tools/virsh-domain.c | 88 ++++++++++++++++++++++++++++++++ 14 files changed, 446 insertions(+), 2 deletions(-) -- 2.54.0
From: Laine Stump <laine@redhat.com> This new function sends the command "self-announce" to QEMU, causing it to inject a series of gratuitous ARP packets (GARP) into the output stream of one or all guest interfaces, which will force any switches the same collision domain to update their forwarding db for that interface. There are several parameters that control which interfaces the GARP packets are sent on, as well as their number and interval, but all of these parameters have sane defaults (even though QEMU's own self-announce requires they all be specified in the QMP command). All parameters except the device are just unsigned integers; the QEMU command accepts a list of interface devices, but it seems much more likely that someone will either want to announce a single interface, or all of them, so our function just accepts a single device name that can be present or not, and puts that single name into a list (in the rare case that someone wants to announce multiple interfaces but not all of them, they can call our API multiple times). Signed-off-by: Laine Stump <laine@redhat.com> --- src/qemu/qemu_monitor.c | 13 +++++++++ src/qemu/qemu_monitor.h | 8 ++++++ src/qemu/qemu_monitor_json.c | 55 ++++++++++++++++++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 8 ++++++ 4 files changed, 84 insertions(+) diff --git a/src/qemu/qemu_monitor.c b/src/qemu/qemu_monitor.c index ef83e7b69a..baa78dd6fe 100644 --- a/src/qemu/qemu_monitor.c +++ b/src/qemu/qemu_monitor.c @@ -4592,3 +4592,16 @@ qemuMonitorBlockLatencyHistogramSet(qemuMonitor *mon, boundaries_zone, boundaries_flush); } + +int +qemuMonitorAnnounceSelf(qemuMonitor *mon, + const char *device, + unsigned int initial, + unsigned int max, + unsigned int rounds, + unsigned int step) +{ + QEMU_CHECK_MONITOR(mon); + + return qemuMonitorJSONAnnounceSelf(mon, device, initial, max, rounds, step); +} diff --git a/src/qemu/qemu_monitor.h b/src/qemu/qemu_monitor.h index f93d193d75..c2afb580e4 100644 --- a/src/qemu/qemu_monitor.h +++ b/src/qemu/qemu_monitor.h @@ -1995,3 +1995,11 @@ qemuMonitorBlockLatencyHistogramSet(qemuMonitor *mon, unsigned int *boundaries_write, unsigned int *boundaries_zone, unsigned int *boundaries_flush); + +int +qemuMonitorAnnounceSelf(qemuMonitor *mon, + const char *device, + unsigned int initial, + unsigned int max, + unsigned int rounds, + unsigned int step); diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c index 7efd3f443a..074eeb856c 100644 --- a/src/qemu/qemu_monitor_json.c +++ b/src/qemu/qemu_monitor_json.c @@ -9237,3 +9237,58 @@ qemuMonitorJSONBlockLatencyHistogramSet(qemuMonitor *mon, return qemuMonitorJSONCheckError(cmd, reply); } + + +int +qemuMonitorJSONAnnounceSelf(qemuMonitor *mon, + const char *device, + unsigned int initial, + unsigned int max, + unsigned int rounds, + unsigned int step) +{ + g_autoptr(virJSONValue) cmd = NULL; + g_autoptr(virJSONValue) reply = NULL; + g_autoptr(virJSONValue) devlist = NULL; + + /* announce-self optionally accepts a list of device names, but we + * only support a single device name (or none), so we make a + * NULL-terminated list with a single item + */ + if (device) { + devlist = virJSONValueNewArray(); + if (virJSONValueArrayAppendString(devlist, device) < 0) + return -1; + } + + /* all the other parameters are mandatory for QEMU, but we make + * them optional to simplify using the API. Since the value 0 + * would be nonsensical for any of these, we make that the "use a + * default value" sentinal. The values we use as default are the + * values QEMU will use internally when it does an announce-self + * at the end of a migration. + */ + if (!initial) + initial = 50; + if (!max) + max = 550; + if (!rounds) + rounds = 5; + if (!step) + step = 50; + + if (!(cmd = qemuMonitorJSONMakeCommand("announce-self", + "A:interfaces", &devlist, + "u:initial", initial, + "u:max", max, + "u:rounds", rounds, + "u:step", step, + NULL))) { + return -1; + } + + if (qemuMonitorJSONCommand(mon, cmd, &reply) < 0) + return -1; + + return qemuMonitorJSONCheckError(cmd, reply); +} diff --git a/src/qemu/qemu_monitor_json.h b/src/qemu/qemu_monitor_json.h index 5034c8d23d..f4c093d717 100644 --- a/src/qemu/qemu_monitor_json.h +++ b/src/qemu/qemu_monitor_json.h @@ -828,3 +828,11 @@ qemuMonitorJSONBlockLatencyHistogramSet(qemuMonitor *mon, unsigned int *boundaries_write, unsigned int *boundaries_zone, unsigned int *boundaries_flush); + +int +qemuMonitorJSONAnnounceSelf(qemuMonitor *mon, + const char *device, + unsigned int initial, + unsigned int max, + unsigned int rounds, + unsigned int step); -- 2.54.0
On Tue, Jun 02, 2026 at 01:46:44AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
This new function sends the command "self-announce" to QEMU, causing it to inject a series of gratuitous ARP packets (GARP) into the output stream of one or all guest interfaces, which will force any switches the same collision domain to update their forwarding db for that interface.
There are several parameters that control which interfaces the GARP packets are sent on, as well as their number and interval, but all of these parameters have sane defaults (even though QEMU's own self-announce requires they all be specified in the QMP command).
All parameters except the device are just unsigned integers; the QEMU command accepts a list of interface devices, but it seems much more likely that someone will either want to announce a single interface, or all of them, so our function just accepts a single device name that can be present or not, and puts that single name into a list (in the rare case that someone wants to announce multiple interfaces but not all of them, they can call our API multiple times).
Signed-off-by: Laine Stump <laine@redhat.com> --- src/qemu/qemu_monitor.c | 13 +++++++++ src/qemu/qemu_monitor.h | 8 ++++++ src/qemu/qemu_monitor_json.c | 55 ++++++++++++++++++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 8 ++++++ 4 files changed, 84 insertions(+)
diff --git a/src/qemu/qemu_monitor_json.c b/src/qemu/qemu_monitor_json.c index 7efd3f443a..074eeb856c 100644 --- a/src/qemu/qemu_monitor_json.c +++ b/src/qemu/qemu_monitor_json.c @@ -9237,3 +9237,58 @@ qemuMonitorJSONBlockLatencyHistogramSet(qemuMonitor *mon,
return qemuMonitorJSONCheckError(cmd, reply); } + + +int +qemuMonitorJSONAnnounceSelf(qemuMonitor *mon, + const char *device, + unsigned int initial, + unsigned int max, + unsigned int rounds, + unsigned int step) +{ + g_autoptr(virJSONValue) cmd = NULL; + g_autoptr(virJSONValue) reply = NULL; + g_autoptr(virJSONValue) devlist = NULL; + + /* announce-self optionally accepts a list of device names, but we + * only support a single device name (or none), so we make a + * NULL-terminated list with a single item + */ + if (device) { + devlist = virJSONValueNewArray(); + if (virJSONValueArrayAppendString(devlist, device) < 0) + return -1; + } + + /* all the other parameters are mandatory for QEMU, but we make + * them optional to simplify using the API. Since the value 0 + * would be nonsensical for any of these, we make that the "use a + * default value" sentinal. The values we use as default are the
s/sentinal/sentinel/ maybe? Reviewed-by: Martin Kletzander <mkletzan@redhat.com>
On Tue, Jun 02, 2026 at 01:46:44 -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
This new function sends the command "self-announce" to QEMU, causing it to inject a series of gratuitous ARP packets (GARP) into the output stream of one or all guest interfaces, which will force any switches the same collision domain to update their forwarding db for that interface.
There are several parameters that control which interfaces the GARP packets are sent on, as well as their number and interval, but all of these parameters have sane defaults (even though QEMU's own self-announce requires they all be specified in the QMP command).
All parameters except the device are just unsigned integers; the QEMU command accepts a list of interface devices, but it seems much more likely that someone will either want to announce a single interface, or all of them, so our function just accepts a single device name that can be present or not, and puts that single name into a list (in the rare case that someone wants to announce multiple interfaces but not all of them, they can call our API multiple times).
Signed-off-by: Laine Stump <laine@redhat.com> --- src/qemu/qemu_monitor.c | 13 +++++++++ src/qemu/qemu_monitor.h | 8 ++++++ src/qemu/qemu_monitor_json.c | 55 ++++++++++++++++++++++++++++++++++++ src/qemu/qemu_monitor_json.h | 8 ++++++ 4 files changed, 84 insertions(+)
Missing addition to qemumonitorjsontest. You can post it separately if you want but I really want that all commands (which don't have a commandline alternative) are tested because qemumonitorjsontest actually validates all arguments against the QMP schema, which allows us to spot if qemu e.g. deprecates them.
From: Laine Stump <laine@redhat.com> Does this do what's needed for testing? (I haven't added any cases to qemumonitorjsontest before, so it was a bit of an adventure to (I hope) figure it out). If this is correct, I'll squash it into Patch 1. Signed-off-by: Laine Stump <laine@redhat.com> --- tests/qemumonitorjsontest.c | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index e34dbad7cd..f59b97c1c3 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -2776,6 +2776,84 @@ testQemuMonitorJSONGetSEVInfo(const void *opaque) } +static int +testQemuMonitorJSONAnnounceInterface(const void *opaque) +{ + const testGenericData *data = opaque; + g_autoptr(qemuMonitorTest) test = NULL; + + if (!(test = qemuMonitorTestNewSchema(data->xmlopt, data->schema))) + return -1; + + /* Test 1 - all parameters are default */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"initial\": 50," + " \"max\": 550," + " \"rounds\": 5," + " \"step\": 50" + " }," + " \"id\":\"libvirt-1\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), NULL, 0, 0, 0, 0) < 0) + return -1; + + /* Test 2 - interface device set, everything else default */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"interfaces\":[\"net0\"]," + " \"initial\": 50," + " \"max\": 550," + " \"rounds\": 5," + " \"step\": 50" + " }," + " \"id\":\"libvirt-2\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), "net0", 0, 0, 0, 0) < 0) + return -1; + + /* Test 3 - all parameters explicitly set */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"interfaces\":[\"skid00\"]," + " \"initial\": 23," + " \"max\": 54," + " \"rounds\": 867," + " \"step\": 5309" + " }," + " \"id\":\"libvirt-3\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), "skid00", 23, 54, 867, 5309) < 0) + return -1; + + return 0; +} + + struct testQemuMonitorJSONGetGuestCPUData { const char *name; bool qomListGet; @@ -2940,6 +3018,7 @@ mymain(void) DO_TEST(Transaction); DO_TEST(BlockExportAdd); DO_TEST(BlockdevReopen); + DO_TEST(AnnounceInterface); DO_TEST_SIMPLE("qmp_capabilities", qemuMonitorJSONSetCapabilities); DO_TEST_SIMPLE("system_powerdown", qemuMonitorJSONSystemPowerdown); DO_TEST_SIMPLE("system_reset", qemuMonitorJSONSystemReset); -- 2.54.0
On Thu, Jun 04, 2026 at 17:17:01 -0400, Laine Stump wrote:
From: Laine Stump <laine@redhat.com>
Does this do what's needed for testing? (I haven't added any cases to qemumonitorjsontest before, so it was a bit of an adventure to (I hope) figure it out).
Well, you did much more than I actually wanted and it's on me for not being specific :). It would be completely enough to just do the auto-generated tests (GEN_TEST_FUNC, DO_TEST_GEN). Those, if you pass arguments which excercise all parameters, are usually sufficient as they still do schema validation. Specifically 'qemuMonitorTestAddItemVerbatim' that you used is a bit harder to maintain if you want to add new stuff later.
If this is correct, I'll squash it into Patch 1.
Anyways, it is correct; go ahead,
Signed-off-by: Laine Stump <laine@redhat.com> --- tests/qemumonitorjsontest.c | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+)
diff --git a/tests/qemumonitorjsontest.c b/tests/qemumonitorjsontest.c index e34dbad7cd..f59b97c1c3 100644 --- a/tests/qemumonitorjsontest.c +++ b/tests/qemumonitorjsontest.c @@ -2776,6 +2776,84 @@ testQemuMonitorJSONGetSEVInfo(const void *opaque) }
+static int +testQemuMonitorJSONAnnounceInterface(const void *opaque) +{ + const testGenericData *data = opaque; + g_autoptr(qemuMonitorTest) test = NULL; + + if (!(test = qemuMonitorTestNewSchema(data->xmlopt, data->schema))) + return -1; + + /* Test 1 - all parameters are default */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"initial\": 50," + " \"max\": 550," + " \"rounds\": 5," + " \"step\": 50" + " }," + " \"id\":\"libvirt-1\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), NULL, 0, 0, 0, 0) < 0) + return -1; + + /* Test 2 - interface device set, everything else default */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"interfaces\":[\"net0\"]," + " \"initial\": 50," + " \"max\": 550," + " \"rounds\": 5," + " \"step\": 50" + " }," + " \"id\":\"libvirt-2\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), "net0", 0, 0, 0, 0) < 0) + return -1; + + /* Test 3 - all parameters explicitly set */ + if (qemuMonitorTestAddItemVerbatim(test, + "{" + " \"execute\": \"announce-self\"," + " \"arguments\": {" + " \"interfaces\":[\"skid00\"]," + " \"initial\": 23," + " \"max\": 54," + " \"rounds\": 867," + " \"step\": 5309" + " }," + " \"id\":\"libvirt-3\"" + "}", + NULL, + "{\"return\":{}}") < 0) { + return -1; + } + + + if (qemuMonitorJSONAnnounceSelf(qemuMonitorTestGetMonitor(test), "skid00", 23, 54, 867, 5309) < 0) + return -1; + + return 0; +} + + struct testQemuMonitorJSONGetGuestCPUData { const char *name; bool qomListGet; @@ -2940,6 +3018,7 @@ mymain(void) DO_TEST(Transaction); DO_TEST(BlockExportAdd); DO_TEST(BlockdevReopen); + DO_TEST(AnnounceInterface); DO_TEST_SIMPLE("qmp_capabilities", qemuMonitorJSONSetCapabilities); DO_TEST_SIMPLE("system_powerdown", qemuMonitorJSONSystemPowerdown); DO_TEST_SIMPLE("system_reset", qemuMonitorJSONSystemReset); -- 2.54.0
Reviewed-by: Peter Krempa <pkrempa@redhat.com>
From: Laine Stump <laine@redhat.com> This API provides a way for a libvirt client to force a guest to inject a "Gratuuitous ARP" packet into the outgoing stream of one, or all, network devices of the guest; this will be used to update the forwarding tables of any network switches in the local broadcast domain so that they will begin forarding traffic correctly in a more timely manner after network topology changes. Signed-off-by: Laine Stump <laine@redhat.com> --- include/libvirt/libvirt-domain.h | 46 ++++++++++++++++++++++ src/driver-hypervisor.h | 8 ++++ src/libvirt-domain.c | 65 ++++++++++++++++++++++++++++++++ src/libvirt_public.syms | 5 +++ src/remote/remote_driver.c | 1 + src/remote/remote_protocol.x | 19 +++++++++- src/remote_protocol-structs | 10 +++++ 7 files changed, 153 insertions(+), 1 deletion(-) diff --git a/include/libvirt/libvirt-domain.h b/include/libvirt/libvirt-domain.h index 8f07ef2156..ccc74ad6a4 100644 --- a/include/libvirt/libvirt-domain.h +++ b/include/libvirt/libvirt-domain.h @@ -9049,5 +9049,51 @@ virDomainDelThrottleGroup(virDomainPtr dom, const char *group, unsigned int flags); +/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL: + * + * Initial delay in milliseonds before the first announce packet is + * sent. If unspecified or 0, a default value of 50 will be used. + * + * Since: 12.5.0 + */ +# define VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL "initial" + +/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX: + * + * Maximum delay in milliseonds between packets. If unspecified or 0, + * a default value of 550 will be used. + * + * Since: 12.5.0 + */ +# define VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX "max" + +/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS: + * + * The number of packets to send. If unspecified or 0, a default value + * of 5 will be used. + * + * Since: 12.5.0 + */ +# define VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS "rounds" + +/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP: + * + * Increment added to the delay (in milliseconds) after each packet is + * sent. If unspecified or 0, a default value of 50 will be used. + * + * Since: 12.5.0 + */ +# define VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP "step" + +int +virDomainAnnounceInterface(virDomainPtr dom, + const char *device, + virTypedParameterPtr params, + int nparams, + unsigned int flags); #endif /* LIBVIRT_DOMAIN_H */ diff --git a/src/driver-hypervisor.h b/src/driver-hypervisor.h index 6a43688b0c..0add95de96 100644 --- a/src/driver-hypervisor.h +++ b/src/driver-hypervisor.h @@ -1473,6 +1473,13 @@ typedef int const char *groupname, unsigned int flags); +typedef int +(*virDrvDomainAnnounceInterface)(virDomainPtr dom, + const char *device, + virTypedParameterPtr params, + int nparams, + unsigned int flags); + typedef struct _virHypervisorDriver virHypervisorDriver; /** @@ -1750,4 +1757,5 @@ struct _virHypervisorDriver { virDrvDomainGraphicsReload domainGraphicsReload; virDrvDomainSetThrottleGroup domainSetThrottleGroup; virDrvDomainDelThrottleGroup domainDelThrottleGroup; + virDrvDomainAnnounceInterface domainAnnounceInterface; }; diff --git a/src/libvirt-domain.c b/src/libvirt-domain.c index a4cbeb8ad4..f9ee416fe7 100644 --- a/src/libvirt-domain.c +++ b/src/libvirt-domain.c @@ -14308,3 +14308,68 @@ virDomainDelThrottleGroup(virDomainPtr dom, virDispatchError(dom->conn); return -1; } + + +/** + * virDomainAnnounceInterface: + * @dom: pointer to domain object + * @device: the interface name or mac address, or NULL to announce all interfaces + * @params: pointer to typed parameters object + * @nparams: number of parameters in @params + * @flags: currently unused, pass 0 + * + * Cause this domain to "announce" its network interfaces by injecting + * a series of "gratuitous ARP" packets into the outgoing data stream + * for the interface matching @device (or all interfaces). This should + * cause local switches to direct traffic for that MAC address + * correctly after a topology change. + * + * See VIR_DOMAIN_ANNOUNCE_INTERFACE_* for detailed descriptions of + * accepted parameters. + * + * Returns: 0 on success, + * -1 otherwise. + * + * Since: 12.5.0 + */ +int +virDomainAnnounceInterface(virDomainPtr dom, + const char *device, + virTypedParameterPtr params, + int nparams, + unsigned int flags) +{ + virConnectPtr conn; + + VIR_DEBUG("dom=%p, device='%s' params=%p nparams=%d flags=0x%x", + dom, NULLSTR(device), params, nparams, flags); + VIR_TYPED_PARAMS_DEBUG(params, nparams); + + virResetLastError(); + + virCheckDomainReturn(dom, -1); + + conn = dom->conn; + + virCheckReadOnlyGoto(conn->flags, error); + if (nparams != 0) + virCheckNonNullArgGoto(params, error); + else + virCheckNullArgGoto(params, error); + + if (virTypedParameterValidateSet(conn, params, nparams) < 0) + goto error; + + if (conn->driver->domainAnnounceInterface) { + int ret = conn->driver->domainAnnounceInterface(dom, device, params, nparams, flags); + if (ret < 0) + goto error; + return ret; + } + + virReportUnsupportedError(); + + error: + virDispatchError(conn); + return -1; +} diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms index c506acd2ed..64ed641b7f 100644 --- a/src/libvirt_public.syms +++ b/src/libvirt_public.syms @@ -956,4 +956,9 @@ LIBVIRT_11.2.0 { virDomainDelThrottleGroup; } LIBVIRT_10.2.0; +LIBVIRT_12.5.0 { + global: + virDomainAnnounceInterface; +} LIBVIRT_11.2.0; + # .... define new API here using predicted next version number .... diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index 873e3d173c..16e82031e9 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -8004,6 +8004,7 @@ static virHypervisorDriver hypervisor_driver = { .domainGraphicsReload = remoteDomainGraphicsReload, /* 10.2.0 */ .domainSetThrottleGroup = remoteDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = remoteDomainDelThrottleGroup, /* 11.2.0 */ + .domainAnnounceInterface = remoteDomainAnnounceInterface, /* 12.5.0 */ }; static virNetworkDriver network_driver = { diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x index 4adba82f6d..32185fde2f 100644 --- a/src/remote/remote_protocol.x +++ b/src/remote/remote_protocol.x @@ -293,6 +293,10 @@ const REMOTE_DOMAIN_AUTHORIZED_SSH_KEYS_MAX = 2048; /* Upper limit on number of messages */ const REMOTE_DOMAIN_MESSAGES_MAX = 2048; +/* + * Upper limit on number of domain announce interface parameters + */ +const REMOTE_DOMAIN_ANNOUNCE_INTERFACE_PARAMS_MAX = 16; /* UUID. VIR_UUID_BUFLEN definition comes from libvirt.h */ typedef opaque remote_uuid[VIR_UUID_BUFLEN]; @@ -4023,6 +4027,13 @@ struct remote_domain_event_callback_channel_lifecycle_msg { int reason; }; +struct remote_domain_announce_interface_args { + remote_nonnull_domain dom; + remote_string device; + remote_typed_param params<REMOTE_DOMAIN_ANNOUNCE_INTERFACE_PARAMS_MAX>; + unsigned int flags; +}; + /*----- Protocol. -----*/ /* Define the program number, protocol version and procedure numbers here. */ @@ -7146,5 +7157,11 @@ enum remote_procedure { * @generate: both * @acl: none */ - REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455 + REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455, + + /** + * @generate: both + * @acl: domain:write + */ + REMOTE_PROC_DOMAIN_ANNOUNCE_INTERFACE = 456 }; diff --git a/src/remote_protocol-structs b/src/remote_protocol-structs index dd297bffff..6093a85c98 100644 --- a/src/remote_protocol-structs +++ b/src/remote_protocol-structs @@ -3349,6 +3349,15 @@ struct remote_domain_event_callback_channel_lifecycle_msg { int state; int reason; }; +struct remote_domain_announce_interface_args { + remote_nonnull_domain dom; + remote_string device; + struct { + u_int params_len; + remote_typed_param * params_val; + } params; + u_int flags; +}; enum remote_procedure { REMOTE_PROC_CONNECT_OPEN = 1, REMOTE_PROC_CONNECT_CLOSE = 2, @@ -3805,4 +3814,5 @@ enum remote_procedure { REMOTE_PROC_DOMAIN_EVENT_NIC_MAC_CHANGE = 453, REMOTE_PROC_DOMAIN_EVENT_VCPU_REMOVED = 454, REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455, + REMOTE_PROC_DOMAIN_ANNOUNCE_INTERFACE = 456, }; -- 2.54.0
On Tue, Jun 02, 2026 at 01:46:45AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
This API provides a way for a libvirt client to force a guest to inject a "Gratuuitous ARP" packet into the outgoing stream of one, or
s/Gratuuitous/Gratuitous/
all, network devices of the guest; this will be used to update the forwarding tables of any network switches in the local broadcast domain so that they will begin forarding traffic correctly in a more
s/forarding/forwarding/
timely manner after network topology changes.
Signed-off-by: Laine Stump <laine@redhat.com> --- include/libvirt/libvirt-domain.h | 46 ++++++++++++++++++++++ src/driver-hypervisor.h | 8 ++++ src/libvirt-domain.c | 65 ++++++++++++++++++++++++++++++++ src/libvirt_public.syms | 5 +++ src/remote/remote_driver.c | 1 + src/remote/remote_protocol.x | 19 +++++++++- src/remote_protocol-structs | 10 +++++ 7 files changed, 153 insertions(+), 1 deletion(-)
diff --git a/include/libvirt/libvirt-domain.h b/include/libvirt/libvirt-domain.h index 8f07ef2156..ccc74ad6a4 100644 --- a/include/libvirt/libvirt-domain.h +++ b/include/libvirt/libvirt-domain.h @@ -9049,5 +9049,51 @@ virDomainDelThrottleGroup(virDomainPtr dom, const char *group, unsigned int flags);
+/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL: + * + * Initial delay in milliseonds before the first announce packet is
s/milliseonds/milliseconds/
+ * sent. If unspecified or 0, a default value of 50 will be used. + * + * Since: 12.5.0 + */ +# define VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL "initial" + +/** + * VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX: + * + * Maximum delay in milliseonds between packets. If unspecified or 0,
s/milliseonds/milliseconds/ Reviewed-by: Martin Kletzander <mkletzan@redhat.com>
On 6/2/26 2:21 AM, Martin Kletzander wrote:
On Tue, Jun 02, 2026 at 01:46:45AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
This API provides a way for a libvirt client to force a guest to inject a "Gratuuitous ARP" packet into the outgoing stream of one, or
s/Gratuuitous/Gratuitous/
That was just a gratuitous "u" to illustrate the meaning of gratuitous :-P
all, network devices of the guest; this will be used to update the forwarding tables of any network switches in the local broadcast domain so that they will begin forarding traffic correctly in a more
s/forarding/forwarding/
Now *that* (and all the other cases of missing lettrs) I think may be due to my keyboard - I've noticed lately that it gets into bouts of dropping letters, and replacing the batteries doesn't seem to make a difference, so I may soon need to break the seal on my final MS Sculpt keyboard (they don't make them any more, and I can't comfortably type on anything else, so when I heard they were being discontinued a few years ago, I bought a spare).
From: Laine Stump <laine@redhat.com> using qemuMonitorAnnounceSelf(). Note that the other public domain interface APIs expect interface to be specified by either its "target" name (i.e. the name of the tap/macvtap device on the host) or by the MAC address, but qemuMonitorAnnounceSelf() expects the QEMU "device id" (which is known in libvirt as the "alias"), so we have to convert. Resolves: https://redhat.atlassian.net/browse/RHEL-7047 Signed-off-by: Laine Stump <laine@redhat.com> --- src/qemu/qemu_driver.c | 81 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 58b68a6e2b..872cf0e8ee 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -20865,7 +20865,85 @@ qemuDomainDelThrottleGroup(virDomainPtr dom, } -static virHypervisorDriver qemuHypervisorDriver = { + + +static int +qemuDomainAnnounceInterface(virDomainPtr dom, + const char *device, + virTypedParameterPtr params, + int nparams, + unsigned int flags) +{ + virDomainObj *vm = NULL; + int ret = -1; + qemuDomainObjPrivate *priv; + const char *alias = NULL; + unsigned int initial = 0; + unsigned int max = 0; + unsigned int rounds = 0; + unsigned int step = 0; + + /* no flags supported */ + virCheckFlags(0, -1); + + if (virTypedParamsValidate(params, nparams, + VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, VIR_TYPED_PARAM_UINT, + NULL) < 0) + return -1; + + if (params && nparams) { + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, &initial); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, &max); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, &rounds); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, &step); + } + + if (!(vm = qemuDomainObjFromDomain(dom))) + return -1; + priv = vm->privateData; + + if (virDomainAnnounceInterfaceEnsureACL(dom->conn, vm->def) < 0) + goto cleanup; + + if (virDomainObjBeginJob(vm, VIR_JOB_MODIFY) < 0) + goto cleanup; + + if (virDomainObjCheckActive(vm) < 0) + goto endjob; + + /* the user is sending either the interface MAC address or the + * name of the tap device (because that's how other APIs are + * implemented), but qemu's announce-self command expects the + * device id (known in libvirt as the "alias id"), so we need to + * find the <interface> and grab the alias from there + */ + + if (device) { + virDomainNetDef *net = NULL; + + if (!(net = virDomainNetFind(vm->def, device))) + goto endjob; + + alias = net->info.alias; + } + + qemuDomainObjEnterMonitor(vm); + ret = qemuMonitorAnnounceSelf(priv->mon, alias, initial, max, rounds, step); + qemuDomainObjExitMonitor(vm); + + endjob: + virDomainObjEndJob(vm); + + cleanup: + virDomainObjEndAPI(&vm); + return ret; +} + + + static virHypervisorDriver qemuHypervisorDriver = { .name = QEMU_DRIVER_NAME, .connectURIProbe = qemuConnectURIProbe, .connectOpen = qemuConnectOpen, /* 0.2.0 */ @@ -21119,6 +21197,7 @@ static virHypervisorDriver qemuHypervisorDriver = { .domainSetAutostartOnce = qemuDomainSetAutostartOnce, /* 11.2.0 */ .domainSetThrottleGroup = qemuDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = qemuDomainDelThrottleGroup, /* 11.2.0 */ + .domainAnnounceInterface = qemuDomainAnnounceInterface /* 12.4.0 */ }; -- 2.54.0
On Tue, Jun 02, 2026 at 01:46:46AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
using qemuMonitorAnnounceSelf(). Note that the other public domain interface APIs expect interface to be specified by either its "target" name (i.e. the name of the tap/macvtap device on the host) or by the MAC address, but qemuMonitorAnnounceSelf() expects the QEMU "device id" (which is known in libvirt as the "alias"), so we have to convert.
Resolves: https://redhat.atlassian.net/browse/RHEL-7047 Signed-off-by: Laine Stump <laine@redhat.com> --- src/qemu/qemu_driver.c | 81 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-)
diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 58b68a6e2b..872cf0e8ee 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -20865,7 +20865,85 @@ qemuDomainDelThrottleGroup(virDomainPtr dom, }
-static virHypervisorDriver qemuHypervisorDriver = {
This should stay here ...
+ + +static int +qemuDomainAnnounceInterface(virDomainPtr dom, + const char *device, + virTypedParameterPtr params, + int nparams, + unsigned int flags) +{ + virDomainObj *vm = NULL; + int ret = -1; + qemuDomainObjPrivate *priv; + const char *alias = NULL; + unsigned int initial = 0; + unsigned int max = 0; + unsigned int rounds = 0; + unsigned int step = 0; + + /* no flags supported */ + virCheckFlags(0, -1); + + if (virTypedParamsValidate(params, nparams, + VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, VIR_TYPED_PARAM_UINT, + VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, VIR_TYPED_PARAM_UINT, + NULL) < 0) + return -1; + + if (params && nparams) { + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, &initial); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, &max); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, &rounds); + virTypedParamsGetUInt(params, nparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, &step); + } + + if (!(vm = qemuDomainObjFromDomain(dom))) + return -1; + priv = vm->privateData; + + if (virDomainAnnounceInterfaceEnsureACL(dom->conn, vm->def) < 0) + goto cleanup; + + if (virDomainObjBeginJob(vm, VIR_JOB_MODIFY) < 0) + goto cleanup; + + if (virDomainObjCheckActive(vm) < 0) + goto endjob; + + /* the user is sending either the interface MAC address or the + * name of the tap device (because that's how other APIs are + * implemented), but qemu's announce-self command expects the + * device id (known in libvirt as the "alias id"), so we need to + * find the <interface> and grab the alias from there + */ + + if (device) { + virDomainNetDef *net = NULL; + + if (!(net = virDomainNetFind(vm->def, device))) + goto endjob; + + alias = net->info.alias; + } + + qemuDomainObjEnterMonitor(vm); + ret = qemuMonitorAnnounceSelf(priv->mon, alias, initial, max, rounds, step); + qemuDomainObjExitMonitor(vm); + + endjob: + virDomainObjEndJob(vm); + + cleanup: + virDomainObjEndAPI(&vm); + return ret; +} + + + static virHypervisorDriver qemuHypervisorDriver = {
I think you added an extra space in front of the `static` and that's why it messed up the diff, so s/^ static/static/
.name = QEMU_DRIVER_NAME, .connectURIProbe = qemuConnectURIProbe, .connectOpen = qemuConnectOpen, /* 0.2.0 */ @@ -21119,6 +21197,7 @@ static virHypervisorDriver qemuHypervisorDriver = { .domainSetAutostartOnce = qemuDomainSetAutostartOnce, /* 11.2.0 */ .domainSetThrottleGroup = qemuDomainSetThrottleGroup, /* 11.2.0 */ .domainDelThrottleGroup = qemuDomainDelThrottleGroup, /* 11.2.0 */ + .domainAnnounceInterface = qemuDomainAnnounceInterface /* 12.4.0 */
This should be 12.5.0. With both fixed: Reviewed-by: Martin Kletzander <mkletzan@redhat.com>
};
-- 2.54.0
On 6/2/26 2:23 AM, Martin Kletzander wrote:
On Tue, Jun 02, 2026 at 01:46:46AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com> [...] + + static virHypervisorDriver qemuHypervisorDriver = {
I think you added an extra space in front of the `static` and that's why it messed up the diff, so s/^ static/static/
Yup! That is what happened!
[...] + .domainAnnounceInterface = qemuDomainAnnounceInterface /* 12.4.0 */
This should be 12.5.0.
Dang! I thought I caught all those.
From: Laine Stump <laine@redhat.com> virsh domifannounce is a thin wrapper around the new API virDomainAnnounceInterface(). Syntax: virsh domifannounce guestname [interfacename] [parameters] where the optional interfacename can be either the MAC address of the interface to announce, or the name of the tap device used to connect the domain's interface to the real network (if the connection is with a tap device), and [parameters] is one or more of the following options: --initial [unsigned integer] --max [unsigned integer] --rounds [unsigned integer] --step [unsigned integer] For example: virsh domifannounce myguest 52:54:00:BE:EF:E1 --initial 100 virsh domifannounce other vnet2 virsh domifaanounce Signed-off-by: Laine Stump <laine@redhat.com> --- docs/manpages/virsh.rst | 41 +++++++++++++++++++ tools/virsh-domain.c | 88 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 129 insertions(+) diff --git a/docs/manpages/virsh.rst b/docs/manpages/virsh.rst index d1901c82c2..83ef7dd7dc 100644 --- a/docs/manpages/virsh.rst +++ b/docs/manpages/virsh.rst @@ -2321,6 +2321,47 @@ exclusive. If no flag is specified, behavior is different depending on hypervisor. +domifannounce +------------- + +**Syntax** + +:: + + domifannounce domain [interface-device] [parameters] + + +Request that a domain inject "gratuitous" ARP responses into the +outbound data stream of a specific network interface of the domain (or +if no interface-device is given, inject ARP responses on the outbound +data stream of all interfaces of the domain). This can be helpful to +re-sync network switches in the broadcast domain of said interfaces +when the network topology has changed. This is usually done +automatically when a domain is started, or after it has migrated (for +example, that is the behavior of QEMU), but a more complicated setup +where the topology changes around an already active domain (or +possibly all the plumbing isn't yet connected and passing traffic at +the time the automatic announcement takes place) might benefit from a +manually triggered announce. + +The optional parameters are: + +- *--initial n* + + initial delay before first announcement (milliseconds) default: 50 + +- *--max n* + + maximum delay between announcements (milliseconds) default: 550 + +- *--rounds n* + + total number of announcements default: 5 + +- *--step n* + + increment added to delay (milliseconds) after each announcement default 50 + dominfo ------- diff --git a/tools/virsh-domain.c b/tools/virsh-domain.c index 76369e8694..d9bc3f5a4d 100644 --- a/tools/virsh-domain.c +++ b/tools/virsh-domain.c @@ -3815,6 +3815,88 @@ cmdDomIftune(vshControl *ctl, const vshCmd *cmd) goto cleanup; } + +/* "domifannounce" command + */ +static const vshCmdInfo info_domifannounce = { + .help = N_("trigger domain to announce virtual interface to network"), + .desc = N_("trigger a live domain to announce one or more virtual interfaces to their attached networks"), + +}; + +static const vshCmdOptDef opts_domifannounce[] = { + VIRSH_COMMON_OPT_DOMAIN_FULL(0), + {.name = "interface", + .type = VSH_OT_STRING, + .positional = true, + .completer = virshDomainInterfaceCompleter, + .help = N_("interface device (MAC Address)") + }, + {.name = VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, + .type = VSH_OT_INT, + .help = N_("initial delay before first announcement (milliseconds)")}, + {.name = VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, + .type = VSH_OT_INT, + .help = N_("maximum delay between announcements (milliseconds)")}, + {.name = VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, + .type = VSH_OT_INT, + .help = N_("total number of announcements")}, + {.name = VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, + .type = VSH_OT_INT, + .help = N_("increment added to delay (milliseconds) after each announcement")}, + {.name = NULL} +}; + +static bool +cmdDomIfAnnounce(vshControl *ctl, const vshCmd *cmd) +{ + g_autoptr(virshDomain) dom = NULL; + const char *name = NULL; + const char *device = NULL; + virTypedParameterPtr params = NULL; + int nparams = 0; + int maxparams = 0; + unsigned int val; + bool ret = false; + int rv; + + if (!(dom = virshCommandOptDomain(ctl, cmd, &name))) + return false; + + if (vshCommandOptString(ctl, cmd, "interface", &device) < 0) + return false; + + if ((rv = vshCommandOptUInt(ctl, cmd, VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, &val)) < 0) + goto cleanup; + else if (rv > 0 && virTypedParamsAddUInt(¶ms, &nparams, &maxparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_INITIAL, val) < 0) + goto cleanup; + + if ((rv = vshCommandOptUInt(ctl, cmd, VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, &val)) < 0) + goto cleanup; + else if (rv > 0 && virTypedParamsAddUInt(¶ms, &nparams, &maxparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_MAX, val) < 0) + goto cleanup; + + if ((rv = vshCommandOptUInt(ctl, cmd, VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, &val)) < 0) + goto cleanup; + else if (rv > 0 && virTypedParamsAddUInt(¶ms, &nparams, &maxparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_ROUNDS, val) < 0) + goto cleanup; + + if ((rv = vshCommandOptUInt(ctl, cmd, VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, &val)) < 0) + goto cleanup; + else if (rv > 0 && virTypedParamsAddUInt(¶ms, &nparams, &maxparams, VIR_DOMAIN_ANNOUNCE_INTERFACE_STEP, val) < 0) + goto cleanup; + + if (virDomainAnnounceInterface(dom, device, params, nparams, 0) < 0) + goto cleanup; + + vshPrintExtra(ctl, _("Interface announcement sent for domain '%1$s'"), name); + ret = true; + cleanup: + virTypedParamsFree(params, nparams); + return ret; +} + + /* * "suspend" command */ @@ -14170,6 +14252,12 @@ const vshCmdDef domManagementCmds[] = { .info = &info_domid, .flags = 0 }, + {.name = "domifannounce", + .handler = cmdDomIfAnnounce, + .opts = opts_domifannounce, + .info = &info_domifannounce, + .flags = 0 + }, {.name = "domif-setlink", .handler = cmdDomIfSetLink, .opts = opts_domif_setlink, -- 2.54.0
On Tue, Jun 02, 2026 at 01:46:47AM -0400, Laine Stump via Devel wrote:
From: Laine Stump <laine@redhat.com>
virsh domifannounce is a thin wrapper around the new API virDomainAnnounceInterface(). Syntax:
virsh domifannounce guestname [interfacename] [parameters]
where the optional interfacename can be either the MAC address of the interface to announce, or the name of the tap device used to connect the domain's interface to the real network (if the connection is with a tap device), and [parameters] is one or more of the following options:
--initial [unsigned integer] --max [unsigned integer] --rounds [unsigned integer] --step [unsigned integer]
For example:
virsh domifannounce myguest 52:54:00:BE:EF:E1 --initial 100 virsh domifannounce other vnet2 virsh domifaanounce
Signed-off-by: Laine Stump <laine@redhat.com>
Reviewed-by: Martin Kletzander <mkletzan@redhat.com>
participants (3)
-
Laine Stump -
Martin Kletzander -
Peter Krempa