Repository: mendersoftware/mender-mcu
Pull request #160: MEN-7598: Inventory API rework
Author: login: vpodzime
Created: 2025-03-04T17:16:52Z | Merged: 2025-03-07T13:05:20Z



Diff:
diff --git a/include/mender/inventory.h b/include/mender/inventory.h
index 5841c666..14dc8642 100644
--- a/include/mender/inventory.h
+++ b/include/mender/inventory.h
@@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
+ */
+typedef mender_err_t(MenderInventoryCallback)(mender_keystore_t **inventory, uint8_t *inventory_len);
+
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
 
 /**
- * @brief Set mender inventory
- * @param inventory Mender inventory key/value pairs table, must end with a NULL/NULL element, NULL if not defined
+ * @brief Add mender inventory callback
+ * @param callback   A function to call to obtain inventory information
+ * @param persistent Whether the inventory information is persistent or dynamic (see notes below)
  * @return MENDER_OK if the function succeeds, error code otherwise
+ * @note Persistent inventory data is only obtained and sent to the server once in the run of the
+ *       client, i.e. during the first successful inventory submission. Dynamic inventory data is
+ *       obtained and sent to the server at every inventory submission (interval).
+ * @note Persistent inventory data is *considered static*, dynamic data is considered
+ *       heap-allocated, with ownership being transferred (IOW, the Mender client deallocates the
+ *       data when no longer needed). This applies to both the container (array) and the actual
+ *       data (key-value pairs).
  */
-mender_err_t mender_inventory_set(mender_keystore_t *inventory);
+mender_err_t mender_inventory_add_callback(MenderInventoryCallback callback, bool persistent);
 
 /**
  * @brief Function used to trigger execution of the inventory work
diff --git a/include/mender/utils.h b/include/mender/utils.h
index bebe842b..70d7f2ac 100644
--- a/include/mender/utils.h
+++ b/include/mender/utils.h
@@ -109,6 +109,18 @@ typedef mender_item_t mender_identity_t;
  */
 const char *mender_utils_deployment_status_to_string(mender_deployment_status_t deployment_status);
 
+/**
+ * @brief Duplicate string using Mender memory allocation
+ */
+char *mender_utils_strdup(const char *str);
+char *mender_utils_strndup(const char *str, size_t n);
+
+/**
+ * @brief Format a new string using Mender memory allocation
+ */
+int mender_utils_asprintf(char **result, const char *fmt, ...);
+int mender_utils_vasprintf(char **result, const char *fmt, va_list ap);
+
 #ifdef __cplusplus
 }
 #endif /* __cplusplus */
