[libvirt PATCH v2] Metadata support for Network Objects

K Shiva posted 1 patch 10 months, 1 week ago
Patches applied successfully (tree, apply log)
git fetch https://github.com/patchew-project/libvirt tags/patchew/20230626092621.2046-1-shiva._5Fkr@riseup.net
There is a newer version of this series
include/libvirt/libvirt-domain.h    |   2 +-
include/libvirt/libvirt-network.h   |  51 ++++
include/libvirt/virterror.h         |   2 +
po/POTFILES                         |   1 +
src/conf/network_conf.c             |   3 +
src/conf/network_conf.h             |   2 +
src/conf/network_event.c            | 115 +++++++++
src/conf/network_event.h            |  11 +
src/conf/virnetworkobj.c            | 347 ++++++++++++++++++++++++++--
src/conf/virnetworkobj.h            |  56 +++++
src/driver-network.h                |  16 ++
src/libvirt-network.c               | 167 +++++++++++++
src/libvirt_public.syms             |   6 +
src/remote/remote_daemon_dispatch.c |  39 ++++
src/remote/remote_driver.c          |  32 +++
src/remote/remote_protocol.x        |  15 +-
src/remote_protocol-structs         |   6 +
src/test/test_driver.c              |  74 ++++++
src/util/virerror.c                 |   3 +
tests/meson.build                   |   1 +
tests/networkmetadatatest.c         | 297 ++++++++++++++++++++++++
tools/virsh-network.c               |  78 ++++++-
22 files changed, 1299 insertions(+), 25 deletions(-)
create mode 100644 tests/networkmetadatatest.c
[libvirt PATCH v2] Metadata support for Network Objects
Posted by K Shiva 10 months, 1 week ago
Adds the following to Network Object:
- <metadata>, <title> and <description> to the Network Schema.
- Get and Set APIs to access or modify the above.
- An async callback that notifies of metadata changes.

Resolves (GSoC 2023): https://wiki.libvirt.org/Google_Summer_of_Code_Ideas.html
Signed-off-by: K Shiva <shiva_kr@riseup.net>
---
This is a v2 of: 
https://listman.redhat.com/archives/libvir-list/2023-June/240299.html

Changes from v1:
- Corrected names in comments

 include/libvirt/libvirt-domain.h    |   2 +-
 include/libvirt/libvirt-network.h   |  51 ++++
 include/libvirt/virterror.h         |   2 +
 po/POTFILES                         |   1 +
 src/conf/network_conf.c             |   3 +
 src/conf/network_conf.h             |   2 +
 src/conf/network_event.c            | 115 +++++++++
 src/conf/network_event.h            |  11 +
 src/conf/virnetworkobj.c            | 347 ++++++++++++++++++++++++++--
 src/conf/virnetworkobj.h            |  56 +++++
 src/driver-network.h                |  16 ++
 src/libvirt-network.c               | 167 +++++++++++++
 src/libvirt_public.syms             |   6 +
 src/remote/remote_daemon_dispatch.c |  39 ++++
 src/remote/remote_driver.c          |  32 +++
 src/remote/remote_protocol.x        |  15 +-
 src/remote_protocol-structs         |   6 +
 src/test/test_driver.c              |  74 ++++++
 src/util/virerror.c                 |   3 +
 tests/meson.build                   |   1 +
 tests/networkmetadatatest.c         | 297 ++++++++++++++++++++++++
 tools/virsh-network.c               |  78 ++++++-
 22 files changed, 1299 insertions(+), 25 deletions(-)
 create mode 100644 tests/networkmetadatatest.c

