| /*****************************************************************************\ | |
| * kv.c - arbitrary JSON metadata documents attached to Slurm objects | |
| ***************************************************************************** | |
| * Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | |
| * | |
| * This file is part of Slurm, a resource management program. | |
| * For details, see <https://slurm.schedmd.com/>. | |
| * Please also read the included file: DISCLAIMER. | |
| * | |
| * Slurm is free software; you can redistribute it and/or modify it under | |
| * the terms of the GNU General Public License as published by the Free | |
| * Software Foundation; either version 2 of the License, or (at your option) | |
| * any later version. | |
| * | |
| * In addition, as a special exception, the copyright holders give permission | |
| * to link the code of portions of this program with the OpenSSL library under | |
| * certain conditions as described in each individual source file, and | |
| * distribute linked combinations including the two. You must obey the GNU | |
| * General Public License in all respects for all of the code used other than | |
| * OpenSSL. If you modify file(s) with this exception, you may extend this | |
| * exception to your version of the file(s), but you are not obligated to do | |
| * so. If you do not wish to do so, delete this exception statement from your | |
| * version. If you delete this exception statement from all source files in | |
| * the program, then also delete it here. | |
| * | |
| * Slurm 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 General Public License for more | |
| * details. | |
| * | |
| * You should have received a copy of the GNU General Public License along | |
| * with Slurm; if not, write to the Free Software Foundation, Inc., | |
| * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. | |
| \*****************************************************************************/ | |
| #include <string.h> | |
| #include "slurm/slurm.h" | |
| #include "slurm/slurm_errno.h" | |
| #include "src/common/data.h" | |
| #include "src/common/kv.h" | |
| #include "src/common/log.h" | |
| #include "src/common/macros.h" | |
| #include "src/common/xassert.h" | |
| #include "src/common/xmalloc.h" | |
| #include "src/common/xstring.h" | |
| #include "src/interfaces/hash.h" | |
| #include "src/interfaces/serializer.h" | |
| strong_alias(kv_from_string, slurm_kv_from_string); | |
| strong_alias(kv_hash, slurm_kv_hash); | |
| strong_alias(kv_op_apply, slurm_kv_op_apply); | |
| strong_alias(kv_op_to_string, slurm_kv_op_to_string); | |
| strong_alias(kv_parse, slurm_kv_parse); | |
| strong_alias(kv_string_to_op, slurm_kv_string_to_op); | |
| strong_alias(kv_to_string, slurm_kv_to_string); | |
| /* Keep hook notices without hiding a later KV failure. */ | |
| #define SET_ERR(err_msg, fmt, ...) \ | |
| do { \ | |
| if (err_msg) { \ | |
| if (*(err_msg)) \ | |
| xstrcat(*(err_msg), "\n"); \ | |
| xstrfmtcat(*(err_msg), fmt, ##__VA_ARGS__); \ | |
| } \ | |
| } while (false) | |
| #define T(_op, _str, _min_len) \ | |
| { .op = KV_OP_##_op, .str = _str, .min_len = _min_len } | |
| static const struct { | |
| kv_op_t op; | |
| const char *str; | |
| size_t min_len; | |
| } kv_ops[] = { | |
| T(SET, "KV", 2), | |
| T(PATCH, "KVPatch", 3), | |
| }; | |
| #undef T | |
| extern const char *kv_op_to_string(kv_op_t op) | |
| { | |
| for (size_t i = 0; i < ARRAY_SIZE(kv_ops); i++) | |
| if (kv_ops[i].op == op) | |
| return kv_ops[i].str; | |
| return "INVALID"; | |
| } | |
| extern kv_op_t kv_string_to_op(const char *tag, size_t taglen) | |
| { | |
| for (size_t i = 0; i < ARRAY_SIZE(kv_ops); i++) | |
| if (!xstrncasecmp(tag, kv_ops[i].str, | |
| MAX(taglen, kv_ops[i].min_len))) | |
| return kv_ops[i].op; | |
| return KV_OP_NONE; | |
| } | |
| /* RET true when kv holds no document: NULL, null or an empty object */ | |
| static bool _is_empty(const data_t *kv) | |
| { | |
| if (!kv) | |
| return true; | |
| switch (data_get_type(kv)) { | |
| case DATA_TYPE_NULL: | |
| return true; | |
| case DATA_TYPE_DICT: | |
| return !data_get_dict_length(kv); | |
| default: | |
| return false; | |
| } | |
| } | |
| /* | |
| * Serialize a tree as compact JSON. | |
| * | |
| * Serializers only read src but do not declare it const. | |
| * | |
| * OUT dst - ptr to NULL, set with the JSON text (caller xfree()s) | |
| * OUT len - length of *dst | |
| * RET SLURM_SUCCESS or error | |
| */ | |
| static int _dump(const data_t *src, char **dst, size_t *len) | |
| { | |
| xassert(dst && !*dst); | |
| return serialize_g_data_to_string(dst, len, (data_t *) src, | |
| MIME_TYPE_JSON, SER_FLAGS_COMPACT); | |
| } | |
| /* | |
| * Refuse a document larger than KV_MAX_SIZE as compact JSON. slurmctld keeps | |
| * each document in memory and in its state, and sends it to clients and to | |
| * accounting, so a document must not grow without bound. | |
| */ | |
| static int _check_size(const data_t *doc, const char *what, char **err_msg) | |
| { | |
| char *str = NULL; | |
| size_t len = 0; | |
| int rc; | |
| if ((rc = _dump(doc, &str, NULL))) { | |
| SET_ERR(err_msg, "Unable to serialize %s: %s", what, | |
| slurm_strerror(rc)); | |
| xfree(str); | |
| return rc; | |
| } | |
| len = strlen(str); | |
| xfree(str); | |
| if (len > KV_MAX_SIZE) { | |
| SET_ERR(err_msg, | |
| "%s is %zu bytes, over the %d byte limit", | |
| what, | |
| len, | |
| KV_MAX_SIZE); | |
| return ESLURM_DATA_TOO_LARGE; | |
| } | |
| return SLURM_SUCCESS; | |
| } | |
| extern int kv_parse(kv_op_t op, const char *str, data_t **dst, char **err_msg) | |
| { | |
| const char *what = kv_op_to_string(op); | |
| data_t *parsed = NULL; | |
| int rc; | |
| xassert(dst && !*dst); | |
| if (((op != KV_OP_SET) && (op != KV_OP_PATCH)) || !str) { | |
| SET_ERR(err_msg, "Invalid KV update request"); | |
| return ESLURM_KV_INVALID; | |
| } | |
| /* An empty KV= clears the document, as with other string options. */ | |
| if ((op == KV_OP_SET) && !str[0]) | |
| return SLURM_SUCCESS; | |
| serializer_required(MIME_TYPE_JSON); | |
| if ((rc = serialize_g_string_to_data(&parsed, str, strlen(str), | |
| MIME_TYPE_JSON))) { | |
| SET_ERR(err_msg, "%s is not valid JSON", what); | |
| FREE_NULL_DATA(parsed); | |
| return ESLURM_KV_INVALID; | |
| } | |
| if ((op == KV_OP_PATCH) && (data_get_type(parsed) != DATA_TYPE_LIST)) { | |
| SET_ERR(err_msg, | |
| "%s must be a JSON array of operations, not %s", what, | |
| data_get_type_string(parsed)); | |
| rc = ESLURM_JSON_PATCH_INVALID; | |
| } else if ((op == KV_OP_SET) && | |
| (data_get_type(parsed) != DATA_TYPE_DICT)) { | |
| SET_ERR(err_msg, "%s must be a JSON object, not %s", what, | |
| data_get_type_string(parsed)); | |
| rc = ESLURM_KV_NOT_DICT; | |
| } else { | |
| rc = _check_size(parsed, what, err_msg); | |
| } | |
| if (rc) | |
| FREE_NULL_DATA(parsed); | |
| else | |
| *dst = parsed; | |
| return rc; | |
| } | |
| extern int kv_from_string(const char *str, data_t **dst) | |
| { | |
| data_t *parsed = NULL; | |
| int rc; | |
| xassert(dst && !*dst); | |
| if (!str || !str[0]) | |
| return SLURM_SUCCESS; | |
| serializer_required(MIME_TYPE_JSON); | |
| if ((rc = serialize_g_string_to_data(&parsed, str, strlen(str), | |
| MIME_TYPE_JSON))) { | |
| FREE_NULL_DATA(parsed); | |
| return rc; | |
| } | |
| if (data_get_type(parsed) != DATA_TYPE_DICT) { | |
| FREE_NULL_DATA(parsed); | |
| return ESLURM_KV_NOT_DICT; | |
| } | |
| *dst = parsed; | |
| return SLURM_SUCCESS; | |
| } | |
| extern char *kv_to_string(const data_t *kv) | |
| { | |
| char *str = NULL; | |
| int rc; | |
| if (_is_empty(kv)) | |
| return NULL; | |
| serializer_required(MIME_TYPE_JSON); | |
| if ((rc = _dump(kv, &str, NULL))) { | |
| error("%s: Unable to serialize KV document: %s", | |
| __func__, slurm_strerror(rc)); | |
| xfree(str); | |
| } | |
| return str; | |
| } | |
| extern char *kv_hash(const data_t *kv) | |
| { | |
| slurm_hash_t hash = { .type = HASH_PLUGIN_K12 }; | |
| char *str = NULL, *hex = NULL, *ret = NULL; | |
| int len; | |
| if (!(str = kv_to_string(kv))) | |
| return NULL; | |
| if ((len = hash_g_compute(str, strlen(str), NULL, 0, &hash)) > 0) { | |
| hex = xstring_bytes2hex(hash.hash, len, NULL); | |
| ret = xstrdup_printf("%d:%s", hash.type, hex); | |
| xfree(hex); | |
| } | |
| xfree(str); | |
| return ret; | |
| } | |
| /* | |
| * Apply an RFC 6902 JSON Patch through serializer/json-patch. | |
| * | |
| * The plugin reads the patch as JSON text, so the operand goes back through | |
| * the JSON serializer first. The plugin leaves doc untouched unless every | |
| * operation applies, and reports SLURM_NO_CHANGE_IN_DATA when a nonempty | |
| * patch leaves the document unchanged. | |
| */ | |
| static int _json_patch(data_t **doc, const data_t *patch, char **err_msg) | |
| { | |
| char *str = NULL; | |
| size_t len = 0; | |
| int rc; | |
| if ((rc = _dump(patch, &str, &len))) { | |
| SET_ERR(err_msg, "Unable to serialize KVPatch: %s", | |
| slurm_strerror(rc)); | |
| return rc; | |
| } | |
| rc = serialize_g_string_to_data(doc, str, len, MIME_TYPE_JSON_PATCH); | |
| if (rc && (rc != SLURM_NO_CHANGE_IN_DATA)) | |
| SET_ERR(err_msg, "Unable to apply KVPatch: %s", | |
| slurm_strerror(rc)); | |
| xfree(str); | |
| return rc; | |
| } | |
| extern int kv_op_apply(const data_t *src_kv, const kv_op_t src_kv_op, | |
| data_t **dst_kv, kv_op_t *dst_kv_op, char **err_msg) | |
| { | |
| data_t *doc = NULL; | |
| int rc = SLURM_SUCCESS; | |
| xassert(dst_kv); | |
| if (src_kv_op == KV_OP_NONE) | |
| return SLURM_SUCCESS; | |
| if ((src_kv_op != KV_OP_SET) && (src_kv_op != KV_OP_PATCH)) { | |
| SET_ERR(err_msg, "Invalid KV update request"); | |
| return ESLURM_KV_INVALID; | |
| } | |
| /* A NULL or null source clears the document for SET and PATCH. */ | |
| if (!src_kv || (data_get_type(src_kv) == DATA_TYPE_NULL)) | |
| goto store; | |
| /* | |
| * A KVPatch is an array of operations. Validate the resulting document | |
| * after applying the patch. | |
| */ | |
| if ((src_kv_op == KV_OP_PATCH) && | |
| (data_get_type(src_kv) != DATA_TYPE_LIST)) { | |
| SET_ERR(err_msg, "%s must be a JSON array of operations", | |
| kv_op_to_string(src_kv_op)); | |
| return ESLURM_JSON_PATCH_INVALID; | |
| } else if ((src_kv_op == KV_OP_SET) && | |
| (data_get_type(src_kv) != DATA_TYPE_DICT)) { | |
| SET_ERR(err_msg, "%s must be a JSON object", | |
| kv_op_to_string(src_kv_op)); | |
| return ESLURM_KV_NOT_DICT; | |
| } | |
| serializer_required(MIME_TYPE_JSON); | |
| if (src_kv_op == KV_OP_PATCH) | |
| serializer_required(MIME_TYPE_JSON_PATCH); | |
| /* | |
| * Work on a copy so a rejected update leaves *dst_kv untouched. If no | |
| * document exists yet, or the patch itself is in *dst_kv, start with | |
| * an empty object. | |
| */ | |
| if (src_kv_op == KV_OP_SET) | |
| rc = data_clone(src_kv, doc); | |
| else if ((src_kv == *dst_kv) || _is_empty(*dst_kv)) | |
| doc = data_set_dict(data_new()); | |
| else | |
| rc = data_clone(*dst_kv, doc); | |
| if (rc) { | |
| SET_ERR(err_msg, "Unable to copy KV document: %s", | |
| slurm_strerror(rc)); | |
| goto cleanup; | |
| } | |
| if (src_kv_op == KV_OP_PATCH) { | |
| rc = _json_patch(&doc, src_kv, err_msg); | |
| if (rc && (rc != SLURM_NO_CHANGE_IN_DATA)) | |
| goto cleanup; | |
| } | |
| /* | |
| * "replace" or "remove" against path "" can leave something that is | |
| * not an object behind. The patch ran against a copy, so rejecting it | |
| * here still costs nothing. | |
| */ | |
| if (data_get_type(doc) != DATA_TYPE_DICT) { | |
| SET_ERR(err_msg, "%s result must be a JSON object, not %s", | |
| kv_op_to_string(src_kv_op), data_get_type_string(doc)); | |
| rc = ESLURM_KV_NOT_DICT; | |
| goto cleanup; | |
| } | |
| if (_is_empty(doc)) | |
| FREE_NULL_DATA(doc); | |
| else if (!rc && (rc = _check_size(doc, "KV", err_msg))) | |
| goto cleanup; | |
| store: | |
| FREE_NULL_DATA(*dst_kv); | |
| *dst_kv = doc; | |
| if (dst_kv_op) | |
| *dst_kv_op = doc ? KV_OP_SET : KV_OP_NONE; | |
| doc = NULL; | |
| cleanup: | |
| FREE_NULL_DATA(doc); | |
| return rc; | |
| } |