diff --git a/src/core/api.c b/src/core/api.c
index 2a127b99..c40426b1 100644
--- a/src/core/api.c
+++ b/src/core/api.c
@@ -630,27 +630,23 @@ mender_api_publish_deployment_status(const char *id, mender_deployment_status_t
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
 
 mender_err_t
-mender_api_publish_inventory_data(mender_keystore_t *inventory) {
+mender_api_publish_inventory_data(cJSON *inventory, bool patch) {
 
     mender_err_t ret;
     char        *payload       = NULL;
     char        *response      = NULL;
     int          status        = 0;
     const char  *artifact_name = NULL;
+    cJSON       *item          = NULL;
 
     if ((MENDER_OK != mender_storage_get_artifact_name(&artifact_name)) && (NULL != artifact_name)) {
         mender_log_error("Unable to get artifact name");
-        return MENDER_FAIL;
-    }
-
-    /* Format payload */
-    cJSON *object = cJSON_CreateArray();
-    if (NULL == object) {
-        mender_log_error("Unable to allocate memory");
         ret = MENDER_FAIL;
         goto END;
     }
-    cJSON *item = cJSON_CreateObject();
+
+    /* Format payload */
+    item = cJSON_CreateObject();
     if (NULL == item) {
         mender_log_error("Unable to allocate memory");
         ret = MENDER_FAIL;
@@ -658,7 +654,8 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     }
     cJSON_AddStringToObject(item, "name", "artifact_name");
     cJSON_AddStringToObject(item, "value", artifact_name);
-    cJSON_AddItemToArray(object, item);
+    cJSON_AddItemToArray(inventory, item);
+
     item = cJSON_CreateObject();
     if (NULL == item) {
         mender_log_error("Unable to allocate memory");
@@ -667,29 +664,18 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     }
     cJSON_AddStringToObject(item, "name", "device_type");
     cJSON_AddStringToObject(item, "value", api_config.device_type);
-    cJSON_AddItemToArray(object, item);
-    if (NULL != inventory) {
-        size_t index = 0;
-        while ((NULL != inventory[index].name) && (NULL != inventory[index].value)) {
-            if (NULL == (item = cJSON_CreateObject())) {
-                mender_log_error("Unable to allocate memory");
-                ret = MENDER_FAIL;
-                goto END;
-            }
-            cJSON_AddStringToObject(item, "name", inventory[index].name);
-            cJSON_AddStringToObject(item, "value", inventory[index].value);
-            cJSON_AddItemToArray(object, item);
-            index++;
-        }
-    }
-    if (NULL == (payload = cJSON_PrintUnformatted(object))) {
+    cJSON_AddItemToArray(inventory, item);
+
+    if (NULL == (payload = cJSON_PrintUnformatted(inventory))) {
         mender_log_error("Unable to allocate memory");
         ret = MENDER_FAIL;
         goto END;
     }
 
     /* Perform HTTP request */
-    if (MENDER_OK != (ret = authenticated_http_perform(MENDER_API_PATH_PUT_DEVICE_ATTRIBUTES, MENDER_HTTP_PUT, payload, NULL, &response, &status))) {
+    if (MENDER_OK
+        != (ret = authenticated_http_perform(
+                MENDER_API_PATH_PUT_DEVICE_ATTRIBUTES, patch ? MENDER_HTTP_PATCH : MENDER_HTTP_PUT, payload, NULL, &response, &status))) {
         mender_log_error("Unable to perform HTTP request");
         goto END;
     }
@@ -709,7 +695,7 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     mender_free((void *)artifact_name);
     mender_free(response);
     mender_free(payload);
-    cJSON_Delete(object);
+    cJSON_Delete(inventory);
 
     return ret;
 }
diff --git a/src/core/inventory.c b/src/core/inventory.c
index bb51195e..6bcef11f 100644
--- a/src/core/inventory.c
+++ b/src/core/inventory.c
@@ -26,6 +26,17 @@
 
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
 
+typedef struct {
+    MenderInventoryCallback *callback;
+    bool                     persistent;
+} callback_item_t;
+
+typedef struct keystores_item_t {
+    mender_keystore_t       *keystore;
+    uint8_t                  keystore_len;
+    struct keystores_item_t *next;
+} keystores_item_t;
+
 /**
  * @brief Default inventory refresh interval (seconds)
  */
@@ -36,8 +47,16 @@
 /**
  * @brief Mender inventory keystore
  */
-static mender_keystore_t *mender_inventory_keystore = NULL;
-static void              *mender_inventory_mutex    = NULL;
+static callback_item_t  *callbacks              = NULL;
+static uint8_t           n_callbacks            = 0;
+static uint8_t           n_persistent_callbacks = 0;
+static uint8_t           callbacks_len          = 0;
+static keystores_item_t *persistent_inventory   = NULL;
+static bool              full_push_done         = false;
+static void             *mender_inventory_mutex = NULL;
+
+#define N_CALLBACKS_INIT 4
+#define N_CALLBACKS_MAX  16
 
 /**
  * @brief Mender inventory work handle
@@ -60,6 +79,14 @@ mender_inventory_init(uint32_t interval) {
         return ret;
     }
 
+    callbacks = mender_calloc(N_CALLBACKS_INIT, sizeof(callback_item_t));
+    if (NULL == callbacks) {
+        mender_log_error("Unable to allocate memory");
+        return MENDER_FAIL;
+    }
+    callbacks_len  = N_CALLBACKS_INIT;
+    full_push_done = false;
+
     /* Create mender inventory work */
     mender_os_scheduler_work_params_t inventory_work_params;
     inventory_work_params.function = mender_inventory_work_function;
@@ -97,8 +124,7 @@ mender_inventory_deactivate(void) {
 }
 
 mender_err_t
-mender_inventory_set(mender_keystore_t *inventory) {
-
+mender_inventory_add_callback(MenderInventoryCallback callback, bool persistent) {
     mender_err_t ret;
 
     /* Take mutex used to protect access to the inventory key-store */
@@ -107,16 +133,26 @@ mender_inventory_set(mender_keystore_t *inventory) {
         return ret;
     }
 
-    /* Release previous inventory */
-    if (MENDER_OK != (ret = mender_utils_keystore_delete(mender_inventory_keystore))) {
-        mender_log_error("Unable to delete inventory");
+    if (N_CALLBACKS_MAX == n_callbacks) {
+        mender_log_error("Too many inventory callbacks");
+        ret = MENDER_FAIL;
         goto END;
     }
 
-    /* Copy the new inventory */
-    if (MENDER_OK != (ret = mender_utils_keystore_copy(&mender_inventory_keystore, inventory))) {
-        mender_log_error("Unable to copy inventory");
-        goto END;
+    if (n_callbacks == callbacks_len) {
+        callbacks = mender_realloc(callbacks, 2 * callbacks_len * sizeof(callback_item_t));
+        if (NULL == callbacks) {
+            mender_log_error("Unable to allocate memory");
+            ret = MENDER_FAIL;
+            goto END;
+        }
+        callbacks_len = 2 * callbacks_len;
+    }
+    callbacks[n_callbacks].callback   = callback;
+    callbacks[n_callbacks].persistent = persistent;
+    n_callbacks++;
+    if (persistent) {
+        n_persistent_callbacks++;
     }
 
 END:
@@ -143,7 +179,6 @@ mender_inventory_execute(void) {
 
 mender_err_t
 mender_inventory_exit(void) {
-
     mender_err_t ret;
 
     /* Delete mender inventory work */
@@ -156,8 +191,20 @@ mender_inventory_exit(void) {
         return ret;
     }
 
-    /* Release memory */
-    DESTROY_AND_NULL(mender_utils_keystore_delete, mender_inventory_keystore);
+    mender_free(callbacks);
+    callbacks_len          = 0;
+    n_callbacks            = 0;
+    n_persistent_callbacks = 0;
+    full_push_done         = false;
+
+    keystores_item_t *inv = persistent_inventory;
+    while (NULL != inv) {
+        keystores_item_t *aux = inv;
+        inv                   = inv->next;
+        mender_free(aux);
+    }
+    persistent_inventory = NULL;
+
     mender_os_mutex_give(mender_inventory_mutex);
     DESTROY_AND_NULL(mender_os_mutex_delete, mender_inventory_mutex);
 
@@ -165,9 +212,48 @@ mender_inventory_exit(void) {
 }
 
 static mender_err_t
-mender_inventory_work_function(void) {
+collect_persistent_inventory(void) {
+    for (uint8_t idx = 0; idx < n_callbacks; idx++) {
+        if (callbacks[idx].persistent) {
+            keystores_item_t *item = mender_calloc(1, sizeof(keystores_item_t));
+            if (NULL == item) {
+                mender_log_error("Unable to allocate memory");
+                return MENDER_FAIL;
+            }
+            if (MENDER_OK != callbacks[idx].callback(&(item->keystore), &(item->keystore_len))) {
+                mender_log_error("Failed to get persistent inventory data");
+                /* keep going and collect as much as we can */
+                mender_free(item);
+                continue;
+            }
+            item->next           = persistent_inventory;
+            persistent_inventory = item;
+        }
+    }
+    return MENDER_OK;
+}
 
-    mender_err_t ret;
+static mender_err_t
+append_keystore_to_inventory_data(cJSON *inventory_data, const mender_keystore_t *keystore, uint8_t keystore_len) {
+    cJSON *item = NULL;
+    for (uint8_t idx = 0; idx < keystore_len; idx++) {
+        if (NULL == (item = cJSON_CreateObject())) {
+            mender_log_error("Unable to allocate memory");
+            return MENDER_FAIL;
+        }
+        cJSON_AddStringToObject(item, "name", keystore[idx].name);
+        cJSON_AddStringToObject(item, "value", keystore[idx].value);
+        cJSON_AddItemToArray(inventory_data, item);
+    }
+    return MENDER_OK;
+}
+
+static mender_err_t
+mender_inventory_work_function(void) {
+    mender_err_t       ret;
+    cJSON             *inventory_data = NULL;
+    mender_keystore_t *keystore;
+    uint8_t            keystore_len;
 
     /* Take mutex used to protect access to the inventory key-store */
     if (MENDER_OK != (ret = mender_os_mutex_take(mender_inventory_mutex, -1))) {
@@ -181,13 +267,69 @@ mender_inventory_work_function(void) {
         goto END;
     }
 
+    /* Gather persistent inventory once */
+    if (!full_push_done && (NULL == persistent_inventory)) {
+        if (MENDER_OK != (ret = collect_persistent_inventory())) {
+            goto END;
+        }
+    }
+
+    /* Check if there's anything to do */
+    if ((full_push_done || (NULL == persistent_inventory)) && (0 == n_callbacks - n_persistent_callbacks)) {
+        /* nothing to do */
+        ret = MENDER_OK;
+        goto END;
+    }
+
+    /* Construct the inventory data */
+    inventory_data = cJSON_CreateArray();
+    if (NULL == inventory_data) {
+        mender_log_error("Failed to allocate memory");
+        ret = MENDER_FAIL;
+        goto END;
+    }
+
+    if (!full_push_done && (NULL != persistent_inventory)) {
+        for (keystores_item_t *pers_ks = persistent_inventory; NULL != pers_ks; pers_ks = pers_ks->next) {
+            if (MENDER_OK != (ret = append_keystore_to_inventory_data(inventory_data, pers_ks->keystore, pers_ks->keystore_len))) {
+                mender_log_error("Failed to add persistent inventory to inventory payload");
+                goto END;
+            }
+        }
+    }
+    /* add dynamic inventory (if any) */
+    if (n_callbacks - n_persistent_callbacks > 0) {
+        for (uint8_t idx = 0; idx < n_callbacks; idx++) {
+            if (callbacks[idx].persistent) {
+                continue;
+            }
+            if (MENDER_OK != callbacks[idx].callback(&keystore, &keystore_len)) {
+                mender_log_error("Failed to get dynamic inventory");
+                /* keep going and collect as much as we can */
+                continue;
+            }
+            if (MENDER_OK != (ret = append_keystore_to_inventory_data(inventory_data, keystore, keystore_len))) {
+                mender_log_error("Failed to add dynamic inventory to inventory payload");
+                mender_utils_keystore_delete(keystore, keystore_len);
+                goto END;
+            }
+            mender_utils_keystore_delete(keystore, keystore_len);
+        }
+    }
+
     /* Publish inventory */
-    if (MENDER_OK != (ret = mender_api_publish_inventory_data(mender_inventory_keystore))) {
+    if (MENDER_OK != (ret = mender_api_publish_inventory_data(inventory_data, full_push_done))) {
         mender_log_error("Unable to publish inventory data");
+    } else {
+        /* we either pushed the persistent inventory or it was pushed before */
+        full_push_done = true;
     }
+    /* mender_api_publish_inventory_data() takes ownership of inventory_data */
+    inventory_data = NULL;
 
 END:
 
+    cJSON_Delete(inventory_data);
     /* Release mutex used to protect access to the inventory key-store */
     mender_os_mutex_give(mender_inventory_mutex);
 
diff --git a/src/core/utils.c b/src/core/utils.c
index bb45c07d..f8f6487e 100644
--- a/src/core/utils.c
+++ b/src/core/utils.c
@@ -264,94 +264,8 @@ mender_utils_hexdump_to_bytes(const char *hexdump, unsigned char *bytes, size_t
     return true;
 }
 
-mender_keystore_t *
-mender_utils_keystore_new(size_t length) {
-
-    /* Allocate memory */
-    mender_keystore_t *keystore = (mender_keystore_t *)mender_malloc((length + 1) * sizeof(mender_item_t));
-    if (NULL == keystore) {
-        mender_log_error("Unable to allocate memory");
-        return NULL;
-    }
-
-    /* Initialize keystore */
-    memset(keystore, 0, (length + 1) * sizeof(mender_item_t));
-
-    return keystore;
-}
-
-mender_err_t
-mender_utils_keystore_copy(mender_keystore_t **dst_keystore, mender_keystore_t *src_keystore) {
-
-    assert(NULL != dst_keystore);
-    mender_err_t ret = MENDER_OK;
-
-    /* Copy the new keystore */
-    size_t length = mender_utils_keystore_length(src_keystore);
-    if (NULL == (*dst_keystore = mender_utils_keystore_new(length))) {
-        mender_log_error("Unable to allocate memory");
-        ret = MENDER_FAIL;
-        goto END;
-    }
-    for (size_t index = 0; index < length; index++) {
-        if (MENDER_OK != (ret = mender_utils_keystore_set_item(*dst_keystore, index, src_keystore[index].name, src_keystore[index].value))) {
-            mender_log_error("Unable to allocate memory");
-            goto END;
-        }
-    }
-
-END:
-
-    return ret;
-}
-
 mender_err_t
-mender_utils_keystore_from_json(mender_keystore_t **keystore, cJSON *object) {
-
-    assert(NULL != keystore);
-    mender_err_t ret;
-
-    /* Release previous keystore */
-    if (MENDER_OK != (ret = mender_utils_keystore_delete(*keystore))) {
-        mender_log_error("Unable to delete keystore");
-        return ret;
-    }
-    *keystore = NULL;
-
-    /* Set key-store */
-    if (NULL != object) {
-        size_t length       = 0;
-        cJSON *current_item = object->child;
-        while (NULL != current_item) {
-            if ((NULL != current_item->string) && (NULL != current_item->valuestring)) {
-                length++;
-            }
-            current_item = current_item->next;
-        }
-        if (NULL != (*keystore = mender_utils_keystore_new(length))) {
-            size_t index = 0;
-            current_item = object->child;
-            while (NULL != current_item) {
-                if ((NULL != current_item->string) && (NULL != current_item->valuestring)) {
-                    if (MENDER_OK != (ret = mender_utils_keystore_set_item(*keystore, index, current_item->string, current_item->valuestring))) {
-                        mender_log_error("Unable to allocate memory");
-                        return ret;
-                    }
-                    index++;
-                }
-                current_item = current_item->next;
-            }
-        } else {
-            mender_log_error("Unable to allocate memory");
-            ret = MENDER_FAIL;
-        }
-    }
-
-    return ret;
-}
-
-mender_err_t
-mender_utils_keystore_to_json(mender_keystore_t *keystore, cJSON **object) {
+mender_utils_identity_to_json(const mender_identity_t *identity, cJSON **object) {
 
     assert(NULL != object);
 
@@ -361,94 +275,24 @@ mender_utils_keystore_to_json(mender_keystore_t *keystore, cJSON **object) {
         mender_log_error("Unable to allocate memory");
         return MENDER_FAIL;
     }
-    if (NULL != keystore) {
-        size_t index = 0;
-        while ((NULL != keystore[index].name) && (NULL != keystore[index].value)) {
-            cJSON_AddStringToObject(*object, keystore[index].name, keystore[index].value);
-            index++;
-        }
-    }
-
-    return MENDER_OK;
-}
-
-mender_err_t
-mender_utils_keystore_set_item(mender_keystore_t *keystore, size_t index, char *name, char *value) {
-
-    assert(NULL != keystore);
-
-    /* Release memory */
-    FREE_AND_NULL(keystore[index].name);
-    FREE_AND_NULL(keystore[index].value);
-
-    /* Copy name and value */
-    if (NULL != name) {
-        if (NULL == (keystore[index].name = mender_utils_strdup(name))) {
-            mender_log_error("Unable to allocate memory");
-            return MENDER_FAIL;
-        }
-    }
-    if (NULL != value) {
-        if (NULL == (keystore[index].value = mender_utils_strdup(value))) {
-            mender_log_error("Unable to allocate memory");
-            return MENDER_FAIL;
-        }
+    if (NULL == cJSON_AddStringToObject(*object, identity->name, identity->value)) {
+        mender_log_error("Unable to add identity to JSON object");
+        return MENDER_FAIL;
     }
-
     return MENDER_OK;
 }
 
-size_t
-mender_utils_keystore_length(mender_keystore_t *keystore) {
-
-    /* Compute key-store length */
-    size_t length = 0;
-    if (NULL != keystore) {
-        while ((NULL != keystore[length].name) && (NULL != keystore[length].value)) {
-            length++;
-        }
-    }
-
-    return length;
-}
-
-mender_err_t
-mender_utils_keystore_delete(mender_keystore_t *keystore) {
-
-    /* Release memory */
-    if (NULL != keystore) {
-        size_t index = 0;
-        while ((NULL != keystore[index].name) || (NULL != keystore[index].value)) {
-            if (NULL != keystore[index].name) {
-                mender_free(keystore[index].name);
-            }
-            if (NULL != keystore[index].value) {
-                mender_free(keystore[index].value);
-            }
-            index++;
-        }
-        mender_free(keystore);
+void
+mender_utils_keystore_delete(mender_keystore_t *keystore, uint8_t keystore_len) {
+    if (NULL == keystore) {
+        return;
     }
 
-    return MENDER_OK;
-}
-
-mender_err_t
-mender_utils_identity_to_json(const mender_identity_t *identity, cJSON **object) {
-
-    assert(NULL != object);
-
-    /* Format data */
-    *object = cJSON_CreateObject();
-    if (NULL == *object) {
-        mender_log_error("Unable to allocate memory");
-        return MENDER_FAIL;
+    for (uint8_t idx = 0; idx < keystore_len; idx++) {
+        mender_free(keystore[idx].name);
+        mender_free(keystore[idx].value);
     }
-    if (NULL == cJSON_AddStringToObject(*object, identity->name, identity->value)) {
-        mender_log_error("Unable to add identity to JSON object");
-        return MENDER_FAIL;
-    }
-    return MENDER_OK;
+    mender_free(keystore);
 }
 
 mender_err_t
diff --git a/src/include/api.h b/src/include/api.h
index d0c2119a..43705bc5 100644
--- a/src/include/api.h
+++ b/src/include/api.h
@@ -113,10 +113,12 @@ mender_err_t mender_api_http_artifact_callback(mender_http_client_event_t event,
 
 /**
  * @brief Publish inventory data of the device to the mender-server
- * @param inventory Mender inventory key/value pairs table, must end with a NULL/NULL element, NULL if not defined
+ * @param inventory Mender inventory data as a JSON array of key-value pairs (ownership transferred)
+ * @param patch Whether to patch inventory data (PATCH) or replace the old data with new (PUT)
  * @return MENDER_OK if the function succeeds, error code otherwise
+ * @note #inventory is consumed by the function (taken over and deallocated)
  */
-mender_err_t mender_api_publish_inventory_data(mender_keystore_t *inventory);
+mender_err_t mender_api_publish_inventory_data(cJSON *inventory, bool patch);
 
 #endif /* CONFIG_MENDER_CLIENT_INVENTORY */
 
diff --git a/src/include/utils.h b/src/include/utils.h
index 2edcd825..e610e29b 100644
--- a/src/include/utils.h
+++ b/src/include/utils.h
@@ -83,18 +83,6 @@ const char *mender_utils_http_status_to_string(int status);
  */
 char *mender_utils_strrstr(const char *haystack, const char *needle);
 
-/**
- * @brief Duplicate string using Mender memory allocation
- */
-char *mender_utils_strdup(const char *str);
-char *mender_utils_strndup(const char *str, size_t n);
-
-/**
- * @brief Format a new string using Mender memory allocation
- */
-int mender_utils_asprintf(char **result, const char *fmt, ...);
-int mender_utils_vasprintf(char **result, const char *fmt, va_list ap);
-
 /**
  * @brief Function used to check if string begins with wanted substring
  * @param s1 String to be checked
@@ -121,37 +109,6 @@ bool mender_utils_strendswith(const char *s1, const char *s2);
  */
 bool mender_utils_hexdump_to_bytes(const char *hexdump, unsigned char *bytes, size_t n_bytes);
 
-/**
- * @brief Function used to create a key-store
- * @param length Length of the key-store
- * @return Key-store if the function succeeds, NULL otherwise
- */
-mender_keystore_t *mender_utils_keystore_new(size_t length);
-
-/**
- * @brief Function used to copy key-store
- * @param dst_keystore Destination key-store to create
- * @param src_keystore Source key-store to copy
- * @return MENDER_OK if the function succeeds, error code otherwise
- */
-mender_err_t mender_utils_keystore_copy(mender_keystore_t **dst_keystore, mender_keystore_t *src_keystore);
-
-/**
- * @brief Function used to set key-store from JSON string
- * @param keystore Key-store
- * @param object JSON object
- * @return MENDER_OK if the function succeeds, error code otherwise
- */
-mender_err_t mender_utils_keystore_from_json(mender_keystore_t **keystore, cJSON *object);
-
-/**
- * @brief Function used to format key-store to JSON object
- * @param keystore Key-store
- * @param object JSON object
- * @return MENDER_OK if the function succeeds, error code otherwise
- */
-mender_err_t mender_utils_keystore_to_json(mender_keystore_t *keystore, cJSON **object);
-
 /**
  * @brief Function used to format identity to JSON object
  * @param  identity Identity
@@ -161,28 +118,9 @@ mender_err_t mender_utils_keystore_to_json(mender_keystore_t *keystore, cJSON **
 mender_err_t mender_utils_identity_to_json(const mender_identity_t *identity, cJSON **object);
 
 /**
- * @brief Function used to set key-store item name and value
- * @param keystore Key-store to be updated
- * @param index Index of the item in the key-store
- * @param name Name of the item
- * @param value Value of the item
- * @return MENDER_OK if the function succeeds, error code otherwise
- */
-mender_err_t mender_utils_keystore_set_item(mender_keystore_t *keystore, size_t index, char *name, char *value);
-
-/**
- * @brief Function used to get length of key-store
- * @param keystore Key-store
- * @return Length of the key-store
- */
-size_t mender_utils_keystore_length(mender_keystore_t *keystore);
-
-/**
- * @brief Function used to delete key-store
- * @param keystore Key-store to be deleted
- * @return MENDER_OK if the function succeeds, error code otherwise
+ * @brief Delete the given keystore
  */
-mender_err_t mender_utils_keystore_delete(mender_keystore_t *keystore);
+void mender_utils_keystore_delete(mender_keystore_t *keystore, uint8_t keystore_len);
 
 /**
  * @brief Free linked list list
diff --git a/tests/smoke/main.c b/tests/smoke/main.c
index cef36731..1861771b 100644
--- a/tests/smoke/main.c
+++ b/tests/smoke/main.c
@@ -130,6 +130,14 @@ get_identity_cb(const mender_identity_t **identity) {
     return MENDER_FAIL;
 }
 
+static mender_err_t
+persistent_inventory_cb(mender_keystore_t **keystore, uint8_t *keystore_len) {
+    static mender_keystore_t inventory[] = { { .name = "demo", .value = "demo" }, { .name = "foo", .value = "var" } };
+    *keystore                            = inventory;
+    *keystore_len                        = 2;
+    return MENDER_OK;
+}
+
 /**
  * @brief Get user-provided keys callback
  * @return MENDER_OK if the function succeeds, error code otherwise
@@ -313,9 +321,8 @@ main(int argc, char **argv) {
 #endif /* CONFIG_MENDER_ZEPHYR_IMAGE_UPDATE_MODULE */
 
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
-    mender_keystore_t inventory[] = { { .name = "demo", .value = "demo" }, { .name = "foo", .value = "var" }, { .name = NULL, .value = NULL } };
-    assert(MENDER_OK == mender_inventory_set(inventory));
-    mender_log_info("Mender inventory set");
+    assert(MENDER_OK == mender_inventory_add_callback(persistent_inventory_cb, true));
+    mender_log_info("Mender inventory callback added");
 #endif /* CONFIG_MENDER_CLIENT_INVENTORY */
 
     /* Finally activate mender client */
diff --git a/tests/unit/core/utils_test.cpp b/tests/unit/core/utils_test.cpp
index c263e8ec..9ad0f2e9 100644
--- a/tests/unit/core/utils_test.cpp
+++ b/tests/unit/core/utils_test.cpp
@@ -288,38 +288,18 @@ TEST(MenderUtilsTest, StringToKeyValueList) {
     EXPECT_EQ(ret, MENDER_OK);
 }
 
-TEST(MenderUtilsTest, KeystoreFromJson) {
-    const string json_str    = "{\"key1\": \"value1\", \"key2\": \"value2\"}";
-    cJSON       *json_object = cJSON_Parse(json_str.c_str());
-
-    mender_keystore_t *keystore = nullptr;
-    mender_err_t       ret      = mender_utils_keystore_from_json(&keystore, json_object);
-    EXPECT_EQ(ret, MENDER_OK);
-
-    EXPECT_EQ(mender_utils_keystore_length(keystore), 2);
-
-    EXPECT_STREQ(keystore[0].name, "key1");
-    EXPECT_STREQ(keystore[0].value, "value1");
-    EXPECT_STREQ(keystore[1].name, "key2");
-    EXPECT_STREQ(keystore[1].value, "value2");
-
-    cJSON_Delete(json_object);
-    mender_utils_keystore_delete(keystore);
-}
-
-TEST(MenderUtilsTest, KeystoreToJson) {
-    mender_keystore_t *keystore = mender_utils_keystore_new(2);
-
-    mender_utils_keystore_set_item(keystore, 0, (char *)"key1", (char *)"value1");
-    mender_utils_keystore_set_item(keystore, 1, (char *)"key2", (char *)"value2");
-
-    cJSON       *json_object = nullptr;
-    mender_err_t ret         = mender_utils_keystore_to_json(keystore, &json_object);
-
-    EXPECT_EQ(ret, MENDER_OK);
-    EXPECT_STREQ(cJSON_GetObjectItem(json_object, "key1")->valuestring, "value1");
-    EXPECT_STREQ(cJSON_GetObjectItem(json_object, "key2")->valuestring, "value2");
-
-    cJSON_Delete(json_object);
-    mender_utils_keystore_delete(keystore);
+TEST(MenderUtilsTest, KeystoreDelete) {
+    mender_keystore_t *keystore = (mender_keystore_t *)mender_calloc(3, sizeof(mender_keystore_t));
+    ASSERT_NE(keystore, nullptr);
+
+    keystore[0].name  = mender_utils_strdup("foo");
+    keystore[0].value = mender_utils_strdup("foo-value");
+    keystore[1].name  = mender_utils_strdup("bar");
+    keystore[1].value = mender_utils_strdup("bar-value");
+    keystore[2].name  = mender_utils_strdup("baz");
+    keystore[2].value = mender_utils_strdup("baz-value");
+
+    mender_utils_keystore_delete(keystore, 3);
+    // Nothing to check here, we just rely on ASAN detecting a potential leak or
+    // memory access violation (if any).
 }


Reviews:
user: login: larsewi
body: Very nice 🚀 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-05T09:49:25Z
user: login: lluiscampos
body: I think there is something odd in the main logic for `mender_inventory_work_function`. Please see my comment(s) below and then I'll finish my review.
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-05T10:12:15Z
user: login: vpodzime
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T18:05:38Z
user: login: vpodzime
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T18:06:46Z
user: login: vpodzime
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-05T18:35:19Z
user: login: vpodzime
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-06T10:16:39Z
user: login: danielskinstad
body: Looks good to me. Just fyi, you don't need a `Changelog` entry for chore-commits.
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-07T10:10:57Z
user: login: lluiscampos
state: COMMENTED
author_association: NONE
submitted_at: 2025-03-10T07:49:43Z

Review comments:
diff_hunk: @@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
+ */
+typedef const mender_err_t *(MenderInventoryCallback)(mender_keystore_t **inventory, uint8_t *inventory_len);
+
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
 
 /**
- * @brief Set mender inventory
- * @param inventory Mender inventory key/value pairs table, must end with a NULL/NULL element, NULL if not defined
+ * @brief Add mender inventory callback
+ * @param callback   A function to call to obtain inventory information
path: include/mender/inventory.h
user: login: larsewi
body: ```suggestion
 * @param callback A function to call to obtain inventory information
```
created_at: 2025-03-05T09:07:21Z
author_association: CONTRIBUTOR
diff_hunk: @@ -181,13 +268,67 @@ mender_inventory_work_function(void) {
         goto END;
     }
 
+    /* Gather persistent inventory once */
+    if (!persistent_pushed && (NULL == persistent_inventory)) {
+        if (MENDER_OK != (ret = collect_persistent_inventory())) {
+            goto END;
+        }
+    }
+
+    /* Check there's anything to do */
path: src/core/inventory.c
user: login: larsewi
body: ```suggestion
    /* Check if there's anything to do */
```
created_at: 2025-03-05T09:25:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
path: include/mender/inventory.h
user: login: lluiscampos
body: ```suggestion
 * @return MENDER_OK in case of success, error otherwise
```

We use the `%retvalue` and  and the `#ref` syntax in two places only. I'd rather remove than just have it used so little.
created_at: 2025-03-05T09:51:39Z
author_association: NONE
diff_hunk: @@ -667,22 +663,9 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     }
     cJSON_AddStringToObject(item, "name", "device_type");
     cJSON_AddStringToObject(item, "value", api_config.device_type);
path: src/core/api.c
user: login: lluiscampos
body: Just a heads-up (or a double-checking),

In the next task, the idea is that `artifact_name` and `device_type` are default inventory callbacks or built-in callbacks, that they can be disabled by the user. This is mimicking the classic client functionality, were we install a set of inventory scripts by default but can be opted out by users
created_at: 2025-03-05T10:03:08Z
author_association: NONE
diff_hunk: @@ -156,18 +191,70 @@ mender_inventory_exit(void) {
         return ret;
     }
 
-    /* Release memory */
-    DESTROY_AND_NULL(mender_utils_keystore_delete, mender_inventory_keystore);
+    mender_free(callbacks);
+    callbacks_len          = 0;
+    n_callbacks            = 0;
+    n_persistent_callbacks = 0;
+    persistent_pushed      = false;
+
+    keystores_item_t *inv = persistent_inventory;
+    while (NULL != inv) {
+        keystores_item_t *aux = inv;
+        inv                   = inv->next;
+        mender_free(aux);
+    }
+    persistent_inventory = NULL;
+
     mender_os_mutex_give(mender_inventory_mutex);
     DESTROY_AND_NULL(mender_os_mutex_delete, mender_inventory_mutex);
 
     return ret;
 }
 
 static mender_err_t
-mender_inventory_work_function(void) {
+collect_persistent_inventory(void) {
+    for (uint8_t idx = 0; idx < n_callbacks; idx++) {
+        if (callbacks[idx].persistent) {
path: src/core/inventory.c
user: login: lluiscampos
body: As we are checking here if its persistent or not, we don't need `n_persistent_callbacks` - iow `n_persistent_callbacks` is not actually being used anywhere
created_at: 2025-03-05T10:06:16Z
author_association: NONE
diff_hunk: @@ -156,18 +191,70 @@ mender_inventory_exit(void) {
         return ret;
     }
 
-    /* Release memory */
-    DESTROY_AND_NULL(mender_utils_keystore_delete, mender_inventory_keystore);
+    mender_free(callbacks);
+    callbacks_len          = 0;
+    n_callbacks            = 0;
+    n_persistent_callbacks = 0;
+    persistent_pushed      = false;
+
+    keystores_item_t *inv = persistent_inventory;
+    while (NULL != inv) {
+        keystores_item_t *aux = inv;
+        inv                   = inv->next;
+        mender_free(aux);
+    }
+    persistent_inventory = NULL;
+
     mender_os_mutex_give(mender_inventory_mutex);
     DESTROY_AND_NULL(mender_os_mutex_delete, mender_inventory_mutex);
 
     return ret;
 }
 
 static mender_err_t
-mender_inventory_work_function(void) {
+collect_persistent_inventory(void) {
+    for (uint8_t idx = 0; idx < n_callbacks; idx++) {
+        if (callbacks[idx].persistent) {
path: src/core/inventory.c
user: login: lluiscampos
body: Or... did we messed up `n_persistent_callbacks` and `n_persistent_cbs` ? That might be the intention
created_at: 2025-03-05T10:08:33Z
author_association: NONE
diff_hunk: @@ -181,13 +268,67 @@ mender_inventory_work_function(void) {
         goto END;
     }
 
+    /* Gather persistent inventory once */
+    if (!persistent_pushed && (NULL == persistent_inventory)) {
+        if (MENDER_OK != (ret = collect_persistent_inventory())) {
+            goto END;
+        }
+    }
+
+    /* Check there's anything to do */
+    if ((persistent_pushed || (NULL == persistent_inventory)) && (0 == n_callbacks - n_persistent_cbs)) {
path: src/core/inventory.c
user: login: lluiscampos
body: Relates to my comment(s) above, `n_persistent_cbs` is never updated.
created_at: 2025-03-05T10:12:05Z
author_association: NONE
diff_hunk: @@ -156,18 +191,70 @@ mender_inventory_exit(void) {
         return ret;
     }
 
-    /* Release memory */
-    DESTROY_AND_NULL(mender_utils_keystore_delete, mender_inventory_keystore);
+    mender_free(callbacks);
+    callbacks_len          = 0;
+    n_callbacks            = 0;
+    n_persistent_callbacks = 0;
+    persistent_pushed      = false;
+
+    keystores_item_t *inv = persistent_inventory;
+    while (NULL != inv) {
+        keystores_item_t *aux = inv;
+        inv                   = inv->next;
+        mender_free(aux);
+    }
+    persistent_inventory = NULL;
+
     mender_os_mutex_give(mender_inventory_mutex);
     DESTROY_AND_NULL(mender_os_mutex_delete, mender_inventory_mutex);
 
     return ret;
 }
 
 static mender_err_t
-mender_inventory_work_function(void) {
+collect_persistent_inventory(void) {
+    for (uint8_t idx = 0; idx < n_callbacks; idx++) {
+        if (callbacks[idx].persistent) {
path: src/core/inventory.c
user: login: vpodzime
body: Good catch! You're right! I originally counted them in a loop inside this function and then realized we can track it while adding them.
created_at: 2025-03-05T18:05:38Z
author_association: COLLABORATOR
diff_hunk: @@ -667,22 +663,9 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     }
     cJSON_AddStringToObject(item, "name", "device_type");
     cJSON_AddStringToObject(item, "value", api_config.device_type);
path: src/core/api.c
user: login: vpodzime
body: Yup, I know. And I take it this will be a persistent callback/inventory, right? Do we need to have one callback for each or can both be added by a single callback?
created_at: 2025-03-05T18:06:46Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
path: include/mender/inventory.h
user: login: vpodzime
body: I don't have a strong opinion here. I like how it gets highlighted in my editor and I'm used to doing it from when I used to work with `gtk-doc`. I'll leave it up to you, guys.
created_at: 2025-03-05T18:35:19Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
+ */
+typedef const mender_err_t *(MenderInventoryCallback)(mender_keystore_t **inventory, uint8_t *inventory_len);
+
 #ifdef CONFIG_MENDER_CLIENT_INVENTORY
 
 /**
- * @brief Set mender inventory
- * @param inventory Mender inventory key/value pairs table, must end with a NULL/NULL element, NULL if not defined
+ * @brief Add mender inventory callback
+ * @param callback   A function to call to obtain inventory information
path: include/mender/inventory.h
user: login: vpodzime
body: I like to keep the descriptions aligned.
created_at: 2025-03-06T10:16:39Z
author_association: COLLABORATOR
diff_hunk: @@ -26,14 +26,31 @@ extern "C" {
 
 #include <mender/utils.h>
 
+/**
+ * @brief Inventory callback type
+ * @param inventory     Output argument for the inventory items array pointer
+ * @param inventory_len Length of the array pointed to by #inventory (limited by the type to 255
+ *                      items which should be more than enough)
+ * @return %MENDER_OK in case of success, error otherwise
path: include/mender/inventory.h
user: login: danielskinstad
body: No opinion here either
created_at: 2025-03-07T09:38:32Z
author_association: CONTRIBUTOR
diff_hunk: @@ -667,22 +663,9 @@ mender_api_publish_inventory_data(mender_keystore_t *inventory) {
     }
     cJSON_AddStringToObject(item, "name", "device_type");
     cJSON_AddStringToObject(item, "value", api_config.device_type);
path: src/core/api.c
user: login: lluiscampos
body: Single callback, together with the rest of the device provides.
created_at: 2025-03-10T07:49:42Z
author_association: NONE

PR comments:
user: login: vpodzime
created_at: 2025-03-04T17:17:51Z
body: Tests require https://github.com/mendersoftware/mender-mcu-integration/pull/53
author_association: COLLABORATOR
user: login: vpodzime
created_at: 2025-03-04T17:18:21Z
body: I still need to test this manually on a board and use something like uptime for dynamic inventory.
author_association: COLLABORATOR
user: login: vpodzime
created_at: 2025-03-07T10:18:08Z
body: > Just fyi, you don't need a `Changelog` entry for chore-commits.

I know, but it's easier for me to have this in my commit message template. :)
author_association: COLLABORATOR