]> git.ipfire.org Git - thirdparty/libvirt.git/commitdiff
conf,remote: add channel lifecycle domain event
authorLucas Kornicki <lucas.kornicki@nutanix.com>
Tue, 19 May 2026 13:11:35 +0000 (15:11 +0200)
committerMichal Privoznik <mprivozn@redhat.com>
Thu, 21 May 2026 13:32:50 +0000 (15:32 +0200)
Add support for a new domain event which can be used to track
the state of any virtio channel.

Previously one could only monitor the "org.qemu.guest_agent.0" channel
which had a dedicated agent lifecycle event. The channel lifecycle event
will be emitted alongside the agent specific one.

Signed-off-by: Lucas Kornicki <lucas.kornicki@nutanix.com>
Signed-off-by: Michal Privoznik <mprivozn@redhat.com>
Reviewed-by: Michal Privoznik <mprivozn@redhat.com>
examples/c/misc/event-test.c
include/libvirt/libvirt-domain.h
src/conf/domain_event.c
src/conf/domain_event.h
src/libvirt_private.syms
src/remote/remote_daemon_dispatch.c
src/remote/remote_driver.c
src/remote/remote_protocol.x
src/remote_protocol-structs
tools/virsh-domain-event.c

index f9e65c55f0647b8810037be6bd1838ef5bf4a15c..601f5eafcf783a4b5b2050bd8a91abcb43e60a17 100644 (file)
@@ -353,6 +353,45 @@ guestAgentLifecycleEventReasonToString(int event)
     return "unknown";
 }
 