diff --git a/include/libvirt/libvirt-domain.h b/include/libvirt/libvirt-domain.h
index a1902546bb..ea36805aa3 100644
--- a/include/libvirt/libvirt-domain.h
+++ b/include/libvirt/libvirt-domain.h
@@ -5184,7 +5184,7 @@ typedef void (*virConnectDomainEventDeviceRemovalFailedCallback)(virConnectPtr c
  * virConnectDomainEventMetadataChangeCallback:
  * @conn: connection object
  * @dom: domain on which the event occurred
- * @type: a value from virDomainMetadataTypea
+ * @type: a value from virDomainMetadataType
  * @nsuri: XML namespace URI
  * @opaque: application specified data
  *
diff --git a/include/libvirt/libvirt-network.h b/include/libvirt/libvirt-network.h
index 90cde0cf24..e5d25d699b 100644
--- a/include/libvirt/libvirt-network.h
+++ b/include/libvirt/libvirt-network.h
@@ -330,6 +330,7 @@ typedef void (*virConnectNetworkEventLifecycleCallback)(virConnectPtr conn,
  */
 typedef enum {
     VIR_NETWORK_EVENT_ID_LIFECYCLE = 0,       /* virConnectNetworkEventLifecycleCallback (Since: 1.2.1) */
+    VIR_NETWORK_EVENT_ID_METADATA_CHANGE = 1, /* virConnectNetworkEventMetadataChangeCallback (Since: 9.5.0) */
 
 # ifdef VIR_ENUM_SENTINELS
     VIR_NETWORK_EVENT_ID_LAST
@@ -547,4 +548,54 @@ virNetworkPortFree(virNetworkPortPtr port);
 int
 virNetworkPortRef(virNetworkPortPtr port);
 
+/**
+ * virNetworkMetadataType:
+ *
+ * Since: 9.5.0
+ */
+typedef enum {
+    VIR_NETWORK_METADATA_DESCRIPTION = 0, /* Operate on <description> (Since: 9.5.0) */
+    VIR_NETWORK_METADATA_TITLE       = 1, /* Operate on <title> (Since: 9.5.0) */
+    VIR_NETWORK_METADATA_ELEMENT     = 2, /* Operate on <metadata> (Since: 9.5.0) */
+
+# ifdef VIR_ENUM_SENTINELS
+    VIR_NETWORK_METADATA_LAST /* (Since: 9.5.0) */
+# endif
+} virNetworkMetadataType;
+
+int
+virNetworkSetMetadata(virNetworkPtr network,
+                          int type,
+                          const char *metadata,
+                          const char *key,
+                          const char *uri,
+                          unsigned int flags);
+
+char *
+virNetworkGetMetadata(virNetworkPtr network,
+                      int type,
+                      const char *uri,
+                      unsigned int flags);
+
+/**
+ * virConnectNetworkEventMetadataChangeCallback:
+ * @conn: connection object
+ * @net: network on which the event occurred
+ * @type: a value from virNetworkMetadataType
+ * @nsuri: XML namespace URI
+ * @opaque: application specified data
+ *
+ * This callback is triggered when the network XML metadata is changed
+ *
+ * The callback signature to use when registering for an event of type
+ * VIR_NETWORK_EVENT_ID_METADATA_CHANGE with virConnectNetworkEventRegisterAny().
+ *
+ * Since: 9.5.0
+ */
+typedef void (*virConnectNetworkEventMetadataChangeCallback)(virConnectPtr conn,
+                                                            virNetworkPtr net,
+                                                            int type,
+                                                            const char *nsuri,
+                                                            void *opaque);
+
 #endif /* LIBVIRT_NETWORK_H */
diff --git a/include/libvirt/virterror.h b/include/libvirt/virterror.h
index df13e4f11e..2910ff03da 100644
--- a/include/libvirt/virterror.h
+++ b/include/libvirt/virterror.h
@@ -348,6 +348,8 @@ typedef enum {
     VIR_ERR_NO_HOSTNAME = 108,          /* no domain's hostname found (Since: 6.1.0) */
     VIR_ERR_CHECKPOINT_INCONSISTENT = 109, /* checkpoint can't be used (Since: 6.10.0) */
     VIR_ERR_MULTIPLE_DOMAINS = 110,     /* more than one matching domain found (Since: 7.1.0) */
+    VIR_ERR_NO_NETWORK_METADATA = 111,  /* Network metadata is not present (Since: 9.5.0) */
+
 
 # ifdef VIR_ENUM_SENTINELS
     VIR_ERR_NUMBER_LAST /* (Since: 5.0.0) */
diff --git a/po/POTFILES b/po/POTFILES
index 5d6ec195b4..933a2e07a4 100644
--- a/po/POTFILES
+++ b/po/POTFILES
@@ -39,6 +39,7 @@ src/conf/netdev_bandwidth_conf.c
 src/conf/netdev_vlan_conf.c
 src/conf/netdev_vport_profile_conf.c
 src/conf/network_conf.c
+src/conf/network_event.c
 src/conf/networkcommon_conf.c
 src/conf/node_device_conf.c
 src/conf/node_device_util.c
diff --git a/src/conf/network_conf.c b/src/conf/network_conf.c
index 73788b6d87..84952db041 100644
--- a/src/conf/network_conf.c
+++ b/src/conf/network_conf.c
@@ -2546,6 +2546,9 @@ virNetworkSaveXML(const char *configDir,
     char uuidstr[VIR_UUID_STRING_BUFLEN];
     g_autofree char *configFile = NULL;
 
+    if (!configDir)
+        return 0;
+
     if ((configFile = virNetworkConfigFile(configDir, def->name)) == NULL)
         return -1;
 
diff --git a/src/conf/network_conf.h b/src/conf/network_conf.h
index 2b2e9d15f0..5a1bdb1284 100644
--- a/src/conf/network_conf.h
+++ b/src/conf/network_conf.h
@@ -249,6 +249,8 @@ struct _virNetworkDef {
     unsigned char uuid[VIR_UUID_BUFLEN];
     bool uuid_specified;
     char *name;
+    char *title;
+    char *description;
     int   connections; /* # of guest interfaces connected to this network */
 
     char *bridge;       /* Name of bridge device */
diff --git a/src/conf/network_event.c b/src/conf/network_event.c
index 6f25e43711..0e12cc2687 100644
--- a/src/conf/network_event.c
+++ b/src/conf/network_event.c
@@ -26,6 +26,9 @@
 #include "object_event_private.h"
 #include "datatypes.h"
 #include "virlog.h"
+#include "virerror.h"
+
+#define VIR_FROM_THIS VIR_FROM_NETWORK
 
 VIR_LOG_INIT("conf.network_event");
 
@@ -45,10 +48,21 @@ struct _virNetworkEventLifecycle {
 };
 typedef struct _virNetworkEventLifecycle virNetworkEventLifecycle;
 
+struct _virNetworkEventMetadataChange {
+    virNetworkEvent parent;
+
+    int type;
+    char *nsuri;
+};
+typedef struct _virNetworkEventMetadataChange virNetworkEventMetadataChange;
+
 static virClass *virNetworkEventClass;
 static virClass *virNetworkEventLifecycleClass;
+static virClass *virNetworkEventMetadataChangeClass;
+
 static void virNetworkEventDispose(void *obj);
 static void virNetworkEventLifecycleDispose(void *obj);
+static void virNetworkEventMetadataChangeDispose(void *obj);
 
 static int
 virNetworkEventsOnceInit(void)
@@ -59,6 +73,9 @@ virNetworkEventsOnceInit(void)
     if (!VIR_CLASS_NEW(virNetworkEventLifecycle, virNetworkEventClass))
         return -1;
 
+    if (!VIR_CLASS_NEW(virNetworkEventMetadataChange, virNetworkEventClass))
+        return -1;
+
     return 0;
 }
 
@@ -104,9 +121,22 @@ virNetworkEventDispatchDefaultFunc(virConnectPtr conn,
             return;
         }
 
+    case VIR_NETWORK_EVENT_ID_METADATA_CHANGE:
+        {
+            virNetworkEventMetadataChange *metadataChangeEvent;
+
+            metadataChangeEvent = (virNetworkEventMetadataChange *)event;
+            ((virConnectNetworkEventMetadataChangeCallback)cb)(conn, net,
+                                                              metadataChangeEvent->type,
+                                                              metadataChangeEvent->nsuri,
+                                                              cbopaque);
+            return;
+        }
+
     case VIR_NETWORK_EVENT_ID_LAST:
         break;
     }
+
     VIR_WARN("Unexpected event ID %d", event->eventID);
 }
 
@@ -231,3 +261,88 @@ virNetworkEventLifecycleNew(const char *name,
 
     return (virObjectEvent *)event;
 }
+
+
+static void *
+virNetworkEventNew(virClass *klass,
+                  int eventID,
+                  const char *name,
+                  const unsigned char *uuid)
+{
+    virNetworkEvent *event;
+    char uuidstr[VIR_UUID_STRING_BUFLEN];
+
+    if (virNetworkEventsInitialize() < 0)
+        return NULL;
+
+    if (!virClassIsDerivedFrom(klass, virNetworkEventClass)) {
+        virReportInvalidArg(klass,
+                            _("Class %1$s must derive from virNetworkEvent"),
+                            virClassName(klass));
+        return NULL;
+    }
+
+    /* We use uuid for matching key. We ignore 'name' because
+     * Xen sometimes renames guests during migration, thus
+     * 'uuid' is the only truly reliable key we can use. */
+    virUUIDFormat(uuid, uuidstr);
+    if (!(event = virObjectEventNew(klass,
+                                    virNetworkEventDispatchDefaultFunc,
+                                    eventID,
+                                    0, name, uuid, uuidstr)))
+        return NULL;
+
+    return (virObjectEvent *)event;
+}
+
+
+static void
+virNetworkEventMetadataChangeDispose(void *obj)
+{
+    virNetworkEventMetadataChange *event = obj;
+    VIR_DEBUG("obj=%p", event);
+
+    g_free(event->nsuri);
+}
+
+
+static virObjectEvent *
+virNetworkEventMetadataChangeNew(const char *name,
+                                 unsigned char *uuid,
+                                 int type,
+                                 const char *nsuri)
+{
+    virNetworkEventMetadataChange *ev;
+
+    if (virNetworkEventsInitialize() < 0)
+        return NULL;
+
+    if (!(ev = virNetworkEventNew(virNetworkEventMetadataChangeClass,
+                                 VIR_NETWORK_EVENT_ID_METADATA_CHANGE,
+                                 name, uuid)))
+        return NULL;
+
+    ev->type = type;
+    if (nsuri)
+        ev->nsuri = g_strdup(nsuri);
+
+    return (virObjectEvent *)ev;
+}
+
+virObjectEvent *
+virNetworkEventMetadataChangeNewFromObj(virNetworkObj *obj,
+                                       int type,
+                                       const char *nsuri)
+{
+    return virNetworkEventMetadataChangeNew(obj->def->name,
+                                           obj->def->uuid, type, nsuri);
+}
+
+virObjectEvent *
+virNetworkEventMetadataChangeNewFromNet(virNetworkPtr net,
+                                       int type,
+                                       const char *nsuri)
+{
+    return virNetworkEventMetadataChangeNew(net->name, net->uuid,
+                                           type, nsuri);
+}
diff --git a/src/conf/network_event.h b/src/conf/network_event.h
index 4502bfcaef..7c98a6ac92 100644
--- a/src/conf/network_event.h
+++ b/src/conf/network_event.h
@@ -23,6 +23,7 @@
 
 #include "internal.h"
 #include "object_event.h"
+#include "virnetworkobj.h"
 
 int
 virNetworkEventStateRegisterID(virConnectPtr conn,
@@ -53,3 +54,13 @@ virNetworkEventLifecycleNew(const char *name,
                             const unsigned char *uuid,
                             int type,
                             int detail);
+
+virObjectEvent *
+virNetworkEventMetadataChangeNewFromObj(virNetworkObj *obj,
+                                       int type,
+                                       const char *nsuri);
+
+virObjectEvent *
+virNetworkEventMetadataChangeNewFromNet(virNetworkPtr net,
+                                       int type,
+                                       const char *nsuri);
diff --git a/src/conf/virnetworkobj.c b/src/conf/virnetworkobj.c
index b8b86da06f..82f90937bc 100644
--- a/src/conf/virnetworkobj.c
+++ b/src/conf/virnetworkobj.c
@@ -39,28 +39,6 @@ VIR_LOG_INIT("conf.virnetworkobj");
  * that big. */
 #define INIT_CLASS_ID_BITMAP_SIZE (1<<4)
 
-struct _virNetworkObj {
-    virObjectLockable parent;
-
-    pid_t dnsmasqPid;
-    bool active;
-    bool autostart;
-    bool persistent;
-
-    virNetworkDef *def; /* The current definition */
-    virNetworkDef *newDef; /* New definition to activate at shutdown */
-
-    virBitmap *classIdMap; /* bitmap of class IDs for QoS */
-    unsigned long long floor_sum; /* sum of all 'floor'-s of attached NICs */
-
-    unsigned int taint;
-
-    /* Immutable pointer, self locking APIs */
-    virMacMap *macmap;
-
-    GHashTable *ports; /* uuid -> virNetworkPortDef **/
-};
-
 struct _virNetworkObjList {
     virObjectRWLockable parent;
 
@@ -1822,3 +1800,328 @@ virNetworkObjLoadAllPorts(virNetworkObj *net,
 
     return 0;
 }
+
+
+/**
+ * virNetworkObjUpdateModificationImpact:
+ *
+ * @net: network object
+ * @flags: flags to update the modification impact on
+ *
+ * Resolves virNetworkUpdateFlags in @flags so that they correctly
+ * apply to the actual state of @net. @flags may be modified after call to this
+ * function.
+ *
+ * Returns 0 on success if @flags point to a valid combination for @net or -1 on
+ * error.
+ */
+int
+virNetworkObjUpdateModificationImpact(virNetworkObj *net,
+                                     unsigned int *flags)
+{
+    bool isActive = virNetworkObjIsActive(net);
+
+    if ((*flags & (VIR_NETWORK_UPDATE_AFFECT_LIVE | VIR_NETWORK_UPDATE_AFFECT_CONFIG)) ==
+        VIR_NETWORK_UPDATE_AFFECT_CURRENT) {
+        if (isActive)
+            *flags |= VIR_NETWORK_UPDATE_AFFECT_LIVE;
+        else
+            *flags |= VIR_NETWORK_UPDATE_AFFECT_CONFIG;
+    }
+
+    if (!isActive && (*flags & VIR_NETWORK_UPDATE_AFFECT_LIVE)) {
+        virReportError(VIR_ERR_OPERATION_INVALID, "%s",
+                       _("network is not running"));
+        return -1;
+    }
+
+    if (!net->persistent && (*flags & VIR_NETWORK_UPDATE_AFFECT_CONFIG)) {
+        virReportError(VIR_ERR_OPERATION_INVALID, "%s",
+                       _("transient networks do not have any "
+                         "persistent config"));
+        return -1;
+    }
+
+    return 0;
+}
+
+
+/**
+ * virNetworkObjGetDefs:
+ *
+ * @net: network object
+ * @flags: for virNetworkUpdateFlags
+ * @liveDef: Set the pointer to the live definition of @net.
+ * @persDef: Set the pointer to the config definition of @net.
+ *
+ * Helper function to resolve @flags and retrieve correct network pointer
+ * objects. This function should be used only when the network driver
+ * creates net->newDef once the network has started.
+ *
+ * If @liveDef or @persDef are set it implies that @flags request modification
+ * thereof.
+ *
+ * Returns 0 on success and sets @liveDef and @persDef; -1 if @flags are
+ * inappropriate.
+ */
+int
+virNetworkObjGetDefs(virNetworkObj *net,
+                    unsigned int flags,
+                    virNetworkDef **liveDef,
+                    virNetworkDef **persDef)
+{
+    if (liveDef)
+        *liveDef = NULL;
+
+    if (persDef)
+        *persDef = NULL;
+
+    if (virNetworkObjUpdateModificationImpact(net, &flags) < 0)
+        return -1;
+
+    if (virNetworkObjIsActive(net)) {
+        if (liveDef && (flags & VIR_NETWORK_UPDATE_AFFECT_LIVE))
+            *liveDef = net->def;
+
+        if (persDef && (flags & VIR_NETWORK_UPDATE_AFFECT_CONFIG))
+            *persDef = net->newDef;
+    } else {
+        if (persDef)
+            *persDef = net->def;
+    }
+
+    return 0;
+}
+
+
+/**
+ * virNetworkObjGetOneDefState:
+ *
+ * @net: Network object
+ * @flags: for virNetworkUpdateFlags
+ * @live: set to true if live config was returned (may be omitted)
+ *
+ * Helper function to resolve @flags and return the correct network pointer
+ * object. This function returns one of @net->def or @net->persistentDef
+ * according to @flags. @live is set to true if the live net config will be
+ * returned. This helper should be used only in APIs that guarantee
+ * that @flags contains exactly one of VIR_NETWORK_UPDATE_AFFECT_LIVE or
+ * VIR_NETWORK_UPDATE_AFFECT_CONFIG and not both.
+ *
+ * Returns the correct definition pointer or NULL on error.
+ */
+virNetworkDef *
+virNetworkObjGetOneDefState(virNetworkObj *net,
+                           unsigned int flags,
+                           bool *live)
+{
+    if (flags & VIR_NETWORK_UPDATE_AFFECT_LIVE &&
+        flags & VIR_NETWORK_UPDATE_AFFECT_CONFIG) {
+        virReportInvalidArg(flags, "%s",
+                            _("Flags 'VIR_NETWORK_UPDATE_AFFECT_LIVE' and "
+                              "'VIR_NETWORK_UPDATE_AFFECT_CONFIG' are mutually "
+                              "exclusive"));
+        return NULL;
+    }
+
+    if (virNetworkObjUpdateModificationImpact(net, &flags) < 0)
+        return NULL;
+
+    if (live)
+        *live = flags & VIR_NETWORK_UPDATE_AFFECT_LIVE;
+
+    if (virNetworkObjIsActive(net) && flags & VIR_NETWORK_UPDATE_AFFECT_CONFIG)
+        return net->newDef;
+
+    return net->def;
+}
+
+
+/**
+ * virNetworkObjGetOneDef:
+ *
+ * @net: Network object
+ * @flags: for virNetworkUpdateFlags
+ *
+ * Helper function to resolve @flags and return the correct network pointer
+ * object. This function returns one of @net->def or @net->persistentDef
+ * according to @flags. This helper should be used only in APIs that guarantee
+ * that @flags contains exactly one of VIR_NETWORK_UPDATE_AFFECT_LIVE or
+ * VIR_NETWORK_UPDATE_AFFECT_CONFIG and not both.
+ *
+ * Returns the correct definition pointer or NULL on error.
+ */
+virNetworkDef *
+virNetworkObjGetOneDef(virNetworkObj *net,
+                      unsigned int flags)
+{
+    return virNetworkObjGetOneDefState(net, flags, NULL);
+}
+
+
+char *
+virNetworkObjGetMetadata(virNetworkObj *net,
+                        int type,
+                        const char *uri,
+                        unsigned int flags)
+{
+    virNetworkDef *def;
+    char *ret = NULL;
+
+    virCheckFlags(VIR_NETWORK_UPDATE_AFFECT_LIVE |
+                  VIR_NETWORK_UPDATE_AFFECT_CONFIG, NULL);
+
+    if (type >= VIR_NETWORK_METADATA_LAST) {
+        virReportError(VIR_ERR_INVALID_ARG,
+                       _("unknown metadata type '%1$d'"), type);
+        return NULL;
+    }
+
+    if (!(def = virNetworkObjGetOneDef(net, flags)))
+        return NULL;
+
+    switch ((virNetworkMetadataType) type) {
+    case VIR_NETWORK_METADATA_DESCRIPTION:
+        ret = g_strdup(def->description);
+        break;
+
+    case VIR_NETWORK_METADATA_TITLE:
+        ret = g_strdup(def->title);
+        break;
+
+    case VIR_NETWORK_METADATA_ELEMENT:
+        if (!def->metadata)
+            break;
+
+        if (virXMLExtractNamespaceXML(def->metadata, uri, &ret) < 0)
+            return NULL;
+        break;
+
+    case VIR_NETWORK_METADATA_LAST:
+        break;
+    }
+
+    if (!ret)
+        virReportError(VIR_ERR_NO_NETWORK_METADATA, "%s",
+                       _("Requested metadata element is not present"));
+
+    return ret;
+}
+
+
+static int
+virNetworkDefSetMetadata(virNetworkDef *def,
+                        int type,
+                        const char *metadata,
+                        const char *key,
+                        const char *uri)
+{
+    g_autoptr(xmlDoc) doc = NULL;
+    xmlNodePtr old;
+    g_autoptr(xmlNode) new = NULL;
+
+    if (type >= VIR_NETWORK_METADATA_LAST) {
+        virReportError(VIR_ERR_INVALID_ARG,
+                       _("unknown metadata type '%1$d'"), type);
+        return -1;
+    }
+
+    switch ((virNetworkMetadataType) type) {
+    case VIR_NETWORK_METADATA_DESCRIPTION:
+        g_clear_pointer(&def->description, g_free);
+
+        if (STRNEQ_NULLABLE(metadata, ""))
+            def->description = g_strdup(metadata);
+        break;
+
+    case VIR_NETWORK_METADATA_TITLE:
+        g_clear_pointer(&def->title, g_free);
+
+        if (STRNEQ_NULLABLE(metadata, ""))
+            def->title = g_strdup(metadata);
+        break;
+
+    case VIR_NETWORK_METADATA_ELEMENT:
+        if (metadata) {
+
+            /* parse and modify the xml from the user */
+            if (!(doc = virXMLParseStringCtxt(metadata, _("(metadata_xml)"), NULL)))
+                return -1;
+
+            if (virXMLInjectNamespace(doc->children, uri, key) < 0)
+                return -1;
+
+            /* create the root node if needed */
+            if (!def->metadata)
+                def->metadata = virXMLNewNode(NULL, "metadata");
+
+            if (!(new = xmlCopyNode(doc->children, 1))) {
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                               _("Failed to copy XML node"));
+                return -1;
+            }
+        }
+
+        /* remove possible other nodes sharing the namespace */
+        while ((old = virXMLFindChildNodeByNs(def->metadata, uri))) {
+            xmlUnlinkNode(old);
+            xmlFreeNode(old);
+        }
+
+        if (new) {
+            if (!(xmlAddChild(def->metadata, new))) {
+                virReportError(VIR_ERR_INTERNAL_ERROR, "%s",
+                               _("failed to add metadata to XML document"));
+                return -1;
+            }
+            new = NULL;
+        }
+        break;
+
+    case VIR_NETWORK_METADATA_LAST:
+        break;
+    }
+
+    return 0;
+}
+
+
+int
+virNetworkObjSetMetadata(virNetworkObj *net,
+                        int type,
+                        const char *metadata,
+                        const char *key,
+                        const char *uri,
+                        virNetworkXMLOption *xmlopt,
+                        const char *stateDir,
+                        const char *configDir,
+                        unsigned int flags)
+{
+    virNetworkDef *def;
+    virNetworkDef *persistentDef;
+
+    virCheckFlags(VIR_NETWORK_UPDATE_AFFECT_LIVE |
+                  VIR_NETWORK_UPDATE_AFFECT_CONFIG, -1);
+
+    if (virNetworkObjGetDefs(net, flags, &def, &persistentDef) < 0)
+        return -1;
+
+    if (def) {
+        if (virNetworkDefSetMetadata(def, type, metadata, key, uri) < 0)
+            return -1;
+
+        if (virNetworkObjSaveStatus(stateDir, net, xmlopt) < 0)
+            return -1;
+    }
+
+    if (persistentDef) {
+        if (virNetworkDefSetMetadata(persistentDef, type, metadata, key,
+                                    uri) < 0)
+            return -1;
+
+        if (virNetworkSaveConfig(configDir, persistentDef, xmlopt) < 0)
+            return -1;
+    }
+
+    return 0;
+}
diff --git a/src/conf/virnetworkobj.h b/src/conf/virnetworkobj.h
index 7d34fa3204..d17a43d7bb 100644
--- a/src/conf/virnetworkobj.h
+++ b/src/conf/virnetworkobj.h
@@ -26,6 +26,28 @@
 
 typedef struct _virNetworkObj virNetworkObj;
 
+struct _virNetworkObj {
+    virObjectLockable parent;
+
+    pid_t dnsmasqPid;
+    bool active;
+    bool autostart;
+    bool persistent;
+
+    virNetworkDef *def; /* The current definition */
+    virNetworkDef *newDef; /* New definition to activate at shutdown */
+
+    virBitmap *classIdMap; /* bitmap of class IDs for QoS */
+    unsigned long long floor_sum; /* sum of all 'floor'-s of attached NICs */
+
+    unsigned int taint;
+
+    /* Immutable pointer, self locking APIs */
+    virMacMap *macmap;
+
+    GHashTable *ports; /* uuid -> virNetworkPortDef **/
+};
+
 virNetworkObj *
 virNetworkObjNew(void);
 
@@ -258,3 +280,37 @@ virNetworkObjListNumOfNetworks(virNetworkObjList *nets,
 void
 virNetworkObjListPrune(virNetworkObjList *nets,
                        unsigned int flags);
+
+int virNetworkObjUpdateModificationImpact(virNetworkObj *net,
+                                          unsigned int *flags);
+
+int
+virNetworkObjGetDefs(virNetworkObj *net,
+                    unsigned int flags,
+                    virNetworkDef **liveDef,
+                    virNetworkDef **persDef);
+
+virNetworkDef *
+virNetworkObjGetOneDefState(virNetworkObj *net,
+                            unsigned int flags,
+                            bool *state);
+virNetworkDef *
+virNetworkObjGetOneDef(virNetworkObj *net,
+                       unsigned int flags);
+
+char *
+virNetworkObjGetMetadata(virNetworkObj *network,
+                         int type,
+                         const char *uri,
+                         unsigned int flags);
+
+int
+virNetworkObjSetMetadata(virNetworkObj *network,
+                         int type,
+                         const char *metadata,
+                         const char *key,
+                         const char *uri,
+                         virNetworkXMLOption *xmlopt,
+                         const char *stateDir,
+                         const char *configDir,
+                         unsigned int flags);
diff --git a/src/driver-network.h b/src/driver-network.h
index 99efd4c8aa..1d19b013c9 100644
--- a/src/driver-network.h
+++ b/src/driver-network.h
@@ -161,6 +161,20 @@ typedef int
                              virNetworkPortPtr **ports,
                              unsigned int flags);
 
+typedef int
+(*virDrvNetworkSetMetadata)(virNetworkPtr network,
+                            int type,
+                            const char *metadata,
+                            const char *key,
+                            const char *uri,
+                            unsigned int flags);
+
+typedef char *
+(*virDrvNetworkGetMetadata)(virNetworkPtr network,
+                            int type,
+                            const char *uri,
+                            unsigned int flags);
+
 typedef struct _virNetworkDriver virNetworkDriver;
 
 /**
@@ -202,4 +216,6 @@ struct _virNetworkDriver {
     virDrvNetworkPortGetParameters networkPortGetParameters;
     virDrvNetworkPortDelete networkPortDelete;
     virDrvNetworkListAllPorts networkListAllPorts;
+    virDrvNetworkSetMetadata networkSetMetadata;
+    virDrvNetworkGetMetadata networkGetMetadata;
 };
diff --git a/src/libvirt-network.c b/src/libvirt-network.c
index 236dfe2f5d..c0c66bb2fa 100644
--- a/src/libvirt-network.c
+++ b/src/libvirt-network.c
@@ -1915,3 +1915,170 @@ virNetworkPortRef(virNetworkPortPtr port)
     virObjectRef(port);
     return 0;
 }
+
+
+/**
+ * virNetworkSetMetadata:
+ * @network: a network object
+ * @type: type of metadata, from virNetworkMetadataType
+ * @metadata: new metadata text
+ * @key: XML namespace key, or NULL
+ * @uri: XML namespace URI, or NULL
+ * @flags: bitwise-OR of virNetworkUpdateFlags
+ *
+ * Sets the appropriate network element given by @type to the
+ * value of @metadata.  A @type of VIR_NETWORK_METADATA_DESCRIPTION
+ * is free-form text; VIR_NETWORK_METADATA_TITLE is free-form, but no
+ * newlines are permitted, and should be short (although the length is
+ * not enforced). For these two options @key and @uri are irrelevant and
+ * must be set to NULL.
+ *
+ * For type VIR_NETWORK_METADATA_ELEMENT @metadata  must be well-formed
+ * XML belonging to namespace defined by @uri with local name @key.
+ *
+ * Passing NULL for @metadata says to remove that element from the
+ * network XML (passing the empty string leaves the element present).
+ *
+ * The resulting metadata will be present in virNetworkGetXMLDesc(),
+ * as well as quick access through virNetworkGetMetadata().
+ *
+ * @flags controls whether the live network state, persistent configuration,
+ * or both will be modified.
+ *
+ * Returns 0 on success, -1 in case of failure.
+ *
+ * Since: 9.5.0
+ */
+int
+virNetworkSetMetadata(virNetworkPtr network,
+                     int type,
+                     const char *metadata,
+                     const char *key,
+                     const char *uri,
+                     unsigned int flags)
+{
+    virConnectPtr conn;
+
+    VIR_DEBUG("network=%p, type=%d, metadata='%s', key='%s', uri='%s', flags=0x%x",
+              network, type, NULLSTR(metadata), NULLSTR(key), NULLSTR(uri),
+              flags);
+
+    virResetLastError();
+
+    virCheckNetworkReturn(network, -1);
+    conn = network->conn;
+
+    virCheckReadOnlyGoto(conn->flags, error);
+
+    switch (type) {
+    case VIR_NETWORK_METADATA_TITLE:
+        if (metadata && strchr(metadata, '\n')) {
+            virReportInvalidArg(metadata, "%s",
+                                _("metadata title can't contain "
+                                  "newlines"));
+            goto error;
+        }
+        G_GNUC_FALLTHROUGH;
+    case VIR_NETWORK_METADATA_DESCRIPTION:
+        virCheckNullArgGoto(uri, error);
+        virCheckNullArgGoto(key, error);
+        break;
+    case VIR_NETWORK_METADATA_ELEMENT:
+        virCheckNonNullArgGoto(uri, error);
+        if (metadata)
+            virCheckNonNullArgGoto(key, error);
+        break;
+    default:
+        /* For future expansion */
+        break;
+    }
+
+    if (conn->networkDriver->networkSetMetadata) {
+        int ret;
+        ret = conn->networkDriver->networkSetMetadata(network, type, metadata, key, uri,
+                                               flags);
+        if (ret < 0)
+            goto error;
+        return ret;
+    }
+
+    virReportUnsupportedError();
+
+ error:
+    virDispatchError(network->conn);
+    return -1;
+}
+
+
+/**
+ * virNetworkGetMetadata:
+ * @network: a network object
+ * @type: type of metadata, from virNetworkMetadataType
+ * @uri: XML namespace identifier
+ * @flags: bitwise-OR of virNetworkUpdateFlags
+ *
+ * Retrieves the appropriate network element given by @type.
+ * If VIR_NETWORK_METADATA_ELEMENT is requested parameter @uri
+ * must be set to the name of the namespace the requested elements
+ * belong to, otherwise must be NULL.
+ *
+ * If an element of the network XML is not present, the resulting
+ * error will be VIR_ERR_NO_NETWORK_METADATA.  This method forms
+ * a shortcut for seeing information from virNetworkSetMetadata()
+ * without having to go through virNetworkGetXMLDesc().
+ *
+ * @flags controls whether the live network state or persistent
+ * configuration will be queried.
+ *
+ * Returns the metadata string on success (caller must free),
+ * or NULL in case of failure.
+ *
+ * Since: 9.5.0
+ */
+char *
+virNetworkGetMetadata(virNetworkPtr network,
+                     int type,
+                     const char *uri,
+                     unsigned int flags)
+{
+    virConnectPtr conn;
+
+    VIR_DEBUG("network=%p, type=%d, uri='%s', flags=0x%x",
+               network, type, NULLSTR(uri), flags);
+
+    virResetLastError();
+
+    virCheckNetworkReturn(network, NULL);
+
+    VIR_EXCLUSIVE_FLAGS_GOTO(VIR_NETWORK_UPDATE_AFFECT_LIVE,
+                             VIR_NETWORK_UPDATE_AFFECT_CONFIG,
+                             error);
+
+    switch (type) {
+    case VIR_NETWORK_METADATA_TITLE:
+    case VIR_NETWORK_METADATA_DESCRIPTION:
+        virCheckNullArgGoto(uri, error);
+        break;
+    case VIR_NETWORK_METADATA_ELEMENT:
+        virCheckNonNullArgGoto(uri, error);
+        break;
+    default:
+        /* For future expansion */
+        break;
+    }
+
+    conn = network->conn;
+
+    if (conn->networkDriver->networkGetMetadata) {
+        char *ret;
+        if (!(ret = conn->networkDriver->networkGetMetadata(network, type, uri, flags)))
+            goto error;
+        return ret;
+    }
+
+    virReportUnsupportedError();
+
+ error:
+    virDispatchError(network->conn);
+    return NULL;
+}
diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms
index 80742f268e..d21fe49caa 100644
--- a/src/libvirt_public.syms
+++ b/src/libvirt_public.syms
@@ -932,4 +932,10 @@ LIBVIRT_9.0.0 {
         virDomainFDAssociate;
 } LIBVIRT_8.5.0;
 
+LIBVIRT_9.5.0 {
+    global:
+        virNetworkGetMetadata;
+        virNetworkSetMetadata;
+} LIBVIRT_9.0.0;
+
 # .... define new API here using predicted next version number ....
diff --git a/src/remote/remote_daemon_dispatch.c b/src/remote/remote_daemon_dispatch.c
index 7144e9e7ca..3e5eaec9e6 100644
--- a/src/remote/remote_daemon_dispatch.c
+++ b/src/remote/remote_daemon_dispatch.c
@@ -1420,8 +1420,47 @@ remoteRelayNetworkEventLifecycle(virConnectPtr conn,
     return 0;
 }
 
+static int
+remoteRelayNetworkEventMetadataChange(virConnectPtr conn,
+                                     virNetworkPtr net,
+                                     int type,
+                                     const char *nsuri,
+                                     void *opaque)
+{
+    daemonClientEventCallback *callback = opaque;
+    remote_network_event_callback_metadata_change_msg data;
+
+    if (callback->callbackID < 0 ||
+        !remoteRelayNetworkEventCheckACL(callback->client, conn, net))
+        return -1;
+
+    VIR_DEBUG("Relaying network metadata change %s %d %s, callback %d",
+              net->name, type, NULLSTR(nsuri), callback->callbackID);
+
+    /* build return data */
+    memset(&data, 0, sizeof(data));
+
+    data.type = type;
+    if (nsuri) {
+        data.nsuri = g_new0(remote_nonnull_string, 1);
+        *(data.nsuri) = g_strdup(nsuri);
+    }
+
+    make_nonnull_network(&data.net, net);
+    data.callbackID = callback->callbackID;
+
+    remoteDispatchObjectEventSend(callback->client, callback->program,
+                                  REMOTE_PROC_NETWORK_EVENT_CALLBACK_METADATA_CHANGE,
+                                  (xdrproc_t)xdr_remote_network_event_callback_metadata_change_msg,
+                                  &data);
+
+    return 0;
+}
+
+
 static virConnectNetworkEventGenericCallback networkEventCallbacks[] = {
     VIR_NETWORK_EVENT_CALLBACK(remoteRelayNetworkEventLifecycle),
+    VIR_NETWORK_EVENT_CALLBACK(remoteRelayNetworkEventMetadataChange),
 };
 
 G_STATIC_ASSERT(G_N_ELEMENTS(networkEventCallbacks) == VIR_NETWORK_EVENT_ID_LAST);
diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c
index 65ec239fb7..310f53fe5e 100644
--- a/src/remote/remote_driver.c
+++ b/src/remote/remote_driver.c
@@ -378,6 +378,12 @@ remoteNetworkBuildEventLifecycle(virNetClientProgram *prog G_GNUC_UNUSED,
                                  virNetClient *client G_GNUC_UNUSED,
                                  void *evdata, void *opaque);
 
+static void
+remoteNetworkBuildEventCallbackMetadataChange(virNetClientProgram *prog,
+                                             virNetClient *client,
+                                             void *evdata, void *opaque);
+
+
 static void
 remoteStoragePoolBuildEventLifecycle(virNetClientProgram *prog G_GNUC_UNUSED,
                                      virNetClient *client G_GNUC_UNUSED,
@@ -505,6 +511,10 @@ static virNetClientProgramEvent remoteEvents[] = {
       remoteNetworkBuildEventLifecycle,
       sizeof(remote_network_event_lifecycle_msg),
       (xdrproc_t)xdr_remote_network_event_lifecycle_msg },
+    { REMOTE_PROC_NETWORK_EVENT_CALLBACK_METADATA_CHANGE,
+      remoteNetworkBuildEventCallbackMetadataChange,
+      sizeof(remote_network_event_callback_metadata_change_msg),
+      (xdrproc_t)xdr_remote_network_event_callback_metadata_change_msg },
     { REMOTE_PROC_DOMAIN_EVENT_CALLBACK_LIFECYCLE,
       remoteDomainBuildEventCallbackLifecycle,
       sizeof(remote_domain_event_callback_lifecycle_msg),
@@ -4951,6 +4961,28 @@ remoteNetworkBuildEventLifecycle(virNetClientProgram *prog G_GNUC_UNUSED,
     virObjectEventStateQueueRemote(priv->eventState, event, msg->callbackID);
 }
 
+static void
+remoteNetworkBuildEventCallbackMetadataChange(virNetClientProgram *prog G_GNUC_UNUSED,
+                                             virNetClient *client G_GNUC_UNUSED,
+                                             void *evdata, void *opaque)
+{
+    virConnectPtr conn = opaque;
+    remote_network_event_callback_metadata_change_msg *msg = evdata;
+    struct private_data *priv = conn->privateData;
+    virNetworkPtr net;
+    virObjectEvent *event = NULL;
+
+    if (!(net = get_nonnull_network(conn, msg->net)))
+        return;
+
+    event = virNetworkEventMetadataChangeNewFromNet(net, msg->type, msg->nsuri ? *msg->nsuri : NULL);
+
+    virObjectUnref(net);
+
+    virObjectEventStateQueueRemote(priv->eventState, event, msg->callbackID);
+}
+
+
 static void
 remoteStoragePoolBuildEventLifecycle(virNetClientProgram *prog G_GNUC_UNUSED,
                                      virNetClient *client G_GNUC_UNUSED,
diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x
index 5d86a51116..72aa69e580 100644
--- a/src/remote/remote_protocol.x
+++ b/src/remote/remote_protocol.x
@@ -3323,6 +3323,13 @@ struct remote_network_event_lifecycle_msg {
     int detail;
 };
 
+struct remote_network_event_callback_metadata_change_msg {
+    int callbackID;
+    remote_nonnull_network net;
+    int type;
+    remote_string nsuri;
+};
+
 struct remote_connect_storage_pool_event_register_any_args {
     int eventID;
     remote_storage_pool pool;
@@ -6974,5 +6981,11 @@ enum remote_procedure {
      * @generate: none
      * @acl: domain:write
      */
-    REMOTE_PROC_DOMAIN_FD_ASSOCIATE = 443
+    REMOTE_PROC_DOMAIN_FD_ASSOCIATE = 443,
+
+    /**
+     * @generate: both
+     * @acl: none
+     */
+    REMOTE_PROC_NETWORK_EVENT_CALLBACK_METADATA_CHANGE = 444
 };
diff --git a/src/remote_protocol-structs b/src/remote_protocol-structs
index 3c6c230a16..3f7256051e 100644
--- a/src/remote_protocol-structs
+++ b/src/remote_protocol-structs
@@ -2687,6 +2687,12 @@ struct remote_network_event_lifecycle_msg {
         int                        event;
         int                        detail;
 };
+struct remote_network_event_callback_metadata_change_msg {
+        int                        callbackID;
+        remote_nonnull_network     net;
+        int                        type;
+        remote_string              nsuri;
+};
 struct remote_connect_storage_pool_event_register_any_args {
         int                        eventID;
         remote_storage_pool        pool;
diff --git a/src/test/test_driver.c b/src/test/test_driver.c
index e7fce053b4..7294766d6e 100644
--- a/src/test/test_driver.c
+++ b/src/test/test_driver.c
@@ -633,6 +633,25 @@ static int testStoragePoolObjSetDefaults(virStoragePoolObj *obj);
 static int testNodeGetInfo(virConnectPtr conn, virNodeInfoPtr info);
 static virNetworkObj *testNetworkObjFindByName(testDriver *privconn, const char *name);
 
+static virNetworkObj *
+testNetworkObjFromNetwork(virNetworkPtr network)
+{
+    virNetworkObj *net;
+    testDriver *driver = network->conn->privateData;
+    char uuidstr[VIR_UUID_STRING_BUFLEN];
+
+    net = virNetworkObjFindByUUID(driver->networks, network->uuid);
+    if (!net) {
+        virUUIDFormat(network->uuid, uuidstr);
+        virReportError(VIR_ERR_NO_NETWORK,
+                       _("no network with matching uuid '%1$s' (%2$s)"),
+                       uuidstr, network->name);
+    }
+
+    return net;
+}
+
+
 static virDomainObj *
 testDomObjFromDomain(virDomainPtr domain)
 {
@@ -9948,6 +9967,59 @@ testConnectGetAllDomainStats(virConnectPtr conn,
     return ret;
 }
 
+static char *
+testNetworkGetMetadata(virNetworkPtr net,
+                       int type,
+                       const char *uri,
+                       unsigned int flags)
+{
+    virNetworkObj *privnet;
+    char *ret;
+
+    virCheckFlags(VIR_NETWORK_UPDATE_AFFECT_LIVE |
+                  VIR_NETWORK_UPDATE_AFFECT_CONFIG, NULL);
+
+    if (!(privnet = testNetworkObjFromNetwork(net)))
+        return NULL;
+
+    ret = virNetworkObjGetMetadata(privnet, type, uri, flags);
+
+    virNetworkObjEndAPI(&privnet);
+    return ret;
+}
+
+static int
+testNetworkSetMetadata(virNetworkPtr net,
+                       int type,
+                       const char *metadata,
+                       const char *key,
+                       const char *uri,
+                       unsigned int flags)
+{
+    testDriver *privconn = net->conn->privateData;
+    virNetworkObj *privnet;
+    int ret;
+
+    virCheckFlags(VIR_NETWORK_UPDATE_AFFECT_LIVE |
+                  VIR_NETWORK_UPDATE_AFFECT_CONFIG, -1);
+
+    if (!(privnet = testNetworkObjFromNetwork(net)))
+        return -1;
+
+    ret = virNetworkObjSetMetadata(privnet, type, metadata,
+                                   key, uri, NULL,
+                                   NULL, NULL, flags);
+
+    if (ret == 0) {
+        virObjectEvent *ev = NULL;
+        ev = virNetworkEventMetadataChangeNewFromObj(privnet, type, uri);
+        virObjectEventStateQueue(privconn->eventState, ev);
+    }
+
+    virNetworkObjEndAPI(&privnet);
+    return ret;
+}
+
 /*
  * Test driver
  */
@@ -10141,6 +10213,8 @@ static virNetworkDriver testNetworkDriver = {
     .networkSetAutostart = testNetworkSetAutostart, /* 0.3.2 */
     .networkIsActive = testNetworkIsActive, /* 0.7.3 */
     .networkIsPersistent = testNetworkIsPersistent, /* 0.7.3 */
+    .networkSetMetadata = testNetworkSetMetadata, /* 9.5.0 */
+    .networkGetMetadata = testNetworkGetMetadata, /* 9.5.0 */
 };
 
 static virInterfaceDriver testInterfaceDriver = {
diff --git a/src/util/virerror.c b/src/util/virerror.c
index 453f19514e..227a182417 100644
--- a/src/util/virerror.c
+++ b/src/util/virerror.c
@@ -1287,6 +1287,9 @@ static const virErrorMsgTuple virErrorMsgStrings[] = {
     [VIR_ERR_MULTIPLE_DOMAINS] = {
         N_("multiple matching domains found"),
         N_("multiple matching domains found: %1$s") },
+    [VIR_ERR_NO_NETWORK_METADATA] = {
+        N_("metadata not found"),
+        N_("metadata not found: %1$s") },
 };
 
 G_STATIC_ASSERT(G_N_ELEMENTS(virErrorMsgStrings) == VIR_ERR_NUMBER_LAST);
diff --git a/tests/meson.build b/tests/meson.build
index 0082446029..d083548c0a 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -258,6 +258,7 @@ tests += [
   { 'name': 'genericxml2xmltest' },
   { 'name': 'interfacexml2xmltest' },
   { 'name': 'metadatatest' },
+  { 'name': 'networkmetadatatest' },
   { 'name': 'networkxml2xmlupdatetest' },
   { 'name': 'nodedevxml2xmltest' },
   { 'name': 'nwfilterxml2xmltest' },
diff --git a/tests/networkmetadatatest.c b/tests/networkmetadatatest.c
new file mode 100644
index 0000000000..4448472776
--- /dev/null
+++ b/tests/networkmetadatatest.c
@@ -0,0 +1,297 @@
+/*
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * This library is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this library;  If not, see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include "testutils.h"
+
+#include "virerror.h"
+#include "virxml.h"
+
+#define VIR_FROM_THIS VIR_FROM_NONE
+
+static const char metadata1[] =
+"<derp xmlns:foobar='http://foo.bar/'>\n"
+"  <bar>foobar</bar>\n"
+"  <foo fooish='blurb'>foofoo</foo>\n"
+"  <foobar:baz>zomg</foobar:baz>\n"
+"</derp>";
+
+
+static const char metadata1_ns[] =
+"<herp:derp xmlns:foobar='http://foo.bar/' xmlns:herp='http://herp.derp/'>\n"
+"  <herp:bar>foobar</herp:bar>\n"
+"  <herp:foo fooish='blurb'>foofoo</herp:foo>\n"
+"  <foobar:baz>zomg</foobar:baz>\n"
+"</herp:derp>";
+
+
+static const char metadata2[] =
+"<foo>\n"
+"  <bar>baz</bar>\n"
+"</foo>";
+
+
+static const char metadata2_ns[] =
+"<blurb:foo xmlns:blurb='http://herp.derp/'>\n"
+"  <blurb:bar>baz</blurb:bar>\n"
+"</blurb:foo>";
+
+
+static char *
+getMetadataFromXML(virNetworkPtr net)
+{
+    g_autoptr(xmlDoc) doc = NULL;
+    g_autoptr(xmlXPathContext) ctxt = NULL;
+    xmlNodePtr node;
+
+    g_autofree char *xml = NULL;
+
+    if (!(xml = virNetworkGetXMLDesc(net, 0)))
+        return NULL;
+
+    if (!(doc = virXMLParseStringCtxt(xml, "(network_definition)", &ctxt)))
+        return NULL;
+
+    if (!(node = virXPathNode("//metadata/*", ctxt)))
+        return NULL;
+
+    return virXMLNodeToString(node->doc, node);
+}
+
+
+static void
+metadataXMLConvertApostrophe(char *str)
+{
+    do {
+        if (*str == '\"')
+            *str = '\'';
+    } while ((*++str) != '\0');
+}
+
+
+static bool
+verifyMetadata(virNetworkPtr net,
+               const char *expectXML,
+               const char *expectAPI,
+               const char *uri)
+{
+    g_autofree char *metadataXML = NULL;
+    g_autofree char *metadataAPI = NULL;
+
+    if (!expectAPI) {
+        if ((metadataAPI = virNetworkGetMetadata(net,
+                                                VIR_NETWORK_METADATA_ELEMENT,
+                                                uri, 0))) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           "expected no metadata in API, but got:\n[%s]",
+                           metadataAPI);
+            return false;
+        }
+    } else {
+        if (!(metadataAPI = virNetworkGetMetadata(net,
+                                                 VIR_NETWORK_METADATA_ELEMENT,
+                                                 uri, 0)))
+            return false;
+
+        metadataXMLConvertApostrophe(metadataAPI);
+
+        if (STRNEQ(metadataAPI, expectAPI)) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           "XML metadata in API doesn't match expected metadata: "
+                           "expected:\n[%s]\ngot:\n[%s]",
+                           expectAPI, metadataAPI);
+            return false;
+        }
+
+    }
+
+    if (!expectXML) {
+        if ((metadataXML = getMetadataFromXML(net))) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           "expected no metadata in XML, but got:\n[%s]",
+                           metadataXML);
+            return false;
+        }
+    } else {
+        if (!(metadataXML = getMetadataFromXML(net)))
+            return false;
+
+        metadataXMLConvertApostrophe(metadataXML);
+
+        if (STRNEQ(metadataXML, expectXML)) {
+            virReportError(VIR_ERR_INTERNAL_ERROR,
+                           "XML in dump doesn't match expected metadata: "
+                           "expected:\n[%s]\ngot:\n[%s]",
+                           expectXML, metadataXML);
+            return false;
+        }
+    }
+
+    return true;
+}
+
+
+struct metadataTest {
+    virConnectPtr conn;
+    virNetworkPtr net;
+
+    const char *data;
+    const char *expect;
+    int type;
+    bool fail;
+};
+
+
+static int
+testAssignMetadata(const void *data)
+{
+    const struct metadataTest *test = data;
+
+    if (virNetworkSetMetadata(test->net, VIR_NETWORK_METADATA_ELEMENT,
+                             metadata1, "herp", "http://herp.derp/", 0) < 0)
+        return -1;
+
+    if (!verifyMetadata(test->net, metadata1_ns, metadata1, "http://herp.derp/"))
+        return -1;
+
+    return 0;
+}
+
+static int
+testRewriteMetadata(const void *data)
+{
+    const struct metadataTest *test = data;
+
+    if (virNetworkSetMetadata(test->net, VIR_NETWORK_METADATA_ELEMENT,
+                             metadata2, "blurb", "http://herp.derp/", 0) < 0)
+        return -1;
+
+    if (!verifyMetadata(test->net, metadata2_ns, metadata2, "http://herp.derp/"))
+        return -1;
+
+    return 0;
+}
+
+static int
+testEraseMetadata(const void *data)
+{
+    const struct metadataTest *test = data;
+
+    if (virNetworkSetMetadata(test->net, VIR_NETWORK_METADATA_ELEMENT,
+                             NULL, NULL, "http://herp.derp/", 0) < 0)
+        return -1;
+
+    if (!verifyMetadata(test->net, NULL, NULL, "http://herp.derp/"))
+        return -1;
+
+    return 0;
+}
+
+static int
+testTextMetadata(const void *data)
+{
+    const struct metadataTest *test = data;
+    g_autofree char *actual = NULL;
+
+    if (virNetworkSetMetadata(test->net, test->type, test->data, NULL, NULL, 0) < 0) {
+        if (test->fail)
+            return 0;
+        return -1;
+    }
+
+    actual = virNetworkGetMetadata(test->net, test->type, NULL, 0);
+
+    if (STRNEQ_NULLABLE(test->expect, actual)) {
+        virReportError(VIR_ERR_INTERNAL_ERROR,
+                       "expected metadata doesn't match actual: "
+                       "expected:'%s'\ngot: '%s'",
+                       NULLSTR(test->data), NULLSTR(actual));
+        return -1;
+    }
+
+    return 0;
+}
+
+#define TEST_TEXT_METADATA(INDEX, TYPE, DATA, EXPECT, FAIL) \
+    do { \
+        test.type = VIR_NETWORK_METADATA_ ## TYPE; \
+        test.data = DATA; \
+        test.expect = EXPECT; \
+        test.fail = FAIL; \
+ \
+        if (virTestRun("text metadata: " #TYPE " " INDEX " ", \
+                       testTextMetadata, &test) < 0) \
+            ret = EXIT_FAILURE; \
+    } while (0)
+
+#define TEST_TITLE(INDEX, DATA) \
+    TEST_TEXT_METADATA(INDEX, TITLE, DATA, DATA, false)
+#define TEST_TITLE_EXPECT(INDEX, DATA, EXPECT) \
+    TEST_TEXT_METADATA(INDEX, TITLE, DATA, EXPECT, false)
+#define TEST_TITLE_FAIL(INDEX, DATA) \
+    TEST_TEXT_METADATA(INDEX, TITLE, DATA, DATA, true)
+#define TEST_DESCR(INDEX, DATA) \
+    TEST_TEXT_METADATA(INDEX, DESCRIPTION, DATA, DATA, false)
+#define TEST_DESCR_EXPECT(INDEX, DATA, EXPECT) \
+    TEST_TEXT_METADATA(INDEX, DESCRIPTION, DATA, EXPECT, false)
+
+static int
+mymain(void)
+{
+    struct metadataTest test = { 0 };
+    int ret = EXIT_SUCCESS;
+
+    if (!(test.conn = virConnectOpen("test:///default")))
+        return EXIT_FAILURE;
+
+    if (!(test.net = virNetworkLookupByName(test.conn, "default"))) {
+        virConnectClose(test.conn);
+        return EXIT_FAILURE;
+    }
+
+    virTestQuiesceLibvirtErrors(false);
+
+    if (virTestRun("Assign metadata ", testAssignMetadata, &test) < 0)
+        ret = EXIT_FAILURE;
+    if (virTestRun("Rewrite Metadata ", testRewriteMetadata, &test) < 0)
+        ret = EXIT_FAILURE;
+    if (virTestRun("Erase metadata ", testEraseMetadata, &test) < 0)
+        ret = EXIT_FAILURE;
+
+    TEST_TITLE("1", "qwert");
+    TEST_TITLE("2", NULL);
+    TEST_TITLE("3", "blah");
+    TEST_TITLE_FAIL("4", "qwe\nrt");
+    TEST_TITLE_EXPECT("5", "", NULL);
+    TEST_TITLE_FAIL("6", "qwert\n");
+    TEST_TITLE_FAIL("7", "\n");
+
+    TEST_DESCR("1", "qwert\nqwert");
+    TEST_DESCR("2", NULL);
+    TEST_DESCR("3", "qwert");
+    TEST_DESCR("4", "\n");
+    TEST_DESCR_EXPECT("5", "", NULL);
+
+    virNetworkFree(test.net);
+    virConnectClose(test.conn);
+
+    return ret;
+}
+
+VIR_TEST_MAIN(mymain)
diff --git a/tools/virsh-network.c b/tools/virsh-network.c
index 42b7dba761..74712e29be 100644
--- a/tools/virsh-network.c
+++ b/tools/virsh-network.c
@@ -1206,7 +1206,8 @@ typedef struct virshNetEventData virshNetEventData;
 VIR_ENUM_DECL(virshNetworkEventId);
 VIR_ENUM_IMPL(virshNetworkEventId,
               VIR_NETWORK_EVENT_ID_LAST,
-              "lifecycle");
+              "lifecycle",
+              "metadata-change");
 
 static void
 vshEventLifecyclePrint(virConnectPtr conn G_GNUC_UNUSED,
@@ -1239,9 +1240,84 @@ vshEventLifecyclePrint(virConnectPtr conn G_GNUC_UNUSED,
         vshEventDone(data->ctl);
 }
 
+static void G_GNUC_PRINTF(2, 3)
+virshEventPrintf(virshNetEventData *data,
+                 const char *fmt,
+                 ...)
+{
+    va_list ap;
+
+    if (!data->loop && data->count)
+        return;
+
+    if (data->timestamp) {
+        char timestamp[VIR_TIME_STRING_BUFLEN] = "";
+
+        ignore_value(virTimeStringNowRaw(timestamp));
+        vshPrint(data->ctl, "%s: ", timestamp);
+    }
+
+    va_start(ap, fmt);
+    vshPrintVa(data->ctl, fmt, ap);
+    va_end(ap);
+
+    (data->count)++;
+    if (!data->loop)
+        vshEventDone(data->ctl);
+}
+
+/**
+ * virshEventPrint:
+ *
+ * @data: opaque data passed to all event callbacks
+ * @buf: string buffer describing the event
+ *
+ * Print the event description found in @buf and update virshNetEventData.
+ *
+ * This function resets @buf and frees all memory consumed by its content.
+ */
+static void
+virshEventPrint(virshNetEventData *data,
+                virBuffer *buf)
+{
+    g_autofree char *msg = NULL;
+
+    if (!(msg = virBufferContentAndReset(buf)))
+        return;
+
+    virshEventPrintf(data, "%s", msg);
+}
+
+#define UNKNOWNSTR(str) (str ? str : N_("unsupported value"))
+
+VIR_ENUM_DECL(virshNetworkEventMetadataChangeType);
+VIR_ENUM_IMPL(virshNetworkEventMetadataChangeType,
+              VIR_NETWORK_METADATA_LAST,
+              N_("description"),
+              N_("title"),
+              N_("element"));
+
+static void
+vshEventMetadataChangePrint(virConnectPtr conn G_GNUC_UNUSED,
+                              virNetworkPtr net,
+                              int type,
+                              const char *nsuri,
+                              void *opaque)
+{
+    g_auto(virBuffer) buf = VIR_BUFFER_INITIALIZER;
+
+    virBufferAsprintf(&buf, _("event 'metadata-change' for network '%1$s': type %2$s, uri %3$s\n"),
+                      virNetworkGetName(net),
+                      UNKNOWNSTR(virshNetworkEventMetadataChangeTypeTypeToString(type)),
+                      NULLSTR(nsuri));
+    virshEventPrint(opaque, &buf);
+}
+
 virshNetworkEventCallback virshNetworkEventCallbacks[] = {
     { "lifecycle",
       VIR_NETWORK_EVENT_CALLBACK(vshEventLifecyclePrint), },
+    { "metadata-change",
+      VIR_NETWORK_EVENT_CALLBACK(vshEventMetadataChangePrint), },
 };
 G_STATIC_ASSERT(VIR_NETWORK_EVENT_ID_LAST == G_N_ELEMENTS(virshNetworkEventCallbacks));
 
-- 
2.41.0
Re: [libvirt PATCH v2] Metadata support for Network Objects
Posted by Michal Prívozník 10 months, 1 week ago
On 6/26/23 11:26, K Shiva wrote:
> Adds the following to Network Object:
> - <metadata>, <title> and <description> to the Network Schema.
> - Get and Set APIs to access or modify the above.
> - An async callback that notifies of metadata changes.
> 
> Resolves (GSoC 2023): https://wiki.libvirt.org/Google_Summer_of_Code_Ideas.html
> Signed-off-by: K Shiva <shiva_kr@riseup.net>
> ---
> This is a v2 of: 
> https://listman.redhat.com/archives/libvir-list/2023-June/240299.html
> 
> Changes from v1:
> - Corrected names in comments

Perfect!

> 
>  include/libvirt/libvirt-domain.h    |   2 +-
>  include/libvirt/libvirt-network.h   |  51 ++++
>  include/libvirt/virterror.h         |   2 +
>  po/POTFILES                         |   1 +
>  src/conf/network_conf.c             |   3 +
>  src/conf/network_conf.h             |   2 +
>  src/conf/network_event.c            | 115 +++++++++
>  src/conf/network_event.h            |  11 +
>  src/conf/virnetworkobj.c            | 347 ++++++++++++++++++++++++++--
>  src/conf/virnetworkobj.h            |  56 +++++
>  src/driver-network.h                |  16 ++
>  src/libvirt-network.c               | 167 +++++++++++++
>  src/libvirt_public.syms             |   6 +
>  src/remote/remote_daemon_dispatch.c |  39 ++++
>  src/remote/remote_driver.c          |  32 +++
>  src/remote/remote_protocol.x        |  15 +-
>  src/remote_protocol-structs         |   6 +
>  src/test/test_driver.c              |  74 ++++++
>  src/util/virerror.c                 |   3 +
>  tests/meson.build                   |   1 +
>  tests/networkmetadatatest.c         | 297 ++++++++++++++++++++++++
>  tools/virsh-network.c               |  78 ++++++-
>  22 files changed, 1299 insertions(+), 25 deletions(-)
>  create mode 100644 tests/networkmetadatatest.c

This still suffers from being one patch with too many things going on at
once. Please split it into smaller ones.

Michal