+
+static const char *
+guestChannelLifecycleEventStateToString(int event)
+{
+    switch ((virConnectDomainEventChannelLifecycleState) event) {
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_DISCONNECTED:
+        return "Disconnected";
+
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_CONNECTED:
+        return "Connected";
+
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_LAST:
+        break;
+    }
+
+    return "unknown";
+}
+
+
+static const char *
+guestChannelLifecycleEventReasonToString(int event)
+{
+    switch ((virConnectDomainEventChannelLifecycleReason) event) {
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_UNKNOWN:
+        return "Unknown";
+
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_DOMAIN_STARTED:
+        return "Domain started";
+
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_CHANNEL:
+        return "Channel event";
+
+    case VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_LAST:
+        break;
+    }
+
+    return "unknown";
+}
+
 static const char *
 storagePoolEventToString(int event)
 {
@@ -869,6 +908,23 @@ myDomainEventAgentLifecycleCallback(virConnectPtr conn G_GNUC_UNUSED,
 }
 
 
+static int
+myDomainEventChannelLifecycleCallback(virConnectPtr conn G_GNUC_UNUSED,
+                                      virDomainPtr dom,
+                                      const char *channelName,
+                                      int state,
+                                      int reason,
+                                      void *opaque G_GNUC_UNUSED)
+{
+    printf("%s EVENT: Domain %s(%d) guest channel(%s) state changed: %s reason: %s\n",
+           __func__, virDomainGetName(dom), virDomainGetID(dom), channelName,
+           guestChannelLifecycleEventStateToString(state),
+           guestChannelLifecycleEventReasonToString(reason));
+
+    return 0;
+}
+
+
 static int
 myDomainEventDeviceAddedCallback(virConnectPtr conn G_GNUC_UNUSED,
                                  virDomainPtr dom,
@@ -1195,6 +1251,7 @@ struct domainEventData domainEvents[] = {
     DOMAIN_EVENT(VIR_DOMAIN_EVENT_ID_MEMORY_DEVICE_SIZE_CHANGE, myDomainEventMemoryDeviceSizeChangeCallback),
     DOMAIN_EVENT(VIR_DOMAIN_EVENT_ID_NIC_MAC_CHANGE, myDomainEventNICMACChangeCallback),
     DOMAIN_EVENT(VIR_DOMAIN_EVENT_ID_VCPU_REMOVED, myDomainEventVcpuRemovedCallback),
+    DOMAIN_EVENT(VIR_DOMAIN_EVENT_ID_CHANNEL_LIFECYCLE, myDomainEventChannelLifecycleCallback),
 };
 
 struct storagePoolEventData {
index 1066a0b3f1438a6fa63a35b8a4f72e9033801937..8f07ef215654da0e4945f12006b003d1c3dbf789 100644 (file)
@@ -7673,6 +7673,66 @@ typedef void (*virConnectDomainEventNICMACChangeCallback)(virConnectPtr conn,
                                                           const char *newMAC,
                                                           void *opaque);
 
+
+/**
+ * virConnectDomainEventChannelLifecycleState:
+ *
+ * Since: 12.4.0
+ */
+typedef enum {
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_CONNECTED = 1, /* channel connected (Since: 12.4.0) */
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_DISCONNECTED = 2, /* channel disconnected (Since: 12.4.0) */
+
+# ifdef VIR_ENUM_SENTINELS
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_LAST /* (Since: 12.4.0) */
+# endif
+} virConnectDomainEventChannelLifecycleState;
+
+/**
+ * virConnectDomainEventChannelLifecycleReason:
+ *
+ * Since: 12.4.0
+ */
+typedef enum {
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_UNKNOWN = 0, /* unknown state change reason (Since: 12.4.0) */
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_DOMAIN_STARTED = 1, /* state changed due to domain start (Since: 12.4.0) */
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_CHANNEL = 2, /* channel state changed (Since: 12.4.0) */
+
+# ifdef VIR_ENUM_SENTINELS
+    VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_LAST /* (Since: 12.4.0) */
+# endif
+} virConnectDomainEventChannelLifecycleReason;
+
+/**
+ * virConnectDomainEventChannelLifecycleCallback:
+ * @conn: connection object
+ * @dom: domain on which the event occurred
+ * @channelName: the name of the channel on which the event occurred
+ * @state: new state of the guest channel, one of virConnectDomainEventChannelLifecycleState
+ * @reason: reason for state change, one of virConnectDomainEventChannelLifecycleReason
+ * @opaque: application specified data
+ *
+ * This callback occurs when libvirt detects a change in the state of a guest
+ * virtio-serial channel. Unlike VIR_DOMAIN_EVENT_ID_AGENT_LIFECYCLE which is
+ * tied to the QEMU guest agent channel ("org.qemu.guest_agent.0"), this event
+ * is emitted for every virtio-serial channel attached to the domain,
+ * including the guest agent channel.
+ *
+ * The hypervisor must support virtio-serial port state notifications for the
+ * event to be delivered.
+ *
+ * The callback signature to use when registering for an event of type
+ * VIR_DOMAIN_EVENT_ID_CHANNEL_LIFECYCLE with virConnectDomainEventRegisterAny()
+ *
+ * Since: 12.4.0
+ */
+typedef void (*virConnectDomainEventChannelLifecycleCallback)(virConnectPtr conn,
+                                                              virDomainPtr dom,
+                                                              const char *channelName,
+                                                              int state,
+                                                              int reason,
+                                                              void *opaque);
+
 /**
  * VIR_DOMAIN_EVENT_CALLBACK:
  *
@@ -7723,6 +7783,7 @@ typedef enum {
     VIR_DOMAIN_EVENT_ID_MEMORY_DEVICE_SIZE_CHANGE = 26, /* virConnectDomainEventMemoryDeviceSizeChangeCallback (Since: 7.9.0) */
     VIR_DOMAIN_EVENT_ID_NIC_MAC_CHANGE = 27, /* virConnectDomainEventNICMACChangeCallback (Since: 11.2.0) */
     VIR_DOMAIN_EVENT_ID_VCPU_REMOVED = 28, /* virConnectDomainEventVcpuRemovedCallback (Since: 12.4.0) */
+    VIR_DOMAIN_EVENT_ID_CHANNEL_LIFECYCLE = 29, /* virConnectDomainEventChannelLifecycleCallback (Since: 12.4.0) */
 
 # ifdef VIR_ENUM_SENTINELS
     VIR_DOMAIN_EVENT_ID_LAST
index f09c6a98164fb781961cb553a30cd48acbce6a5e..a6bf9735aa560db0b0bd19a25dec4ad402fe994e 100644 (file)
@@ -59,6 +59,7 @@ static virClass *virDomainEventBlockThresholdClass;
 static virClass *virDomainEventMemoryFailureClass;
 static virClass *virDomainEventMemoryDeviceSizeChangeClass;
 static virClass *virDomainEventNICMACChangeClass;
+static virClass *virDomainEventChannelLifecycleClass;
 
 static void virDomainEventDispose(void *obj);
 static void virDomainEventLifecycleDispose(void *obj);
@@ -85,6 +86,7 @@ static void virDomainEventBlockThresholdDispose(void *obj);
 static void virDomainEventMemoryFailureDispose(void *obj);
 static void virDomainEventMemoryDeviceSizeChangeDispose(void *obj);
 static void virDomainEventNICMACChangeDispose(void *obj);
+static void virDomainEventChannelLifecycleDispose(void *obj);
 
 static void
 virDomainEventDispatchDefaultFunc(virConnectPtr conn,
@@ -305,6 +307,27 @@ struct _virDomainEventNICMACChange {
 };
 typedef struct _virDomainEventNICMACChange virDomainEventNICMACChange;
 
+struct _virDomainEventChannelLifecycle {
+    virDomainEvent parent;
+
+    char *channelName;
+    int state;
+    int reason;
+};
+typedef struct _virDomainEventChannelLifecycle virDomainEventChannelLifecycle;
+
+/* Make sure the AGENT and CHANNEL lifecycle enums stay in sync with each other. */
+G_STATIC_ASSERT((int)VIR_CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_DOMAIN_STARTED ==
+                (int)VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_DOMAIN_STARTED);
+G_STATIC_ASSERT((int)VIR_CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_CHANNEL ==
+                (int)VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_CHANNEL);
+G_STATIC_ASSERT((int)VIR_CONNECT_DOMAIN_EVENT_AGENT_LIFECYCLE_REASON_LAST ==
+                (int)VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_LAST);
+G_STATIC_ASSERT((int)VIR_DOMAIN_CHR_DEVICE_STATE_CONNECTED ==
+                (int)VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_CONNECTED);
+G_STATIC_ASSERT((int)VIR_DOMAIN_CHR_DEVICE_STATE_DISCONNECTED ==
+                (int)VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_DISCONNECTED);
+
 static int
 virDomainEventsOnceInit(void)
 {
@@ -358,6 +381,8 @@ virDomainEventsOnceInit(void)
         return -1;
     if (!VIR_CLASS_NEW(virDomainEventNICMACChange, virDomainEventClass))
         return -1;
+    if (!VIR_CLASS_NEW(virDomainEventChannelLifecycle, virDomainEventClass))
+        return -1;
     return 0;
 }
 
@@ -600,6 +625,14 @@ virDomainEventNICMACChangeDispose(void *obj)
     g_free(event->newMAC);
 }
 
+static void
+virDomainEventChannelLifecycleDispose(void *obj)
+{
+    virDomainEventChannelLifecycle *event = obj;
+
+    g_free(event->channelName);
+}
+
 static void *
 virDomainEventNew(virClass *klass,
                   int eventID,
@@ -1867,6 +1900,61 @@ virDomainEventNICMACChangeNewFromDom(virDomainPtr dom,
 
 }
 
+
+static virObjectEvent *
+virDomainEventChannelLifecycleNew(int id,
+                                  const char *name,
+                                  const unsigned char *uuid,
+                                  const char *channelName,
+                                  int state,
+                                  int reason)
+{
+    virDomainEventChannelLifecycle *ev;
+
+    if (virDomainEventsInitialize() < 0)
+        return NULL;
+
+    if (!(ev = virDomainEventNew(virDomainEventChannelLifecycleClass,
+                                 VIR_DOMAIN_EVENT_ID_CHANNEL_LIFECYCLE,
+                                 id, name, uuid)))
+        return NULL;
+
+    ev->channelName = g_strdup(channelName);
+    ev->state = state;
+    ev->reason = reason;
+
+    return (virObjectEvent *)ev;
+}
+
+
+virObjectEvent *
+virDomainEventChannelLifecycleNewFromObj(virDomainObj *obj,
+                                         const char *channelName,
+                                         int state,
+                                         int reason)
+{
+    return virDomainEventChannelLifecycleNew(obj->def->id,
+                                             obj->def->name,
+                                             obj->def->uuid,
+                                             channelName,
+                                             state,
+                                             reason);
+}
+
+virObjectEvent *
+virDomainEventChannelLifecycleNewFromDom(virDomainPtr dom,
+                                         const char *channelName,
+                                         int state,
+                                         int reason)
+{
+    return virDomainEventChannelLifecycleNew(dom->id,
+                                             dom->name,
+                                             dom->uuid,
+                                             channelName,
+                                             state,
+                                             reason);
+}
+
 static void
 virDomainEventDispatchDefaultFunc(virConnectPtr conn,
                                   virObjectEvent *event,
@@ -2200,6 +2288,19 @@ virDomainEventDispatchDefaultFunc(virConnectPtr conn,
             goto cleanup;
         }
 
+    case VIR_DOMAIN_EVENT_ID_CHANNEL_LIFECYCLE:
+        {
+            virDomainEventChannelLifecycle *channelLifecycleEvent;
+
+            channelLifecycleEvent = (virDomainEventChannelLifecycle *)event;
+            ((virConnectDomainEventChannelLifecycleCallback)cb)(conn, dom,
+                                                                channelLifecycleEvent->channelName,
+                                                                channelLifecycleEvent->state,
+                                                                channelLifecycleEvent->reason,
+                                                                cbopaque);
+            goto cleanup;
+        }
+
     case VIR_DOMAIN_EVENT_ID_LAST:
         break;
     }
index 930b66b13a7967bb6ed1279481a215e747790c97..4678bec385b4dfe5ac50a0cfca965ffc7ac30f06 100644 (file)
@@ -295,6 +295,18 @@ virDomainEventNICMACChangeNewFromDom(virDomainPtr dom,
                                      const char *oldMAC,
                                      const char *newMAC);
 
+virObjectEvent *
+virDomainEventChannelLifecycleNewFromObj(virDomainObj *obj,
+                                         const char *channelName,
+                                         int state,
+                                         int reason);
+
+virObjectEvent *
+virDomainEventChannelLifecycleNewFromDom(virDomainPtr dom,
+                                         const char *channelName,
+                                         int state,
+                                         int reason);
+
 int
 virDomainEventStateRegister(virConnectPtr conn,
                             virObjectEventState *state,
index 7232b7c6631df5ba49ce20b9122ae4ad74f219c5..a76da45fb962a9f75fefdc247c4f1cea6273c262 100644 (file)
@@ -761,6 +761,8 @@ virDomainEventBlockJobNewFromDom;
 virDomainEventBlockJobNewFromObj;
 virDomainEventBlockThresholdNewFromDom;
 virDomainEventBlockThresholdNewFromObj;
+virDomainEventChannelLifecycleNewFromDom;
+virDomainEventChannelLifecycleNewFromObj;
 virDomainEventControlErrorNewFromDom;
 virDomainEventControlErrorNewFromObj;
 virDomainEventDeviceAddedNewFromDom;
index 55a1bd2af8d450c251975bc8f018da2e3add2c3f..329853b6da4f054f225520f8d91d45b775f272cd 100644 (file)
@@ -1379,6 +1379,39 @@ remoteRelayDomainEventNICMACChange(virConnectPtr conn,
 }
 
 
+static int
+remoteRelayDomainEventChannelLifecycle(virConnectPtr conn,
+                                       virDomainPtr dom,
+                                       const char *channelName,
+                                       int state,
+                                       int reason,
+                                       void *opaque)
+{
+    daemonClientEventCallback *callback = opaque;
+    remote_domain_event_callback_channel_lifecycle_msg data = { 0 };
+
+    if (callback->callbackID < 0 ||
+        !remoteRelayDomainEventCheckACL(callback->client, conn, dom))
+        return -1;
+
+    VIR_DEBUG("Relaying domain channel lifecycle event %s %d, callback %d, "
+              "name %s, state %d, reason %d",
+              dom->name, dom->id, callback->callbackID, channelName, state, reason);
+
+    data.callbackID = callback->callbackID;
+    make_nonnull_domain(&data.dom, dom);
+    data.channelName = g_strdup(channelName);
+    data.state = state;
+    data.reason = reason;
+
+    remoteDispatchObjectEventSend(callback->client, remoteProgram,
+                                  REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE,
+                                  (xdrproc_t)xdr_remote_domain_event_callback_channel_lifecycle_msg,
+                                  &data);
+    return 0;
+}
+
+
 static virConnectDomainEventGenericCallback domainEventCallbacks[] = {
     VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventLifecycle),
     VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventReboot),
@@ -1409,6 +1442,7 @@ static virConnectDomainEventGenericCallback domainEventCallbacks[] = {
     VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventMemoryDeviceSizeChange),
     VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventNICMACChange),
     VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventVcpuRemoved),
+    VIR_DOMAIN_EVENT_CALLBACK(remoteRelayDomainEventChannelLifecycle),
 };
 
 G_STATIC_ASSERT(G_N_ELEMENTS(domainEventCallbacks) == VIR_DOMAIN_EVENT_ID_LAST);
index e83b0abcbef6439a56ef444e3a2548f4f512545f..873e3d173cb3782edcafd5cacf0cc2b716cfe951 100644 (file)
@@ -441,6 +441,11 @@ remoteDomainBuildEventNICMACChange(virNetClientProgram *prog,
                                    virNetClient *client,
                                    void *evdata, void *opaque);
 
+static void
+remoteDomainBuildEventCallbackChannelLifecycle(virNetClientProgram *prog,
+                                               virNetClient *client,
+                                               void *evdata, void *opaque);
+
 static virNetClientProgramEvent remoteEvents[] = {
     { REMOTE_PROC_DOMAIN_EVENT_LIFECYCLE,
       remoteDomainBuildEventLifecycle,
@@ -667,6 +672,10 @@ static virNetClientProgramEvent remoteEvents[] = {
       remoteDomainBuildEventVcpuRemoved,
       sizeof(remote_domain_event_vcpu_removed_msg),
       (xdrproc_t)xdr_remote_domain_event_vcpu_removed_msg },
+    { REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE,
+      remoteDomainBuildEventCallbackChannelLifecycle,
+      sizeof(remote_domain_event_callback_channel_lifecycle_msg),
+      (xdrproc_t)xdr_remote_domain_event_callback_channel_lifecycle_msg },
 };
 
 static void
@@ -5192,6 +5201,31 @@ remoteDomainBuildEventNICMACChange(virNetClientProgram *prog G_GNUC_UNUSED,
 }
 
 
+static void
+remoteDomainBuildEventCallbackChannelLifecycle(virNetClientProgram *prog G_GNUC_UNUSED,
+                                               virNetClient *client G_GNUC_UNUSED,
+                                               void *evdata, void *opaque)
+{
+    virConnectPtr conn = opaque;
+    remote_domain_event_callback_channel_lifecycle_msg *msg = evdata;
+    struct private_data *priv = conn->privateData;
+    virDomainPtr dom;
+    virObjectEvent *event = NULL;
+
+    if (!(dom = get_nonnull_domain(conn, msg->dom)))
+        return;
+
+    event = virDomainEventChannelLifecycleNewFromDom(dom,
+                                                     msg->channelName,
+                                                     msg->state,
+                                                     msg->reason);
+
+    virObjectUnref(dom);
+
+    virObjectEventStateQueueRemote(priv->eventState, event, msg->callbackID);
+}
+
+
 static int
 remoteStreamSend(virStreamPtr st,
                  const char *data,
index 23699e99a60aaf1cfd5d7df3f136781c26e201a6..4adba82f6d09d666c272aa072791a76bd4c6595b 100644 (file)
@@ -4015,6 +4015,14 @@ struct remote_domain_event_nic_mac_change_msg {
     remote_nonnull_string newMAC;
 };
 
+struct remote_domain_event_callback_channel_lifecycle_msg {
+    int callbackID;
+    remote_nonnull_domain dom;
+    remote_nonnull_string channelName;
+    int state;
+    int reason;
+};
+
 /*----- Protocol. -----*/
 
 /* Define the program number, protocol version and procedure numbers here. */
@@ -7132,5 +7140,11 @@ enum remote_procedure {
      * @generate: both
      * @acl: none
      */
-    REMOTE_PROC_DOMAIN_EVENT_VCPU_REMOVED = 454
+    REMOTE_PROC_DOMAIN_EVENT_VCPU_REMOVED = 454,
+
+    /**
+     * @generate: both
+     * @acl: none
+     */
+    REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455
 };
index 4c24245d2b5d976b43da404a883f7eb42d0bcd8a..dd297bffff860f6253fb02b984e06d90bddf5dee 100644 (file)
@@ -3342,6 +3342,13 @@ struct remote_domain_event_nic_mac_change_msg {
         remote_nonnull_string      oldMAC;
         remote_nonnull_string      newMAC;
 };
+struct remote_domain_event_callback_channel_lifecycle_msg {
+        int                        callbackID;
+        remote_nonnull_domain      dom;
+        remote_nonnull_string      channelName;
+        int                        state;
+        int                        reason;
+};
 enum remote_procedure {
         REMOTE_PROC_CONNECT_OPEN = 1,
         REMOTE_PROC_CONNECT_CLOSE = 2,
@@ -3797,4 +3804,5 @@ enum remote_procedure {
         REMOTE_PROC_DOMAIN_DEL_THROTTLE_GROUP = 452,
         REMOTE_PROC_DOMAIN_EVENT_NIC_MAC_CHANGE = 453,
         REMOTE_PROC_DOMAIN_EVENT_VCPU_REMOVED = 454,
+        REMOTE_PROC_DOMAIN_EVENT_CALLBACK_CHANNEL_LIFECYCLE = 455,
 };
index 4a9a831b4525b307d330a1fc012d00e10ee09534..a541b155f4ff63a2805c9fd51b7d1ff727cd2bfa 100644 (file)
@@ -655,6 +655,39 @@ virshEventAgentLifecyclePrint(virConnectPtr conn G_GNUC_UNUSED,
     virshEventPrint(opaque, &buf);
 }
 
+VIR_ENUM_DECL(virshEventChannelLifecycleState);
+VIR_ENUM_IMPL(virshEventChannelLifecycleState,
+              VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_STATE_LAST,
+              N_("unknown"),
+              N_("connected"),
+              N_("disconnected"));
+
+VIR_ENUM_DECL(virshEventChannelLifecycleReason);
+VIR_ENUM_IMPL(virshEventChannelLifecycleReason,
+              VIR_CONNECT_DOMAIN_EVENT_CHANNEL_LIFECYCLE_REASON_LAST,
+              N_("unknown"),
+              N_("domain started"),
+              N_("channel event"));
+
+static void
+virshEventChannelLifecyclePrint(virConnectPtr conn G_GNUC_UNUSED,
+                                virDomainPtr dom,
+                                const char *channelName,
+                                int state,
+                                int reason,
+                                void *opaque)
+{
+    g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
+
+    virBufferAsprintf(&buf,
+                      _("event 'channel-lifecycle' for domain '%1$s': channel name: '%2$s' state: '%3$s' reason: '%4$s'\n"),
+                      virDomainGetName(dom),
+                      channelName,
+                      UNKNOWNSTR(virshEventChannelLifecycleStateTypeToString(state)),
+                      UNKNOWNSTR(virshEventChannelLifecycleReasonTypeToString(reason)));
+    virshEventPrint(opaque, &buf);
+}
+
 static void
 virshEventMigrationIterationPrint(virConnectPtr conn G_GNUC_UNUSED,
                                   virDomainPtr dom,
@@ -889,6 +922,8 @@ virshDomainEventCallback virshDomainEventCallbacks[] = {
       VIR_DOMAIN_EVENT_CALLBACK(virshEventNICMACChangePrint), },
     { "vcpu-removed",
       VIR_DOMAIN_EVENT_CALLBACK(virshEventVcpuRemovedPrint), },
+    { "channel-lifecycle",
+      VIR_DOMAIN_EVENT_CALLBACK(virshEventChannelLifecyclePrint), },
 };
 G_STATIC_ASSERT(VIR_DOMAIN_EVENT_ID_LAST == G_N_ELEMENTS(virshDomainEventCallbacks));