From e61089e9cc818f612ebf7830930cbb162f2dab86 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 9 Nov 2026 10:32:59 +0100 Subject: [PATCH 1/7] Moved augments loading code to its own file Moved from generic_agent.[ch] to augments.[ch], code unchanged. First step towards splitting it up and sharing code with cmdb.c. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/Makefile.am | 1 + libpromises/augments.c | 783 ++++++++++++++++++++++++++++++++++++ libpromises/augments.h | 38 ++ libpromises/generic_agent.c | 746 +--------------------------------- libpromises/generic_agent.h | 2 - 5 files changed, 823 insertions(+), 747 deletions(-) create mode 100644 libpromises/augments.c create mode 100644 libpromises/augments.h diff --git a/libpromises/Makefile.am b/libpromises/Makefile.am index 99368a35df4..71b88fd9b40 100644 --- a/libpromises/Makefile.am +++ b/libpromises/Makefile.am @@ -66,6 +66,7 @@ libpromises_la_SOURCES = \ assoc.c assoc.h \ attributes.c attributes.h \ audit.c audit.h \ + augments.c augments.h \ bootstrap.c bootstrap.h bootstrap.inc failsafe.cf \ cf-windows-functions.h \ cf3.defs.h \ diff --git a/libpromises/augments.c b/libpromises/augments.c new file mode 100644 index 00000000000..57258bc2776 --- /dev/null +++ b/libpromises/augments.c @@ -0,0 +1,783 @@ +/* + Copyright 2024 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program 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; version 3. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include + +#include + +#include +#include +#include /* StringSet */ +#include /* StringEqual(), StringEndsWith(), StringFormat() */ +#include /* FileCanOpen() */ +#include /* EvalContext*(), ClassesMatching(), JsonExpandElement() */ +#include /* VarRef, VarRefParse() */ +#include /* ExpandScalar() */ +#include /* RlistFromContainer() */ +#include /* ReadJsonFile(), CMDB_SOURCE_TAG */ + +#define AUGMENTS_VARIABLES_TAGS "tags" +#define AUGMENTS_VARIABLES_DATA "value" +#define AUGMENTS_CLASSES_TAGS "tags" +#define AUGMENTS_CLASSES_CLASS_EXPRESSIONS "class_expressions" +#define AUGMENTS_CLASSES_REGULAR_EXPRESSIONS "regular_expressions" +#define AUGMENTS_COMMENT_KEY "comment" + +static bool LoadAugmentsFiles(EvalContext *ctx, const char *filename); + +static bool CheckContextClassmatch(EvalContext *ctx, const char *class_str) +{ + if (StringEndsWith(class_str, "::")) // Treat as class expression, not regex + { + const size_t length = strlen(class_str); + if (length <= 2) + { + assert(length == 2); // True because StringEndsWith + Log(LOG_LEVEL_ERR, + "Invalid class expression in augments: '%s'", + class_str); + return false; + } + + char *const tmp_class_str = xstrdup(class_str); + assert(strlen(tmp_class_str) == length); + + tmp_class_str[length - 2] = '\0'; // 2 = strlen("::") + const bool found = IsDefinedClass(ctx, tmp_class_str); + + free(tmp_class_str); + return found; + } + + ClassTableIterator *iter = EvalContextClassTableIteratorNewGlobal(ctx, NULL, true, true); + StringSet *global_matches = ClassesMatching(ctx, iter, class_str, NULL, true); // returns early + + const bool found = (StringSetSize(global_matches) > 0); + + StringSetDestroy(global_matches); + ClassTableIteratorDestroy(iter); + + return found; +} + +static StringSet *GetTagsFromAugmentsTags(const char *item_type, + const char *key, + const JsonElement *json_tags, + const char *default_tag, + const char *filename) +{ + StringSet *tags = NULL; + if (JSON_NOT_NULL(json_tags)) + { + if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) || + (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags))) + { + Log(LOG_LEVEL_ERR, + "Invalid tags information for %s '%s' in augments file '%s':" + " must be a JSON array of strings", + item_type, key, filename); + } + else + { + tags = JsonArrayToStringSet(json_tags); + if (tags == NULL) + { + Log(LOG_LEVEL_ERR, + "Invalid meta information %s '%s' in augments file '%s':" + " must be a JSON array of strings", + item_type, key, filename); + } + } + } + if (tags == NULL) + { + tags = StringSetNew(); + } + StringSetAdd(tags, xstrdup(default_tag)); + + return tags; +} + +static inline bool CanSetVariable(const EvalContext *ctx, VarRef *var_ref) +{ + assert(var_ref != NULL); + + bool null_ns = false; + if (var_ref->ns == NULL) + { + null_ns = true; + var_ref->ns = "default"; + } + StringSet *tags = EvalContextVariableTags(ctx, var_ref); + bool can_set = ((tags == NULL) || !StringSetContains(tags, CMDB_SOURCE_TAG)); + if (!can_set) + { + Log(LOG_LEVEL_VERBOSE, + "Cannot set variable %s:%s.%s from augments, already defined from host-specific data", + var_ref->ns, var_ref->scope, var_ref->lval); + } + if (null_ns) + { + var_ref->ns = NULL; + } + + return can_set; +} + +static inline bool CanSetClass(const EvalContext *ctx, const char *class_spec) +{ + char *ns = NULL; + const char *ns_delim = strchr(class_spec, ':'); + if (ns_delim != NULL) + { + ns = xstrndup(class_spec, ns_delim - class_spec); + class_spec = ns_delim + 1; + } + + StringSet *tags = EvalContextClassTags(ctx, ns, class_spec); + bool can_set = ((tags == NULL) || !StringSetContains(tags, CMDB_SOURCE_TAG)); + if (!can_set) + { + Log(LOG_LEVEL_VERBOSE, + "Cannot set class %s:%s from augments, already defined from host-specific data", + ns, class_spec); + } + + return can_set; +} + +static inline const char *GetAugmentsComment(const char *item_type, const char *identifier, + const char *file_name, const JsonElement *json_object) +{ + assert(JsonGetType(json_object) == JSON_TYPE_OBJECT); + + JsonElement *json_comment = JsonObjectGet(json_object, AUGMENTS_COMMENT_KEY); + if (NULL_JSON(json_comment)) + { + return NULL; + } + + if (JsonGetType(json_comment) != JSON_TYPE_STRING) + { + Log(LOG_LEVEL_ERR, + "Invalid type of the 'comment' field for the '%s' %s in augments data in '%s', must be a string", + identifier, item_type, file_name); + return NULL; + } + + return JsonPrimitiveGetAsString(json_comment); +} + +static bool LoadAugmentsData(EvalContext *ctx, const char *filename, const JsonElement* augment) +{ + bool loaded = false; + + if (JsonGetElementType(augment) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(augment) != JSON_CONTAINER_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments file contents in '%s', must be a JSON object", filename); + } + else + { + loaded = true; + Log(LOG_LEVEL_VERBOSE, "Loaded augments file '%s', installing contents", filename); + + JsonIterator iter = JsonIteratorInit(augment); + const char *key; + while ((key = JsonIteratorNextKey(&iter))) + { + if (!(StringEqual(key, "vars") || + StringEqual(key, "classes") || + StringEqual(key, "inputs") || + StringEqual(key, "augments") || + StringEqual(key, "variables"))) + { + Log(LOG_LEVEL_VERBOSE, "Unknown augments key '%s' in file '%s', skipping it", + key, filename); + } + } + + /* load variables (if any) */ + JsonElement *element = JsonObjectGet(augment, "vars"); + if (JSON_NOT_NULL(element)) + { + JsonElement* vars = JsonExpandElement(ctx, element); + + if (vars == NULL || + JsonGetElementType(vars) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(vars) != JSON_CONTAINER_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments vars in '%s', must be a JSON object", filename); + goto vars_cleanup; + } + + JsonIterator iter = JsonIteratorInit(vars); + const char *vkey; + while ((vkey = JsonIteratorNextKey(&iter))) + { + VarRef *ref = VarRefParse(vkey); + if (ref->ns != NULL) + { + if (ref->scope == NULL) + { + Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" + " (bundle name has to be specified if namespace is specified)", filename, vkey); + VarRefDestroy(ref); + continue; + } + } + if (ref->scope == NULL) + { + ref->scope = xstrdup("def"); + } + + JsonElement *data = JsonObjectGet(vars, vkey); + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *value = JsonPrimitiveToString(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, vkey, value, CF_DATA_TYPE_STRING, "source=augments_file"); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", + vkey, value, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePut(ctx, ref, value, CF_DATA_TYPE_STRING, "source=augments_file"); + } + } + free(value); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(data)) + { + // map to slist if the data only has primitives + Rlist *data_as_rlist = RlistFromContainer(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + vkey, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePut(ctx, ref, data_as_rlist, CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); + } + } + + RlistDestroy(data_as_rlist); + } + else // install as a data container + { + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + vkey, data, + CF_DATA_TYPE_CONTAINER, + "source=augments_file"); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePut(ctx, ref, data, + CF_DATA_TYPE_CONTAINER, + "source=augments_file"); + } + } + } + VarRefDestroy(ref); + } + + vars_cleanup: + JsonDestroy(vars); + } + + /* Uses the new format allowing metadata (CFE-3633) */ + element = JsonObjectGet(augment, "variables"); + if (JSON_NOT_NULL(element)) + { + JsonElement* variables = JsonExpandElement(ctx, element); + + if (variables == NULL || JsonGetType(variables) != JSON_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments variables in '%s', must be a JSON object", filename); + goto variables_cleanup; + } + + JsonIterator variables_iter = JsonIteratorInit(variables); + const char *vkey; + while ((vkey = JsonIteratorNextKey(&variables_iter))) + { + VarRef *ref = VarRefParse(vkey); + if (ref->ns != NULL) + { + if (ref->scope == NULL) + { + Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" + " (bundle name has to be specified if namespace is specified)", filename, vkey); + VarRefDestroy(ref); + continue; + } + } + if (ref->scope == NULL) + { + ref->scope = xstrdup("def"); + } + + const JsonElement *const var_info = JsonObjectGet(variables, vkey); + + const JsonElement *data; + StringSet *tags; + const char *comment = NULL; + + if (JsonGetType(var_info) == JSON_TYPE_OBJECT) + { + data = JsonObjectGet(var_info, AUGMENTS_VARIABLES_DATA); + + if (NULL_JSON(data)) + { + Log(LOG_LEVEL_ERR, "Missing value for the augments variable '%s' in '%s' (value field is required)", + vkey, filename); + VarRefDestroy(ref); + continue; + } + + const JsonElement *json_tags = JsonObjectGet(var_info, AUGMENTS_VARIABLES_TAGS); + tags = GetTagsFromAugmentsTags("variable", vkey, json_tags, "source=augments_file", filename); + comment = GetAugmentsComment("variable", vkey, filename, var_info); + } + else + { + // Just a bare value, like in "vars", no metadata + data = var_info; + tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + } + + assert(tags != NULL); + assert(data != NULL); + + bool installed = false; + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *value = JsonPrimitiveToString(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, vkey, value, + CF_DATA_TYPE_STRING, tags, comment); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", + vkey, value, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING, + tags, comment); + } + } + free(value); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives((JsonElement *) data)) + { + // map to slist if the data only has primitives + Rlist *data_as_rlist = RlistFromContainer(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, + vkey, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + tags, comment); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + tags, comment); + } + } + + RlistDestroy(data_as_rlist); + } + else // install as a data container + { + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, + vkey, data, + CF_DATA_TYPE_CONTAINER, + tags, comment); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) + { + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data, + CF_DATA_TYPE_CONTAINER, + tags, comment); + } + } + } + VarRefDestroy(ref); + if (!installed) + { + /* EvalContextVariablePutTagsSetWithComment() and + * EvalContextVariablePutSpecialTagsSetWithComment() take + * over tags in case of success. Otherwise we have to + * destroy the set. */ + StringSetDestroy(tags); + } + } + + variables_cleanup: + JsonDestroy(variables); + } + + /* load classes (if any) */ + element = JsonObjectGet(augment, "classes"); + if (JSON_NOT_NULL(element)) + { + JsonElement* classes = JsonExpandElement(ctx, element); + + if (JsonGetElementType(classes) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(classes) != JSON_CONTAINER_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments classes in '%s', must be a JSON object", filename); + goto classes_cleanup; + } + + const char default_tags[] = "source=augments_file"; + JsonIterator iter = JsonIteratorInit(classes); + const char *ckey; + while ((ckey = JsonIteratorNextKey(&iter))) + { + JsonElement *data = JsonObjectGet(classes, ckey); + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *check = JsonPrimitiveToString(data); + // check if class is true + if (CheckContextClassmatch(ctx, check)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) + { + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + } + } + else if (StringEqual(check, "true")) + { + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + } + free(check); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(data)) + { + // check if each class is true + JsonIterator data_iter = JsonIteratorInit(data); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&data_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) + { + char *check = JsonPrimitiveToString(el); + if (CheckContextClassmatch(ctx, check)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) + { + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + } + free(check); + break; + } + else if (StringEqual(check, "true")) + { + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + } + + free(check); + } + } + else if (JsonGetType(data) == JSON_TYPE_OBJECT) + { + const JsonElement *class_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_CLASS_EXPRESSIONS); + const JsonElement *reg_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_REGULAR_EXPRESSIONS); + const JsonElement *json_tags = JsonObjectGet(data, AUGMENTS_CLASSES_TAGS); + + if ((JSON_NOT_NULL(class_exprs) && JSON_NOT_NULL(reg_exprs)) || + (NULL_JSON(class_exprs) && NULL_JSON(reg_exprs))) + { + Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s':" + " either \"class_expressions\" or \"regular_expressions\" need to be specified", + ckey, filename); + continue; + } + + StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, + "source=augments_file", filename); + const char *comment = GetAugmentsComment("class", ckey, filename, data); + bool installed = false; + JsonIterator exprs_iter = JsonIteratorInit(class_exprs ? class_exprs : reg_exprs); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&exprs_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) + { + char *check = JsonPrimitiveToString(el); + if (CheckContextClassmatch(ctx, check)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) + { + installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, + tags, comment); + } + else if (StringEqual(check, "true")) + { + installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, + tags, comment); + } + free(check); + break; + } + + free(check); + } + if (!installed) + { + /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in + * case of success. Otherwise we have to destroy the set. */ + StringSetDestroy(tags); + } + } + else + { + Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s'", + ckey, filename); + } + } + + classes_cleanup: + JsonDestroy(classes); + } + + /* load inputs (if any) */ + element = JsonObjectGet(augment, "inputs"); + if (JSON_NOT_NULL(element)) + { + JsonElement* inputs = JsonExpandElement(ctx, element); + + if (JsonGetElementType(inputs) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(inputs) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(inputs)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments def.augments_inputs from file '%s'", + filename); + Rlist *rlist = RlistFromContainer(inputs); + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + "augments_inputs", rlist, + CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); + RlistDestroy(rlist); + } + else + { + Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", + filename); + } + + JsonDestroy(inputs); + } + + /* load further def.json files (if any) */ + element = JsonObjectGet(augment, "augments"); + if (JSON_NOT_NULL(element)) + { + JsonElement* further_augments = element; + assert(further_augments != NULL); + + if (JsonGetElementType(further_augments) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(further_augments) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(further_augments)) + { + JsonIterator iter = JsonIteratorInit(further_augments); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true)) != NULL) + { + char *nested_filename = JsonPrimitiveToString(el); + bool further_loaded = LoadAugmentsFiles(ctx, nested_filename); + if (further_loaded) + { + Log(LOG_LEVEL_VERBOSE, "Completed augmenting from file '%s'", nested_filename); + } + else + { + Log(LOG_LEVEL_ERR, "Could not load requested further augments from file '%s'", nested_filename); + } + free(nested_filename); + } + } + else + { + Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", + filename); + } + } + } + + return loaded; +} + +static bool LoadAugmentsFiles(EvalContext *ctx, const char *unexpanded_filename) +{ + bool loaded = false; + + char *filename = ExpandScalar(ctx, NULL, "this", unexpanded_filename, NULL); + + if (strstr(filename, "/.json")) + { + Log(LOG_LEVEL_DEBUG, + "Skipping augments file '%s' because it failed to expand the base filename, resulting in '%s'", + unexpanded_filename, filename); + } + else + { + Log(LOG_LEVEL_DEBUG, "Searching for augments file '%s' (original '%s')", + filename, unexpanded_filename); + if (FileCanOpen(filename, "r")) + { + // 5 MB should be enough for most reasonable def.json data + JsonElement* augment = ReadJsonFile(filename, LOG_LEVEL_ERR, 5 * 1024 * 1024); + if (augment != NULL) + { + loaded = LoadAugmentsData(ctx, filename, augment); + JsonDestroy(augment); + } + } + else + { + Log(LOG_LEVEL_VERBOSE, "could not load JSON augments from '%s'", filename); + } + } + + free(filename); + return loaded; +} + +static bool IsFile(const char *const filename) +{ + struct stat buffer; + if (stat(filename, &buffer) != 0) + { + return false; + } + if (S_ISREG(buffer.st_mode) != 0) + { + return true; + } + return false; +} + +void LoadAugments(EvalContext *ctx, GenericAgentConfig *config) +{ + assert(config != NULL); + + char* def_json = NULL; + // --ignore-preferred-augments command line option: + if (config->ignore_preferred_augments) + { + EvalContextClassPutHard(ctx, "ignore_preferred_augments", "source=command_line_option"); + // def_json is NULL so it will be assigned below + } + else + { + def_json = StringFormat("%s%c%s", config->input_dir, FILE_SEPARATOR, "def_preferred.json"); + if (!IsFile(def_json)) + { + // def_preferred.json does not exist or we cannot read it + FREE_AND_NULL(def_json); + } + } + + if (def_json == NULL) + { + // No def_preferred.json, either because the feature is disabled + // or we could not read the file. + // Fall back to old / default behavior, using def.json: + def_json = StringFormat("%s%c%s", config->input_dir, FILE_SEPARATOR, "def.json"); + } + Log(LOG_LEVEL_VERBOSE, "Loading JSON augments from '%s' (input dir '%s', input file '%s'", def_json, config->input_dir, config->input_file); + LoadAugmentsFiles(ctx, def_json); + free(def_json); +} diff --git a/libpromises/augments.h b/libpromises/augments.h new file mode 100644 index 00000000000..d8939f3edf2 --- /dev/null +++ b/libpromises/augments.h @@ -0,0 +1,38 @@ +/* + Copyright 2024 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program 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; version 3. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef CFENGINE_AUGMENTS_H +#define CFENGINE_AUGMENTS_H + +#include +#include /* GenericAgentConfig */ + +/** + * @brief Load augments data (def_preferred.json or def.json in the policy + * input directory) and install the defined variables and classes + * into the evaluation context. + */ +void LoadAugments(EvalContext *ctx, GenericAgentConfig *config); + +#endif /* CFENGINE_AUGMENTS_H */ diff --git a/libpromises/generic_agent.c b/libpromises/generic_agent.c index 6cc9f6897c0..9a738f5f117 100644 --- a/libpromises/generic_agent.c +++ b/libpromises/generic_agent.c @@ -73,15 +73,9 @@ #include #include #include /* LoadCMDBData() */ +#include /* LoadAugments() */ #include "cf3.defs.h" -#define AUGMENTS_VARIABLES_TAGS "tags" -#define AUGMENTS_VARIABLES_DATA "value" -#define AUGMENTS_CLASSES_TAGS "tags" -#define AUGMENTS_CLASSES_CLASS_EXPRESSIONS "class_expressions" -#define AUGMENTS_CLASSES_REGULAR_EXPRESSIONS "regular_expressions" -#define AUGMENTS_COMMENT_KEY "comment" - static pthread_once_t pid_cleanup_once = PTHREAD_ONCE_INIT; /* GLOBAL_T */ static char PIDFILE[CF_BUFSIZE] = ""; /* GLOBAL_C */ @@ -102,8 +96,6 @@ static char* ReadReleaseIdFromReleaseIdFileMasterfiles(const char *maybe_dirname static bool MissingInputFile(const char *input_file); -static bool LoadAugmentsFiles(EvalContext *ctx, const char* filename); - static void GetChangesChrootDir(char *buf, size_t buf_size); static void DeleteChangesChroot(); static int ParseFacility(const char *name); @@ -242,742 +234,6 @@ Policy *SelectAndLoadPolicy(GenericAgentConfig *config, EvalContext *ctx, bool v return policy; } -static bool CheckContextClassmatch(EvalContext *ctx, const char *class_str) -{ - if (StringEndsWith(class_str, "::")) // Treat as class expression, not regex - { - const size_t length = strlen(class_str); - if (length <= 2) - { - assert(length == 2); // True because StringEndsWith - Log(LOG_LEVEL_ERR, - "Invalid class expression in augments: '%s'", - class_str); - return false; - } - - char *const tmp_class_str = xstrdup(class_str); - assert(strlen(tmp_class_str) == length); - - tmp_class_str[length - 2] = '\0'; // 2 = strlen("::") - const bool found = IsDefinedClass(ctx, tmp_class_str); - - free(tmp_class_str); - return found; - } - - ClassTableIterator *iter = EvalContextClassTableIteratorNewGlobal(ctx, NULL, true, true); - StringSet *global_matches = ClassesMatching(ctx, iter, class_str, NULL, true); // returns early - - const bool found = (StringSetSize(global_matches) > 0); - - StringSetDestroy(global_matches); - ClassTableIteratorDestroy(iter); - - return found; -} - -static StringSet *GetTagsFromAugmentsTags(const char *item_type, - const char *key, - const JsonElement *json_tags, - const char *default_tag, - const char *filename) -{ - StringSet *tags = NULL; - if (JSON_NOT_NULL(json_tags)) - { - if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) || - (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags))) - { - Log(LOG_LEVEL_ERR, - "Invalid tags information for %s '%s' in augments file '%s':" - " must be a JSON array of strings", - item_type, key, filename); - } - else - { - tags = JsonArrayToStringSet(json_tags); - if (tags == NULL) - { - Log(LOG_LEVEL_ERR, - "Invalid meta information %s '%s' in augments file '%s':" - " must be a JSON array of strings", - item_type, key, filename); - } - } - } - if (tags == NULL) - { - tags = StringSetNew(); - } - StringSetAdd(tags, xstrdup(default_tag)); - - return tags; -} - -static inline bool CanSetVariable(const EvalContext *ctx, VarRef *var_ref) -{ - assert(var_ref != NULL); - - bool null_ns = false; - if (var_ref->ns == NULL) - { - null_ns = true; - var_ref->ns = "default"; - } - StringSet *tags = EvalContextVariableTags(ctx, var_ref); - bool can_set = ((tags == NULL) || !StringSetContains(tags, CMDB_SOURCE_TAG)); - if (!can_set) - { - Log(LOG_LEVEL_VERBOSE, - "Cannot set variable %s:%s.%s from augments, already defined from host-specific data", - var_ref->ns, var_ref->scope, var_ref->lval); - } - if (null_ns) - { - var_ref->ns = NULL; - } - - return can_set; -} - -static inline bool CanSetClass(const EvalContext *ctx, const char *class_spec) -{ - char *ns = NULL; - const char *ns_delim = strchr(class_spec, ':'); - if (ns_delim != NULL) - { - ns = xstrndup(class_spec, ns_delim - class_spec); - class_spec = ns_delim + 1; - } - - StringSet *tags = EvalContextClassTags(ctx, ns, class_spec); - bool can_set = ((tags == NULL) || !StringSetContains(tags, CMDB_SOURCE_TAG)); - if (!can_set) - { - Log(LOG_LEVEL_VERBOSE, - "Cannot set class %s:%s from augments, already defined from host-specific data", - ns, class_spec); - } - - return can_set; -} - -static inline const char *GetAugmentsComment(const char *item_type, const char *identifier, - const char *file_name, const JsonElement *json_object) -{ - assert(JsonGetType(json_object) == JSON_TYPE_OBJECT); - - JsonElement *json_comment = JsonObjectGet(json_object, AUGMENTS_COMMENT_KEY); - if (NULL_JSON(json_comment)) - { - return NULL; - } - - if (JsonGetType(json_comment) != JSON_TYPE_STRING) - { - Log(LOG_LEVEL_ERR, - "Invalid type of the 'comment' field for the '%s' %s in augments data in '%s', must be a string", - identifier, item_type, file_name); - return NULL; - } - - return JsonPrimitiveGetAsString(json_comment); -} - -static bool LoadAugmentsData(EvalContext *ctx, const char *filename, const JsonElement* augment) -{ - bool loaded = false; - - if (JsonGetElementType(augment) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(augment) != JSON_CONTAINER_TYPE_OBJECT) - { - Log(LOG_LEVEL_ERR, "Invalid augments file contents in '%s', must be a JSON object", filename); - } - else - { - loaded = true; - Log(LOG_LEVEL_VERBOSE, "Loaded augments file '%s', installing contents", filename); - - JsonIterator iter = JsonIteratorInit(augment); - const char *key; - while ((key = JsonIteratorNextKey(&iter))) - { - if (!(StringEqual(key, "vars") || - StringEqual(key, "classes") || - StringEqual(key, "inputs") || - StringEqual(key, "augments") || - StringEqual(key, "variables"))) - { - Log(LOG_LEVEL_VERBOSE, "Unknown augments key '%s' in file '%s', skipping it", - key, filename); - } - } - - /* load variables (if any) */ - JsonElement *element = JsonObjectGet(augment, "vars"); - if (JSON_NOT_NULL(element)) - { - JsonElement* vars = JsonExpandElement(ctx, element); - - if (vars == NULL || - JsonGetElementType(vars) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(vars) != JSON_CONTAINER_TYPE_OBJECT) - { - Log(LOG_LEVEL_ERR, "Invalid augments vars in '%s', must be a JSON object", filename); - goto vars_cleanup; - } - - JsonIterator iter = JsonIteratorInit(vars); - const char *vkey; - while ((vkey = JsonIteratorNextKey(&iter))) - { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) - { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } - } - if (ref->scope == NULL) - { - ref->scope = xstrdup("def"); - } - - JsonElement *data = JsonObjectGet(vars, vkey); - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, vkey, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - free(value); - } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) - { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data_as_rlist, CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } - - RlistDestroy(data_as_rlist); - } - else // install as a data container - { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } - } - VarRefDestroy(ref); - } - - vars_cleanup: - JsonDestroy(vars); - } - - /* Uses the new format allowing metadata (CFE-3633) */ - element = JsonObjectGet(augment, "variables"); - if (JSON_NOT_NULL(element)) - { - JsonElement* variables = JsonExpandElement(ctx, element); - - if (variables == NULL || JsonGetType(variables) != JSON_TYPE_OBJECT) - { - Log(LOG_LEVEL_ERR, "Invalid augments variables in '%s', must be a JSON object", filename); - goto variables_cleanup; - } - - JsonIterator variables_iter = JsonIteratorInit(variables); - const char *vkey; - while ((vkey = JsonIteratorNextKey(&variables_iter))) - { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) - { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } - } - if (ref->scope == NULL) - { - ref->scope = xstrdup("def"); - } - - const JsonElement *const var_info = JsonObjectGet(variables, vkey); - - const JsonElement *data; - StringSet *tags; - const char *comment = NULL; - - if (JsonGetType(var_info) == JSON_TYPE_OBJECT) - { - data = JsonObjectGet(var_info, AUGMENTS_VARIABLES_DATA); - - if (NULL_JSON(data)) - { - Log(LOG_LEVEL_ERR, "Missing value for the augments variable '%s' in '%s' (value field is required)", - vkey, filename); - VarRefDestroy(ref); - continue; - } - - const JsonElement *json_tags = JsonObjectGet(var_info, AUGMENTS_VARIABLES_TAGS); - tags = GetTagsFromAugmentsTags("variable", vkey, json_tags, "source=augments_file", filename); - comment = GetAugmentsComment("variable", vkey, filename, var_info); - } - else - { - // Just a bare value, like in "vars", no metadata - data = var_info; - tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); - } - - assert(tags != NULL); - assert(data != NULL); - - bool installed = false; - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, vkey, value, - CF_DATA_TYPE_STRING, tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING, - tags, comment); - } - } - free(value); - } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives((JsonElement *) data)) - { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - - RlistDestroy(data_as_rlist); - } - else // install as a data container - { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - } - VarRefDestroy(ref); - if (!installed) - { - /* EvalContextVariablePutTagsSetWithComment() and - * EvalContextVariablePutSpecialTagsSetWithComment() take - * over tags in case of success. Otherwise we have to - * destroy the set. */ - StringSetDestroy(tags); - } - } - - variables_cleanup: - JsonDestroy(variables); - } - - /* load classes (if any) */ - element = JsonObjectGet(augment, "classes"); - if (JSON_NOT_NULL(element)) - { - JsonElement* classes = JsonExpandElement(ctx, element); - - if (JsonGetElementType(classes) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(classes) != JSON_CONTAINER_TYPE_OBJECT) - { - Log(LOG_LEVEL_ERR, "Invalid augments classes in '%s', must be a JSON object", filename); - goto classes_cleanup; - } - - const char default_tags[] = "source=augments_file"; - JsonIterator iter = JsonIteratorInit(classes); - const char *ckey; - while ((ckey = JsonIteratorNextKey(&iter))) - { - JsonElement *data = JsonObjectGet(classes, ckey); - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *check = JsonPrimitiveToString(data); - // check if class is true - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - } - else if (StringEqual(check, "true")) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - free(check); - } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) - { - // check if each class is true - JsonIterator data_iter = JsonIteratorInit(data); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&data_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) - { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - free(check); - break; - } - else if (StringEqual(check, "true")) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - - free(check); - } - } - else if (JsonGetType(data) == JSON_TYPE_OBJECT) - { - const JsonElement *class_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_CLASS_EXPRESSIONS); - const JsonElement *reg_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_REGULAR_EXPRESSIONS); - const JsonElement *json_tags = JsonObjectGet(data, AUGMENTS_CLASSES_TAGS); - - if ((JSON_NOT_NULL(class_exprs) && JSON_NOT_NULL(reg_exprs)) || - (NULL_JSON(class_exprs) && NULL_JSON(reg_exprs))) - { - Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s':" - " either \"class_expressions\" or \"regular_expressions\" need to be specified", - ckey, filename); - continue; - } - - StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, - "source=augments_file", filename); - const char *comment = GetAugmentsComment("class", ckey, filename, data); - bool installed = false; - JsonIterator exprs_iter = JsonIteratorInit(class_exprs ? class_exprs : reg_exprs); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&exprs_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) - { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - else if (StringEqual(check, "true")) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - free(check); - break; - } - - free(check); - } - if (!installed) - { - /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in - * case of success. Otherwise we have to destroy the set. */ - StringSetDestroy(tags); - } - } - else - { - Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s'", - ckey, filename); - } - } - - classes_cleanup: - JsonDestroy(classes); - } - - /* load inputs (if any) */ - element = JsonObjectGet(augment, "inputs"); - if (JSON_NOT_NULL(element)) - { - JsonElement* inputs = JsonExpandElement(ctx, element); - - if (JsonGetElementType(inputs) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(inputs) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(inputs)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments def.augments_inputs from file '%s'", - filename); - Rlist *rlist = RlistFromContainer(inputs); - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - "augments_inputs", rlist, - CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - RlistDestroy(rlist); - } - else - { - Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", - filename); - } - - JsonDestroy(inputs); - } - - /* load further def.json files (if any) */ - element = JsonObjectGet(augment, "augments"); - if (JSON_NOT_NULL(element)) - { - JsonElement* further_augments = element; - assert(further_augments != NULL); - - if (JsonGetElementType(further_augments) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(further_augments) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(further_augments)) - { - JsonIterator iter = JsonIteratorInit(further_augments); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true)) != NULL) - { - char *nested_filename = JsonPrimitiveToString(el); - bool further_loaded = LoadAugmentsFiles(ctx, nested_filename); - if (further_loaded) - { - Log(LOG_LEVEL_VERBOSE, "Completed augmenting from file '%s'", nested_filename); - } - else - { - Log(LOG_LEVEL_ERR, "Could not load requested further augments from file '%s'", nested_filename); - } - free(nested_filename); - } - } - else - { - Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", - filename); - } - } - } - - return loaded; -} - -static bool LoadAugmentsFiles(EvalContext *ctx, const char *unexpanded_filename) -{ - bool loaded = false; - - char *filename = ExpandScalar(ctx, NULL, "this", unexpanded_filename, NULL); - - if (strstr(filename, "/.json")) - { - Log(LOG_LEVEL_DEBUG, - "Skipping augments file '%s' because it failed to expand the base filename, resulting in '%s'", - unexpanded_filename, filename); - } - else - { - Log(LOG_LEVEL_DEBUG, "Searching for augments file '%s' (original '%s')", - filename, unexpanded_filename); - if (FileCanOpen(filename, "r")) - { - // 5 MB should be enough for most reasonable def.json data - JsonElement* augment = ReadJsonFile(filename, LOG_LEVEL_ERR, 5 * 1024 * 1024); - if (augment != NULL) - { - loaded = LoadAugmentsData(ctx, filename, augment); - JsonDestroy(augment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "could not load JSON augments from '%s'", filename); - } - } - - free(filename); - return loaded; -} - -static bool IsFile(const char *const filename) -{ - struct stat buffer; - if (stat(filename, &buffer) != 0) - { - return false; - } - if (S_ISREG(buffer.st_mode) != 0) - { - return true; - } - return false; -} - -void LoadAugments(EvalContext *ctx, GenericAgentConfig *config) -{ - assert(config != NULL); - - char* def_json = NULL; - // --ignore-preferred-augments command line option: - if (config->ignore_preferred_augments) - { - EvalContextClassPutHard(ctx, "ignore_preferred_augments", "source=command_line_option"); - // def_json is NULL so it will be assigned below - } - else - { - def_json = StringFormat("%s%c%s", config->input_dir, FILE_SEPARATOR, "def_preferred.json"); - if (!IsFile(def_json)) - { - // def_preferred.json does not exist or we cannot read it - FREE_AND_NULL(def_json); - } - } - - if (def_json == NULL) - { - // No def_preferred.json, either because the feature is disabled - // or we could not read the file. - // Fall back to old / default behavior, using def.json: - def_json = StringFormat("%s%c%s", config->input_dir, FILE_SEPARATOR, "def.json"); - } - Log(LOG_LEVEL_VERBOSE, "Loading JSON augments from '%s' (input dir '%s', input file '%s'", def_json, config->input_dir, config->input_file); - LoadAugmentsFiles(ctx, def_json); - free(def_json); -} - static void AddPolicyEntryVariables (EvalContext *ctx, const GenericAgentConfig *config) { char *abs_input_path = GetAbsolutePath(config->input_file); diff --git a/libpromises/generic_agent.h b/libpromises/generic_agent.h index 58f792ab763..8c51f271aee 100644 --- a/libpromises/generic_agent.h +++ b/libpromises/generic_agent.h @@ -153,8 +153,6 @@ bool GenericAgentPostLoadInit(const EvalContext *ctx); void SetupSignalsForAgent(void); -void LoadAugments(EvalContext *ctx, GenericAgentConfig *config); - void GenericAgentShowContextsFormatted(EvalContext *ctx, const char *regexp); void GenericAgentShowVariablesFormatted(EvalContext *ctx, const char *regexp); From 3049a7c113e5be24a61187369580ab60e8b18d3e Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 9 Nov 2026 11:31:41 +0100 Subject: [PATCH 2/7] Split LoadAugmentsData into ReadAugment.* helper functions LoadAugmentsData() was a single ~500 line function handling all keys inline. One function per top-level key, mirroring cmdb.c, makes it readable and allows sharing code with cmdb.c in the following commits. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/augments.c | 919 ++++++++++++++++++++++------------------- 1 file changed, 494 insertions(+), 425 deletions(-) diff --git a/libpromises/augments.c b/libpromises/augments.c index 57258bc2776..36f6bf1dd0b 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -22,6 +22,29 @@ included file COSL.txt. */ +/* + * Loading of augments files (def.json). + * + * LoadAugments() is called by the agents after the CMDB (host-specific) + * data is loaded and before the policy is parsed. It loads + * def_preferred.json (unless --ignore-preferred-augments is given) or + * def.json from the policy input directory: + * + * LoadAugments() pick def_preferred.json or def.json + * LoadAugmentsFiles() expand the filename and parse the JSON file + * LoadAugmentsData() handle each top-level key of the JSON object + * ReadAugmentVars() "vars" + * ReadAugmentVariables() "variables" + * ReadAugmentClasses() "classes" + * ReadAugmentInputs() "inputs" + * ReadAugmentAugments() "augments", calls LoadAugmentsFiles() for + * each further file + * + * Variables and classes already defined from host-specific data are not + * overridden (see CanSetVariable() and CanSetClass()), except for classes + * whose expression is "true". + */ + #include #include @@ -189,514 +212,560 @@ static inline const char *GetAugmentsComment(const char *item_type, const char * return JsonPrimitiveGetAsString(json_comment); } -static bool LoadAugmentsData(EvalContext *ctx, const char *filename, const JsonElement* augment) +/** Loads the "vars" key of augments data into the context */ +static bool ReadAugmentVars(EvalContext *ctx, const char *filename, JsonElement *element) { - bool loaded = false; + JsonElement* vars = JsonExpandElement(ctx, element); - if (JsonGetElementType(augment) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(augment) != JSON_CONTAINER_TYPE_OBJECT) + if (vars == NULL || + JsonGetElementType(vars) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(vars) != JSON_CONTAINER_TYPE_OBJECT) { - Log(LOG_LEVEL_ERR, "Invalid augments file contents in '%s', must be a JSON object", filename); + Log(LOG_LEVEL_ERR, "Invalid augments vars in '%s', must be a JSON object", filename); + JsonDestroy(vars); + return false; } - else - { - loaded = true; - Log(LOG_LEVEL_VERBOSE, "Loaded augments file '%s', installing contents", filename); - JsonIterator iter = JsonIteratorInit(augment); - const char *key; - while ((key = JsonIteratorNextKey(&iter))) + JsonIterator iter = JsonIteratorInit(vars); + const char *vkey; + while ((vkey = JsonIteratorNextKey(&iter))) + { + VarRef *ref = VarRefParse(vkey); + if (ref->ns != NULL) { - if (!(StringEqual(key, "vars") || - StringEqual(key, "classes") || - StringEqual(key, "inputs") || - StringEqual(key, "augments") || - StringEqual(key, "variables"))) + if (ref->scope == NULL) { - Log(LOG_LEVEL_VERBOSE, "Unknown augments key '%s' in file '%s', skipping it", - key, filename); + Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" + " (bundle name has to be specified if namespace is specified)", filename, vkey); + VarRefDestroy(ref); + continue; } } - - /* load variables (if any) */ - JsonElement *element = JsonObjectGet(augment, "vars"); - if (JSON_NOT_NULL(element)) + if (ref->scope == NULL) { - JsonElement* vars = JsonExpandElement(ctx, element); + ref->scope = xstrdup("def"); + } - if (vars == NULL || - JsonGetElementType(vars) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(vars) != JSON_CONTAINER_TYPE_OBJECT) + JsonElement *data = JsonObjectGet(vars, vkey); + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *value = JsonPrimitiveToString(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) { - Log(LOG_LEVEL_ERR, "Invalid augments vars in '%s', must be a JSON object", filename); - goto vars_cleanup; + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, vkey, value, CF_DATA_TYPE_STRING, "source=augments_file"); + } } - - JsonIterator iter = JsonIteratorInit(vars); - const char *vkey; - while ((vkey = JsonIteratorNextKey(&iter))) + else { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", + vkey, value, filename); + if (CanSetVariable(ctx, ref)) { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } + EvalContextVariablePut(ctx, ref, value, CF_DATA_TYPE_STRING, "source=augments_file"); } - if (ref->scope == NULL) + } + free(value); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(data)) + { + // map to slist if the data only has primitives + Rlist *data_as_rlist = RlistFromContainer(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) { - ref->scope = xstrdup("def"); + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + vkey, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); } - - JsonElement *data = JsonObjectGet(vars, vkey); - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, vkey, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - free(value); + EvalContextVariablePut(ctx, ref, data_as_rlist, CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) - { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data_as_rlist, CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } + } - RlistDestroy(data_as_rlist); + RlistDestroy(data_as_rlist); + } + else // install as a data container + { + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) + { + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + vkey, data, + CF_DATA_TYPE_CONTAINER, + "source=augments_file"); } - else // install as a data container + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } + EvalContextVariablePut(ctx, ref, data, + CF_DATA_TYPE_CONTAINER, + "source=augments_file"); } - VarRefDestroy(ref); } + } + VarRefDestroy(ref); + } + + JsonDestroy(vars); + return true; +} - vars_cleanup: - JsonDestroy(vars); +/** Loads the "variables" key of augments data (uses the new format allowing metadata, CFE-3633) */ +static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonElement *element) +{ + JsonElement* variables = JsonExpandElement(ctx, element); + + if (variables == NULL || JsonGetType(variables) != JSON_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments variables in '%s', must be a JSON object", filename); + JsonDestroy(variables); + return false; + } + + JsonIterator variables_iter = JsonIteratorInit(variables); + const char *vkey; + while ((vkey = JsonIteratorNextKey(&variables_iter))) + { + VarRef *ref = VarRefParse(vkey); + if (ref->ns != NULL) + { + if (ref->scope == NULL) + { + Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" + " (bundle name has to be specified if namespace is specified)", filename, vkey); + VarRefDestroy(ref); + continue; + } } + if (ref->scope == NULL) + { + ref->scope = xstrdup("def"); + } + + const JsonElement *const var_info = JsonObjectGet(variables, vkey); - /* Uses the new format allowing metadata (CFE-3633) */ - element = JsonObjectGet(augment, "variables"); - if (JSON_NOT_NULL(element)) + const JsonElement *data; + StringSet *tags; + const char *comment = NULL; + + if (JsonGetType(var_info) == JSON_TYPE_OBJECT) { - JsonElement* variables = JsonExpandElement(ctx, element); + data = JsonObjectGet(var_info, AUGMENTS_VARIABLES_DATA); - if (variables == NULL || JsonGetType(variables) != JSON_TYPE_OBJECT) + if (NULL_JSON(data)) { - Log(LOG_LEVEL_ERR, "Invalid augments variables in '%s', must be a JSON object", filename); - goto variables_cleanup; + Log(LOG_LEVEL_ERR, "Missing value for the augments variable '%s' in '%s' (value field is required)", + vkey, filename); + VarRefDestroy(ref); + continue; } - JsonIterator variables_iter = JsonIteratorInit(variables); - const char *vkey; - while ((vkey = JsonIteratorNextKey(&variables_iter))) + const JsonElement *json_tags = JsonObjectGet(var_info, AUGMENTS_VARIABLES_TAGS); + tags = GetTagsFromAugmentsTags("variable", vkey, json_tags, "source=augments_file", filename); + comment = GetAugmentsComment("variable", vkey, filename, var_info); + } + else + { + // Just a bare value, like in "vars", no metadata + data = var_info; + tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + } + + assert(tags != NULL); + assert(data != NULL); + + bool installed = false; + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *value = JsonPrimitiveToString(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); + if (CanSetVariable(ctx, ref)) { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, vkey, value, + CF_DATA_TYPE_STRING, tags, comment); } - if (ref->scope == NULL) + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", + vkey, value, filename); + if (CanSetVariable(ctx, ref)) { - ref->scope = xstrdup("def"); + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING, + tags, comment); } - - const JsonElement *const var_info = JsonObjectGet(variables, vkey); - - const JsonElement *data; - StringSet *tags; - const char *comment = NULL; - - if (JsonGetType(var_info) == JSON_TYPE_OBJECT) + } + free(value); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives((JsonElement *) data)) + { + // map to slist if the data only has primitives + Rlist *data_as_rlist = RlistFromContainer(data); + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) { - data = JsonObjectGet(var_info, AUGMENTS_VARIABLES_DATA); - - if (NULL_JSON(data)) - { - Log(LOG_LEVEL_ERR, "Missing value for the augments variable '%s' in '%s' (value field is required)", - vkey, filename); - VarRefDestroy(ref); - continue; - } - - const JsonElement *json_tags = JsonObjectGet(var_info, AUGMENTS_VARIABLES_TAGS); - tags = GetTagsFromAugmentsTags("variable", vkey, json_tags, "source=augments_file", filename); - comment = GetAugmentsComment("variable", vkey, filename, var_info); + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, + vkey, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + tags, comment); } - else + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) { - // Just a bare value, like in "vars", no metadata - data = var_info; - tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data_as_rlist, + CF_DATA_TYPE_STRING_LIST, + tags, comment); } + } - assert(tags != NULL); - assert(data != NULL); - - bool installed = false; - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, vkey, value, - CF_DATA_TYPE_STRING, tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING, - tags, comment); - } - } - free(value); - } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives((JsonElement *) data)) + RlistDestroy(data_as_rlist); + } + else // install as a data container + { + if ((ref->ns == NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", + SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); + if (CanSetVariable(ctx, ref)) { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - - RlistDestroy(data_as_rlist); + installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, + vkey, data, + CF_DATA_TYPE_CONTAINER, + tags, comment); } - else // install as a data container - { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - } - VarRefDestroy(ref); - if (!installed) + } + else + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", + vkey, filename); + if (CanSetVariable(ctx, ref)) { - /* EvalContextVariablePutTagsSetWithComment() and - * EvalContextVariablePutSpecialTagsSetWithComment() take - * over tags in case of success. Otherwise we have to - * destroy the set. */ - StringSetDestroy(tags); + installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data, + CF_DATA_TYPE_CONTAINER, + tags, comment); } } - - variables_cleanup: - JsonDestroy(variables); } - - /* load classes (if any) */ - element = JsonObjectGet(augment, "classes"); - if (JSON_NOT_NULL(element)) + VarRefDestroy(ref); + if (!installed) { - JsonElement* classes = JsonExpandElement(ctx, element); + /* EvalContextVariablePutTagsSetWithComment() and + * EvalContextVariablePutSpecialTagsSetWithComment() take + * over tags in case of success. Otherwise we have to + * destroy the set. */ + StringSetDestroy(tags); + } + } + + JsonDestroy(variables); + return true; +} - if (JsonGetElementType(classes) != JSON_ELEMENT_TYPE_CONTAINER || - JsonGetContainerType(classes) != JSON_CONTAINER_TYPE_OBJECT) +/** Loads the "classes" key of augments data into the context */ +static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonElement *element) +{ + JsonElement* classes = JsonExpandElement(ctx, element); + + if (JsonGetElementType(classes) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(classes) != JSON_CONTAINER_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments classes in '%s', must be a JSON object", filename); + JsonDestroy(classes); + return false; + } + + const char default_tags[] = "source=augments_file"; + JsonIterator iter = JsonIteratorInit(classes); + const char *ckey; + while ((ckey = JsonIteratorNextKey(&iter))) + { + JsonElement *data = JsonObjectGet(classes, ckey); + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + char *check = JsonPrimitiveToString(data); + // check if class is true + if (CheckContextClassmatch(ctx, check)) + { + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) + { + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + } + } + else if (StringEqual(check, "true")) { - Log(LOG_LEVEL_ERR, "Invalid augments classes in '%s', must be a JSON object", filename); - goto classes_cleanup; + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); } - - const char default_tags[] = "source=augments_file"; - JsonIterator iter = JsonIteratorInit(classes); - const char *ckey; - while ((ckey = JsonIteratorNextKey(&iter))) + free(check); + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(data)) + { + // check if each class is true + JsonIterator data_iter = JsonIteratorInit(data); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&data_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) { - JsonElement *data = JsonObjectGet(classes, ckey); - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + char *check = JsonPrimitiveToString(el); + if (CheckContextClassmatch(ctx, check)) { - char *check = JsonPrimitiveToString(data); - // check if class is true - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - } - else if (StringEqual(check, "true")) + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) { EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); } free(check); + break; } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) + else if (StringEqual(check, "true")) { - // check if each class is true - JsonIterator data_iter = JsonIteratorInit(data); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&data_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) - { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - free(check); - break; - } - else if (StringEqual(check, "true")) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - - free(check); - } + EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); } - else if (JsonGetType(data) == JSON_TYPE_OBJECT) - { - const JsonElement *class_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_CLASS_EXPRESSIONS); - const JsonElement *reg_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_REGULAR_EXPRESSIONS); - const JsonElement *json_tags = JsonObjectGet(data, AUGMENTS_CLASSES_TAGS); - if ((JSON_NOT_NULL(class_exprs) && JSON_NOT_NULL(reg_exprs)) || - (NULL_JSON(class_exprs) && NULL_JSON(reg_exprs))) - { - Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s':" - " either \"class_expressions\" or \"regular_expressions\" need to be specified", - ckey, filename); - continue; - } - - StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, - "source=augments_file", filename); - const char *comment = GetAugmentsComment("class", ckey, filename, data); - bool installed = false; - JsonIterator exprs_iter = JsonIteratorInit(class_exprs ? class_exprs : reg_exprs); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&exprs_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) - { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - else if (StringEqual(check, "true")) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - free(check); - break; - } - - free(check); - } - if (!installed) - { - /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in - * case of success. Otherwise we have to destroy the set. */ - StringSetDestroy(tags); - } - } - else - { - Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s'", - ckey, filename); - } + free(check); } - - classes_cleanup: - JsonDestroy(classes); } - - /* load inputs (if any) */ - element = JsonObjectGet(augment, "inputs"); - if (JSON_NOT_NULL(element)) + else if (JsonGetType(data) == JSON_TYPE_OBJECT) { - JsonElement* inputs = JsonExpandElement(ctx, element); + const JsonElement *class_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_CLASS_EXPRESSIONS); + const JsonElement *reg_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_REGULAR_EXPRESSIONS); + const JsonElement *json_tags = JsonObjectGet(data, AUGMENTS_CLASSES_TAGS); - if (JsonGetElementType(inputs) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(inputs) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(inputs)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments def.augments_inputs from file '%s'", - filename); - Rlist *rlist = RlistFromContainer(inputs); - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - "augments_inputs", rlist, - CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - RlistDestroy(rlist); - } - else + if ((JSON_NOT_NULL(class_exprs) && JSON_NOT_NULL(reg_exprs)) || + (NULL_JSON(class_exprs) && NULL_JSON(reg_exprs))) { - Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", - filename); + Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s':" + " either \"class_expressions\" or \"regular_expressions\" need to be specified", + ckey, filename); + continue; } - JsonDestroy(inputs); - } - - /* load further def.json files (if any) */ - element = JsonObjectGet(augment, "augments"); - if (JSON_NOT_NULL(element)) - { - JsonElement* further_augments = element; - assert(further_augments != NULL); - - if (JsonGetElementType(further_augments) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(further_augments) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(further_augments)) + StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, + "source=augments_file", filename); + const char *comment = GetAugmentsComment("class", ckey, filename, data); + bool installed = false; + JsonIterator exprs_iter = JsonIteratorInit(class_exprs ? class_exprs : reg_exprs); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&exprs_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) { - JsonIterator iter = JsonIteratorInit(further_augments); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true)) != NULL) + char *check = JsonPrimitiveToString(el); + if (CheckContextClassmatch(ctx, check)) { - char *nested_filename = JsonPrimitiveToString(el); - bool further_loaded = LoadAugmentsFiles(ctx, nested_filename); - if (further_loaded) + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + if (CanSetClass(ctx, ckey)) { - Log(LOG_LEVEL_VERBOSE, "Completed augmenting from file '%s'", nested_filename); + installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, + tags, comment); } - else + else if (StringEqual(check, "true")) { - Log(LOG_LEVEL_ERR, "Could not load requested further augments from file '%s'", nested_filename); + installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, + tags, comment); } - free(nested_filename); + free(check); + break; } + + free(check); } - else + if (!installed) { - Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", - filename); + /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in + * case of success. Otherwise we have to destroy the set. */ + StringSetDestroy(tags); } } + else + { + Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s'", + ckey, filename); + } } - return loaded; + JsonDestroy(classes); + return true; +} + +/** Loads the "inputs" key of augments data into def.augments_inputs */ +static bool ReadAugmentInputs(EvalContext *ctx, const char *filename, JsonElement *element) +{ + JsonElement* inputs = JsonExpandElement(ctx, element); + + if (JsonGetElementType(inputs) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(inputs) != JSON_CONTAINER_TYPE_ARRAY || + !JsonArrayContainsOnlyPrimitives(inputs)) + { + Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", + filename); + JsonDestroy(inputs); + return false; + } + + Log(LOG_LEVEL_VERBOSE, "Installing augments def.augments_inputs from file '%s'", + filename); + Rlist *rlist = RlistFromContainer(inputs); + EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, + "augments_inputs", rlist, + CF_DATA_TYPE_STRING_LIST, + "source=augments_file"); + RlistDestroy(rlist); + + JsonDestroy(inputs); + return true; +} + +/** Loads further augments files listed in the "augments" key */ +static bool ReadAugmentAugments(EvalContext *ctx, const char *filename, JsonElement *further_augments) +{ + assert(further_augments != NULL); + + if (JsonGetElementType(further_augments) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(further_augments) != JSON_CONTAINER_TYPE_ARRAY || + !JsonArrayContainsOnlyPrimitives(further_augments)) + { + Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", + filename); + return false; + } + + JsonIterator iter = JsonIteratorInit(further_augments); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true)) != NULL) + { + char *nested_filename = JsonPrimitiveToString(el); + bool further_loaded = LoadAugmentsFiles(ctx, nested_filename); + if (further_loaded) + { + Log(LOG_LEVEL_VERBOSE, "Completed augmenting from file '%s'", nested_filename); + } + else + { + Log(LOG_LEVEL_ERR, "Could not load requested further augments from file '%s'", nested_filename); + } + free(nested_filename); + } + + return true; +} + +/** + * Installs the contents of one augments file into the evaluation context. + * + * The data is a JSON object and each supported top-level key is handled by its + * own ReadAugment*() function, in this order (the same structure as + * LoadCMDBData() in cmdb.c): + * + * "vars" ReadAugmentVars() variables (old format) + * "variables" ReadAugmentVariables() variables with tags and comments + * "classes" ReadAugmentClasses() classes + * "inputs" ReadAugmentInputs() def.augments_inputs + * "augments" ReadAugmentAugments() further augments files to load + * + * Unknown keys are logged and skipped. An invalid key does not stop the + * processing of the other keys. + * + * @return %false if the data is not a JSON object or any key is invalid + */ +static bool LoadAugmentsData(EvalContext *ctx, const char *filename, const JsonElement* augment) +{ + if (JsonGetElementType(augment) != JSON_ELEMENT_TYPE_CONTAINER || + JsonGetContainerType(augment) != JSON_CONTAINER_TYPE_OBJECT) + { + Log(LOG_LEVEL_ERR, "Invalid augments file contents in '%s', must be a JSON object", filename); + return false; + } + + Log(LOG_LEVEL_VERBOSE, "Loaded augments file '%s', installing contents", filename); + + JsonIterator iter = JsonIteratorInit(augment); + const char *key; + while ((key = JsonIteratorNextKey(&iter))) + { + if (!(StringEqual(key, "vars") || + StringEqual(key, "classes") || + StringEqual(key, "inputs") || + StringEqual(key, "augments") || + StringEqual(key, "variables"))) + { + Log(LOG_LEVEL_VERBOSE, "Unknown augments key '%s' in file '%s', skipping it", + key, filename); + } + } + + bool success = true; + JsonElement *vars = JsonObjectGet(augment, "vars"); + if (JSON_NOT_NULL(vars) && !ReadAugmentVars(ctx, filename, vars)) + { + success = false; + } + + /* Uses the new format allowing metadata (CFE-3633) */ + JsonElement *variables = JsonObjectGet(augment, "variables"); + if (JSON_NOT_NULL(variables) && !ReadAugmentVariables(ctx, filename, variables)) + { + success = false; + } + + JsonElement *classes = JsonObjectGet(augment, "classes"); + if (JSON_NOT_NULL(classes) && !ReadAugmentClasses(ctx, filename, classes)) + { + success = false; + } + + JsonElement *inputs = JsonObjectGet(augment, "inputs"); + if (JSON_NOT_NULL(inputs) && !ReadAugmentInputs(ctx, filename, inputs)) + { + success = false; + } + + /* load further def.json files (if any) */ + JsonElement *augments = JsonObjectGet(augment, "augments"); + if (JSON_NOT_NULL(augments) && !ReadAugmentAugments(ctx, filename, augments)) + { + success = false; + } + + return success; } static bool LoadAugmentsFiles(EvalContext *ctx, const char *unexpanded_filename) From df00d6fac998ad2ac5a1ccb2f0272f60ca3f1e1d Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 9 Nov 2026 12:48:36 +0100 Subject: [PATCH 3/7] Split ReadAugment.* functions into helper functions Removes duplicated code for installing variables and classes. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/augments.c | 427 ++++++++++++++++------------------------- 1 file changed, 168 insertions(+), 259 deletions(-) diff --git a/libpromises/augments.c b/libpromises/augments.c index 36f6bf1dd0b..e370e9a3d3f 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -212,6 +212,143 @@ static inline const char *GetAugmentsComment(const char *item_type, const char * return JsonPrimitiveGetAsString(json_comment); } +/** + * Parses the name of a variable from augments data. Variables without a bundle + * name go into the 'def' bundle. + * + * @return The variable reference or %NULL (after logging an error) if the + * specification is invalid. + */ +static VarRef *GetAugmentsVariableRef(const char *key, const char *filename) +{ + VarRef *ref = VarRefParse(key); + if ((ref->ns != NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" + " (bundle name has to be specified if namespace is specified)", filename, key); + VarRefDestroy(ref); + return NULL; + } + if (ref->scope == NULL) + { + ref->scope = xstrdup("def"); + } + return ref; +} + +/** + * Installs a variable from augments data as a string, an slist (if @data is an + * array of primitives) or a data container. + * + * @note Takes over @tags. The set is destroyed if the variable is not installed. + */ +static bool AddAugmentsVariable(EvalContext *ctx, const char *key, VarRef *ref, + const JsonElement *data, StringSet *tags, + const char *comment, const char *filename) +{ + assert(ctx != NULL); + assert(key != NULL); + assert(ref != NULL); + assert(data != NULL); + assert(tags != NULL); + + char *value_str = NULL; + Rlist *value_rlist = NULL; + const void *value; + DataType type; + + if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) + { + value_str = JsonPrimitiveToString(data); + Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", + key, value_str, filename); + value = value_str; + type = CF_DATA_TYPE_STRING; + } + else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives((JsonElement *) data)) + { + // map to slist if the data only has primitives + value_rlist = RlistFromContainer(data); + Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", + key, filename); + value = value_rlist; + type = CF_DATA_TYPE_STRING_LIST; + } + else // install as a data container + { + Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", + key, filename); + value = data; + type = CF_DATA_TYPE_CONTAINER; + } + + const bool installed = (CanSetVariable(ctx, ref) && + EvalContextVariablePutTagsSetWithComment(ctx, ref, value, type, + tags, comment)); + free(value_str); + RlistDestroy(value_rlist); + + if (!installed) + { + /* EvalContextVariablePutTagsSetWithComment() takes over tags in case + * of success. Otherwise we have to destroy the set. */ + StringSetDestroy(tags); + } + return installed; +} + +/** + * Installs a class from augments data because @matched_expr matched. + * + * Unless @matched_expr is "true", a class already defined from host-specific + * data is not overridden. + * + * @note Takes over @tags. The set is destroyed if the class is not installed. + */ +static bool AddAugmentsClass(EvalContext *ctx, const char *key, const char *matched_expr, + StringSet *tags, const char *comment) +{ + assert(ctx != NULL); + assert(key != NULL); + assert(matched_expr != NULL); + assert(tags != NULL); + + const bool installed = ((StringEqual(matched_expr, "true") || CanSetClass(ctx, key)) && + EvalContextClassPutSoftTagsSetWithComment(ctx, key, CONTEXT_SCOPE_NAMESPACE, + tags, comment)); + if (!installed) + { + /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in case + * of success. Otherwise we have to destroy the set. */ + StringSetDestroy(tags); + } + return installed; +} + +/** + * Finds the first primitive in the @exprs array that matches + * + * @return The matching expression (to be freed by the caller) or %NULL if + * none matches. + */ +static char *GetFirstMatchingClassExpression(EvalContext *ctx, const JsonElement *exprs) +{ + JsonIterator iter = JsonIteratorInit(exprs); + const JsonElement *el; + while ((el = JsonIteratorNextValueByType(&iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) + { + char *check = JsonPrimitiveToString(el); + if ((StringEqual(check, "true") || CheckContextClassmatch(ctx, check))) + { + return check; + } + free(check); + } + return NULL; +} + /** Loads the "vars" key of augments data into the context */ static bool ReadAugmentVars(EvalContext *ctx, const char *filename, JsonElement *element) { @@ -230,103 +367,15 @@ static bool ReadAugmentVars(EvalContext *ctx, const char *filename, JsonElement const char *vkey; while ((vkey = JsonIteratorNextKey(&iter))) { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) + VarRef *ref = GetAugmentsVariableRef(vkey, filename); + if (ref == NULL) { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } - } - if (ref->scope == NULL) - { - ref->scope = xstrdup("def"); - } - - JsonElement *data = JsonObjectGet(vars, vkey); - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, vkey, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, value, CF_DATA_TYPE_STRING, "source=augments_file"); - } - } - free(value); + continue; } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) - { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data_as_rlist, CF_DATA_TYPE_STRING_LIST, - "source=augments_file"); - } - } - RlistDestroy(data_as_rlist); - } - else // install as a data container - { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePutSpecial(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - EvalContextVariablePut(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - "source=augments_file"); - } - } - } + const JsonElement *data = JsonObjectGet(vars, vkey); + StringSet *tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + AddAugmentsVariable(ctx, vkey, ref, data, tags, NULL, filename); VarRefDestroy(ref); } @@ -350,20 +399,10 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle const char *vkey; while ((vkey = JsonIteratorNextKey(&variables_iter))) { - VarRef *ref = VarRefParse(vkey); - if (ref->ns != NULL) + VarRef *ref = GetAugmentsVariableRef(vkey, filename); + if (ref == NULL) { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, vkey); - VarRefDestroy(ref); - continue; - } - } - if (ref->scope == NULL) - { - ref->scope = xstrdup("def"); + continue; } const JsonElement *const var_info = JsonObjectGet(variables, vkey); @@ -398,99 +437,8 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle assert(tags != NULL); assert(data != NULL); - bool installed = false; - if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_PRIMITIVE) - { - char *value = JsonPrimitiveToString(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s.%s=%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, vkey, value, - CF_DATA_TYPE_STRING, tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments variable '%s=%s' from file '%s'", - vkey, value, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, value, CF_DATA_TYPE_STRING, - tags, comment); - } - } - free(value); - } - else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives((JsonElement *) data)) - { - // map to slist if the data only has primitives - Rlist *data_as_rlist = RlistFromContainer(data); - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments slist variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data_as_rlist, - CF_DATA_TYPE_STRING_LIST, - tags, comment); - } - } - - RlistDestroy(data_as_rlist); - } - else // install as a data container - { - if ((ref->ns == NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s.%s' from file '%s'", - SpecialScopeToString(SPECIAL_SCOPE_DEF), vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutSpecialTagsSetWithComment(ctx, SPECIAL_SCOPE_DEF, - vkey, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - else - { - Log(LOG_LEVEL_VERBOSE, "Installing augments data container variable '%s' from file '%s'", - vkey, filename); - if (CanSetVariable(ctx, ref)) - { - installed = EvalContextVariablePutTagsSetWithComment(ctx, ref, data, - CF_DATA_TYPE_CONTAINER, - tags, comment); - } - } - } + AddAugmentsVariable(ctx, vkey, ref, data, tags, comment, filename); VarRefDestroy(ref); - if (!installed) - { - /* EvalContextVariablePutTagsSetWithComment() and - * EvalContextVariablePutSpecialTagsSetWithComment() take - * over tags in case of success. Otherwise we have to - * destroy the set. */ - StringSetDestroy(tags); - } } JsonDestroy(variables); @@ -510,7 +458,6 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme return false; } - const char default_tags[] = "source=augments_file"; JsonIterator iter = JsonIteratorInit(classes); const char *ckey; while ((ckey = JsonIteratorNextKey(&iter))) @@ -520,47 +467,29 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme { char *check = JsonPrimitiveToString(data); // check if class is true - if (CheckContextClassmatch(ctx, check)) + if ((StringEqual(check, "true") || CheckContextClassmatch(ctx, check))) { Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - } - else if (StringEqual(check, "true")) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); + StringSet *tags = GetTagsFromAugmentsTags("class", ckey, NULL, + "source=augments_file", filename); + AddAugmentsClass(ctx, ckey, check, tags, NULL); } free(check); } else if (JsonGetElementType(data) == JSON_ELEMENT_TYPE_CONTAINER && - JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && - JsonArrayContainsOnlyPrimitives(data)) + JsonGetContainerType(data) == JSON_CONTAINER_TYPE_ARRAY && + JsonArrayContainsOnlyPrimitives(data)) { - // check if each class is true - JsonIterator data_iter = JsonIteratorInit(data); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&data_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) + // check if any of the expressions is true + char *check = GetFirstMatchingClassExpression(ctx, data); + if (check != NULL) { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - free(check); - break; - } - else if (StringEqual(check, "true")) - { - EvalContextClassPutSoft(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, default_tags); - } - + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + StringSet *tags = GetTagsFromAugmentsTags("class", ckey, NULL, + "source=augments_file", filename); + AddAugmentsClass(ctx, ckey, check, tags, NULL); free(check); } } @@ -580,38 +509,18 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme } StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, - "source=augments_file", filename); + "source=augments_file", filename); const char *comment = GetAugmentsComment("class", ckey, filename, data); - bool installed = false; - JsonIterator exprs_iter = JsonIteratorInit(class_exprs ? class_exprs : reg_exprs); - const JsonElement *el; - while ((el = JsonIteratorNextValueByType(&exprs_iter, JSON_ELEMENT_TYPE_PRIMITIVE, true))) + char *check = GetFirstMatchingClassExpression(ctx, class_exprs ? class_exprs : reg_exprs); + if (check != NULL) { - char *check = JsonPrimitiveToString(el); - if (CheckContextClassmatch(ctx, check)) - { - Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", - ckey, check, filename); - if (CanSetClass(ctx, ckey)) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - else if (StringEqual(check, "true")) - { - installed = EvalContextClassPutSoftTagsSetWithComment(ctx, ckey, CONTEXT_SCOPE_NAMESPACE, - tags, comment); - } - free(check); - break; - } - + Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", + ckey, check, filename); + AddAugmentsClass(ctx, ckey, check, tags, comment); free(check); } - if (!installed) + else { - /* EvalContextClassPutSoftTagsSetWithComment() takes over tags in - * case of success. Otherwise we have to destroy the set. */ StringSetDestroy(tags); } } From d163bfa645cfd77966d18b04249ce1cab7ed386c Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Mon, 9 Nov 2026 13:16:43 +0100 Subject: [PATCH 4/7] Moved shared code between augments.c and cmdb.c into host_data.c Both files had near-identical copies of these helpers. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/Makefile.am | 1 + libpromises/augments.c | 139 ++++++++--------------------------- libpromises/cmdb.c | 138 ++++------------------------------- libpromises/cmdb.h | 4 +- libpromises/generic_agent.c | 1 + libpromises/host_data.c | 140 ++++++++++++++++++++++++++++++++++++ libpromises/host_data.h | 83 +++++++++++++++++++++ 7 files changed, 270 insertions(+), 236 deletions(-) create mode 100644 libpromises/host_data.c create mode 100644 libpromises/host_data.h diff --git a/libpromises/Makefile.am b/libpromises/Makefile.am index 71b88fd9b40..2f36172cd16 100644 --- a/libpromises/Makefile.am +++ b/libpromises/Makefile.am @@ -107,6 +107,7 @@ libpromises_la_SOURCES = \ generic_agent.c generic_agent.h \ global_mutex.c global_mutex.h \ granules.c granules.h \ + host_data.c host_data.h \ instrumentation.c instrumentation.h \ item_lib.c item_lib.h \ iteration.c iteration.h \ diff --git a/libpromises/augments.c b/libpromises/augments.c index e370e9a3d3f..1ac1eff2d8f 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -58,14 +58,8 @@ #include /* VarRef, VarRefParse() */ #include /* ExpandScalar() */ #include /* RlistFromContainer() */ -#include /* ReadJsonFile(), CMDB_SOURCE_TAG */ - -#define AUGMENTS_VARIABLES_TAGS "tags" -#define AUGMENTS_VARIABLES_DATA "value" -#define AUGMENTS_CLASSES_TAGS "tags" -#define AUGMENTS_CLASSES_CLASS_EXPRESSIONS "class_expressions" -#define AUGMENTS_CLASSES_REGULAR_EXPRESSIONS "regular_expressions" -#define AUGMENTS_COMMENT_KEY "comment" +#include /* CMDB_SOURCE_TAG */ +#include /* ReadJsonFile(), JSON data helpers */ static bool LoadAugmentsFiles(EvalContext *ctx, const char *filename); @@ -104,44 +98,6 @@ static bool CheckContextClassmatch(EvalContext *ctx, const char *class_str) return found; } -static StringSet *GetTagsFromAugmentsTags(const char *item_type, - const char *key, - const JsonElement *json_tags, - const char *default_tag, - const char *filename) -{ - StringSet *tags = NULL; - if (JSON_NOT_NULL(json_tags)) - { - if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) || - (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags))) - { - Log(LOG_LEVEL_ERR, - "Invalid tags information for %s '%s' in augments file '%s':" - " must be a JSON array of strings", - item_type, key, filename); - } - else - { - tags = JsonArrayToStringSet(json_tags); - if (tags == NULL) - { - Log(LOG_LEVEL_ERR, - "Invalid meta information %s '%s' in augments file '%s':" - " must be a JSON array of strings", - item_type, key, filename); - } - } - } - if (tags == NULL) - { - tags = StringSetNew(); - } - StringSetAdd(tags, xstrdup(default_tag)); - - return tags; -} - static inline bool CanSetVariable(const EvalContext *ctx, VarRef *var_ref) { assert(var_ref != NULL); @@ -190,52 +146,6 @@ static inline bool CanSetClass(const EvalContext *ctx, const char *class_spec) return can_set; } -static inline const char *GetAugmentsComment(const char *item_type, const char *identifier, - const char *file_name, const JsonElement *json_object) -{ - assert(JsonGetType(json_object) == JSON_TYPE_OBJECT); - - JsonElement *json_comment = JsonObjectGet(json_object, AUGMENTS_COMMENT_KEY); - if (NULL_JSON(json_comment)) - { - return NULL; - } - - if (JsonGetType(json_comment) != JSON_TYPE_STRING) - { - Log(LOG_LEVEL_ERR, - "Invalid type of the 'comment' field for the '%s' %s in augments data in '%s', must be a string", - identifier, item_type, file_name); - return NULL; - } - - return JsonPrimitiveGetAsString(json_comment); -} - -/** - * Parses the name of a variable from augments data. Variables without a bundle - * name go into the 'def' bundle. - * - * @return The variable reference or %NULL (after logging an error) if the - * specification is invalid. - */ -static VarRef *GetAugmentsVariableRef(const char *key, const char *filename) -{ - VarRef *ref = VarRefParse(key); - if ((ref->ns != NULL) && (ref->scope == NULL)) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in augments data in '%s': '%s'" - " (bundle name has to be specified if namespace is specified)", filename, key); - VarRefDestroy(ref); - return NULL; - } - if (ref->scope == NULL) - { - ref->scope = xstrdup("def"); - } - return ref; -} - /** * Installs a variable from augments data as a string, an slist (if @data is an * array of primitives) or a data container. @@ -363,22 +273,25 @@ static bool ReadAugmentVars(EvalContext *ctx, const char *filename, JsonElement return false; } + char *source = StringFormat("augments data in '%s'", filename); + JsonIterator iter = JsonIteratorInit(vars); const char *vkey; while ((vkey = JsonIteratorNextKey(&iter))) { - VarRef *ref = GetAugmentsVariableRef(vkey, filename); + VarRef *ref = GetVariableRefFromDataKey(vkey, NULL, "def", source); if (ref == NULL) { continue; } const JsonElement *data = JsonObjectGet(vars, vkey); - StringSet *tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + StringSet *tags = GetTagsFromJsonTags("variable", vkey, NULL, "source=augments_file", source); AddAugmentsVariable(ctx, vkey, ref, data, tags, NULL, filename); VarRefDestroy(ref); } + free(source); JsonDestroy(vars); return true; } @@ -395,11 +308,13 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle return false; } + char *source = StringFormat("augments data in '%s'", filename); + JsonIterator variables_iter = JsonIteratorInit(variables); const char *vkey; while ((vkey = JsonIteratorNextKey(&variables_iter))) { - VarRef *ref = GetAugmentsVariableRef(vkey, filename); + VarRef *ref = GetVariableRefFromDataKey(vkey, NULL, "def", source); if (ref == NULL) { continue; @@ -413,7 +328,7 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle if (JsonGetType(var_info) == JSON_TYPE_OBJECT) { - data = JsonObjectGet(var_info, AUGMENTS_VARIABLES_DATA); + data = JsonObjectGet(var_info, JSON_DATA_KEY_VALUE); if (NULL_JSON(data)) { @@ -423,15 +338,15 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle continue; } - const JsonElement *json_tags = JsonObjectGet(var_info, AUGMENTS_VARIABLES_TAGS); - tags = GetTagsFromAugmentsTags("variable", vkey, json_tags, "source=augments_file", filename); - comment = GetAugmentsComment("variable", vkey, filename, var_info); + const JsonElement *json_tags = JsonObjectGet(var_info, JSON_DATA_KEY_TAGS); + tags = GetTagsFromJsonTags("variable", vkey, json_tags, "source=augments_file", source); + comment = GetCommentFromJsonData("variable", vkey, var_info, source); } else { // Just a bare value, like in "vars", no metadata data = var_info; - tags = GetTagsFromAugmentsTags("variable", vkey, NULL, "source=augments_file", filename); + tags = GetTagsFromJsonTags("variable", vkey, NULL, "source=augments_file", source); } assert(tags != NULL); @@ -441,6 +356,7 @@ static bool ReadAugmentVariables(EvalContext *ctx, const char *filename, JsonEle VarRefDestroy(ref); } + free(source); JsonDestroy(variables); return true; } @@ -458,6 +374,8 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme return false; } + char *source = StringFormat("augments data in '%s'", filename); + JsonIterator iter = JsonIteratorInit(classes); const char *ckey; while ((ckey = JsonIteratorNextKey(&iter))) @@ -471,8 +389,8 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme { Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked '%s') from file '%s'", ckey, check, filename); - StringSet *tags = GetTagsFromAugmentsTags("class", ckey, NULL, - "source=augments_file", filename); + StringSet *tags = GetTagsFromJsonTags("class", ckey, NULL, + "source=augments_file", source); AddAugmentsClass(ctx, ckey, check, tags, NULL); } free(check); @@ -487,17 +405,17 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme { Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", ckey, check, filename); - StringSet *tags = GetTagsFromAugmentsTags("class", ckey, NULL, - "source=augments_file", filename); + StringSet *tags = GetTagsFromJsonTags("class", ckey, NULL, + "source=augments_file", source); AddAugmentsClass(ctx, ckey, check, tags, NULL); free(check); } } else if (JsonGetType(data) == JSON_TYPE_OBJECT) { - const JsonElement *class_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_CLASS_EXPRESSIONS); - const JsonElement *reg_exprs = JsonObjectGet(data, AUGMENTS_CLASSES_REGULAR_EXPRESSIONS); - const JsonElement *json_tags = JsonObjectGet(data, AUGMENTS_CLASSES_TAGS); + const JsonElement *class_exprs = JsonObjectGet(data, JSON_DATA_KEY_CLASS_EXPRESSIONS); + const JsonElement *reg_exprs = JsonObjectGet(data, JSON_DATA_KEY_REGULAR_EXPRESSIONS); + const JsonElement *json_tags = JsonObjectGet(data, JSON_DATA_KEY_TAGS); if ((JSON_NOT_NULL(class_exprs) && JSON_NOT_NULL(reg_exprs)) || (NULL_JSON(class_exprs) && NULL_JSON(reg_exprs))) @@ -508,9 +426,9 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme continue; } - StringSet *tags = GetTagsFromAugmentsTags("class", ckey, json_tags, - "source=augments_file", filename); - const char *comment = GetAugmentsComment("class", ckey, filename, data); + StringSet *tags = GetTagsFromJsonTags("class", ckey, json_tags, + "source=augments_file", source); + const char *comment = GetCommentFromJsonData("class", ckey, data, source); char *check = GetFirstMatchingClassExpression(ctx, class_exprs ? class_exprs : reg_exprs); if (check != NULL) { @@ -531,6 +449,7 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme } } + free(source); JsonDestroy(classes); return true; } diff --git a/libpromises/cmdb.c b/libpromises/cmdb.c index dd25fdc978d..79e4dd493ec 100644 --- a/libpromises/cmdb.c +++ b/libpromises/cmdb.c @@ -32,40 +32,16 @@ #include /* EvalContext*() */ #include /* JoinPaths() */ +#include #include #define HOST_SPECIFIC_DATA_FILE "host_specific.json" #define HOST_SPECIFIC_DATA_MAX_SIZE (5 * 1024 * 1024) /* maximum size of the host-specific.json file */ #define CMDB_NAMESPACE "data" -#define CMDB_VARIABLES_TAGS "tags" -#define CMDB_VARIABLES_DATA "value" -#define CMDB_CLASSES_TAGS "tags" -#define CMDB_CLASSES_CLASS_EXPRESSIONS "class_expressions" -#define CMDB_CLASSES_REGULAR_EXPRESSIONS "regular_expressions" -#define CMDB_COMMENT_KEY "comment" - -JsonElement *ReadJsonFile(const char *filename, LogLevel log_level, size_t size_max) -{ - assert(filename != NULL); - - JsonElement *doc = NULL; - JsonParseError err = JsonParseFile(filename, size_max, &doc); - - if (err == JSON_PARSE_ERROR_NO_SUCH_FILE) - { - Log(log_level, "Could not open JSON file %s", filename); - return NULL; - } - - if (err != JSON_PARSE_OK || - doc == NULL) - { - Log(log_level, "Could not parse JSON file %s: %s", filename, JsonParseErrorToString(err)); - } - return doc; -} +/* Description of the data used in log messages */ +#define CMDB_DATA_DESCRIPTION "CMDB data" static bool CheckPrimitiveForUnexpandedVars(JsonElement *primitive, ARG_UNUSED void *data) { @@ -93,31 +69,6 @@ static bool CheckObjectForUnexpandedVars(JsonElement *object, ARG_UNUSED void *d return true; } -static VarRef *GetCMDBVariableRef(const char *key) -{ - VarRef *ref = VarRefParse(key); - if (ref->ns == NULL) - { - ref->ns = xstrdup(CMDB_NAMESPACE); - } - else - { - if (ref->scope == NULL) - { - Log(LOG_LEVEL_ERR, "Invalid variable specification in CMDB data: '%s'" - " (bundle name has to be specified if namespace is specified)", key); - VarRefDestroy(ref); - return NULL; - } - } - - if (ref->scope == NULL) - { - ref->scope = xstrdup("variables"); - } - return ref; -} - static bool AddCMDBVariable(EvalContext *ctx, const char *key, const VarRef *ref, JsonElement *data, StringSet *tags, const char *comment) { @@ -189,7 +140,7 @@ static bool ReadCMDBVars(EvalContext *ctx, JsonElement *vars) const char *key = JsonIteratorNextKey(&iter); JsonElement *data = JsonObjectGet(vars, key); - VarRef *ref = GetCMDBVariableRef(key); + VarRef *ref = GetVariableRefFromDataKey(key, CMDB_NAMESPACE, "variables", CMDB_DATA_DESCRIPTION); if (ref == NULL) { continue; @@ -208,65 +159,6 @@ static bool ReadCMDBVars(EvalContext *ctx, JsonElement *vars) return true; } -static StringSet *GetTagsFromJsonTags(const char *item_type, - const char *key, - const JsonElement *json_tags, - const char *default_tag) -{ - StringSet *tags = NULL; - if (JSON_NOT_NULL(json_tags)) - { - if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) || - (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags))) - { - Log(LOG_LEVEL_ERR, - "Invalid json_tags information for %s '%s' in CMDB data:" - " must be a JSON array of strings", - item_type, key); - } - else - { - tags = JsonArrayToStringSet(json_tags); - if (tags == NULL) - { - Log(LOG_LEVEL_ERR, - "Invalid json_tags information %s '%s' in CMDB data:" - " must be a JSON array of strings", - item_type, key); - } - } - } - if (tags == NULL) - { - tags = StringSetNew(); - } - StringSetAdd(tags, xstrdup(default_tag)); - - return tags; -} - -static inline const char *GetCMDBComment(const char *item_type, const char *identifier, - const JsonElement *json_object) -{ - assert(JsonGetType(json_object) == JSON_TYPE_OBJECT); - - JsonElement *json_comment = JsonObjectGet(json_object, CMDB_COMMENT_KEY); - if (NULL_JSON(json_comment)) - { - return NULL; - } - - if (JsonGetType(json_comment) != JSON_TYPE_STRING) - { - Log(LOG_LEVEL_ERR, - "Invalid type of the 'comment' field for the '%s' %s in CMDB data, must be a string", - identifier, item_type); - return NULL; - } - - return JsonPrimitiveGetAsString(json_comment); -} - /** Uses the new format allowing metadata (CFE-3633) */ static bool ReadCMDBVariables(EvalContext *ctx, JsonElement *variables) { @@ -289,7 +181,7 @@ static bool ReadCMDBVariables(EvalContext *ctx, JsonElement *variables) { const char *key = JsonIteratorNextKey(&iter); - VarRef *ref = GetCMDBVariableRef(key); + VarRef *ref = GetVariableRefFromDataKey(key, CMDB_NAMESPACE, "variables", CMDB_DATA_DESCRIPTION); if (ref == NULL) { continue; @@ -303,7 +195,7 @@ static bool ReadCMDBVariables(EvalContext *ctx, JsonElement *variables) if (JsonGetType(var_info) == JSON_TYPE_OBJECT) { - data = JsonObjectGet(var_info, CMDB_VARIABLES_DATA); + data = JsonObjectGet(var_info, JSON_DATA_KEY_VALUE); if (data == NULL) { @@ -312,15 +204,15 @@ static bool ReadCMDBVariables(EvalContext *ctx, JsonElement *variables) continue; } - JsonElement *json_tags = JsonObjectGet(var_info, CMDB_VARIABLES_TAGS); - tags = GetTagsFromJsonTags("variable", key, json_tags, CMDB_SOURCE_TAG); - comment = GetCMDBComment("variable", key, var_info); + JsonElement *json_tags = JsonObjectGet(var_info, JSON_DATA_KEY_TAGS); + tags = GetTagsFromJsonTags("variable", key, json_tags, CMDB_SOURCE_TAG, CMDB_DATA_DESCRIPTION); + comment = GetCommentFromJsonData("variable", key, var_info, CMDB_DATA_DESCRIPTION); } else { // Just a bare value, like in "vars", no metadata data = var_info; - tags = GetTagsFromJsonTags("variable", key, NULL, CMDB_SOURCE_TAG); + tags = GetTagsFromJsonTags("variable", key, NULL, CMDB_SOURCE_TAG, CMDB_DATA_DESCRIPTION); } assert(tags != NULL); @@ -456,9 +348,9 @@ static bool ReadCMDBClasses(EvalContext *ctx, JsonElement *classes) } else if (JsonGetContainerType(data) == JSON_CONTAINER_TYPE_OBJECT) { - const JsonElement *class_exprs = JsonObjectGet(data, CMDB_CLASSES_CLASS_EXPRESSIONS); - const JsonElement *reg_exprs = JsonObjectGet(data, CMDB_CLASSES_REGULAR_EXPRESSIONS); - const JsonElement *json_tags = JsonObjectGet(data, CMDB_CLASSES_TAGS); + const JsonElement *class_exprs = JsonObjectGet(data, JSON_DATA_KEY_CLASS_EXPRESSIONS); + const JsonElement *reg_exprs = JsonObjectGet(data, JSON_DATA_KEY_REGULAR_EXPRESSIONS); + const JsonElement *json_tags = JsonObjectGet(data, JSON_DATA_KEY_TAGS); if (JSON_NOT_NULL(class_exprs) && (JsonGetType(class_exprs) != JSON_TYPE_ARRAY || @@ -483,8 +375,8 @@ static bool ReadCMDBClasses(EvalContext *ctx, JsonElement *classes) continue; } - StringSet *tags = GetTagsFromJsonTags("class", key, json_tags, CMDB_SOURCE_TAG); - const char *comment = GetCMDBComment("class", key, data); + StringSet *tags = GetTagsFromJsonTags("class", key, json_tags, CMDB_SOURCE_TAG, CMDB_DATA_DESCRIPTION); + const char *comment = GetCommentFromJsonData("class", key, data, CMDB_DATA_DESCRIPTION); bool ret = AddCMDBClass(ctx, key, tags, comment); if (!ret) { diff --git a/libpromises/cmdb.h b/libpromises/cmdb.h index e9e34be78ff..01bd07ce9e0 100644 --- a/libpromises/cmdb.h +++ b/libpromises/cmdb.h @@ -25,12 +25,10 @@ #ifndef CFENGINE_CMDB_H #define CFENGINE_CMDB_H -#include -#include +#include /* EvalContext */ #define CMDB_SOURCE_TAG "source=cmdb" -JsonElement *ReadJsonFile(const char *filename, LogLevel log_level, size_t size_max); bool LoadCMDBData(EvalContext *ctx); #endif /* CFENGINE_CMDB_H */ diff --git a/libpromises/generic_agent.c b/libpromises/generic_agent.c index 9a738f5f117..53414906026 100644 --- a/libpromises/generic_agent.c +++ b/libpromises/generic_agent.c @@ -73,6 +73,7 @@ #include #include #include /* LoadCMDBData() */ +#include /* ReadJsonFile() */ #include /* LoadAugments() */ #include "cf3.defs.h" diff --git a/libpromises/host_data.c b/libpromises/host_data.c new file mode 100644 index 00000000000..8237a92bbc0 --- /dev/null +++ b/libpromises/host_data.c @@ -0,0 +1,140 @@ +/* + Copyright 2024 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program 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; version 3. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#include +#include +#include +#include /* StringSet, JsonArrayToStringSet() */ +#include /* VarRef, VarRefParse() */ + +#include + +JsonElement *ReadJsonFile(const char *filename, LogLevel log_level, size_t size_max) +{ + assert(filename != NULL); + + JsonElement *doc = NULL; + JsonParseError err = JsonParseFile(filename, size_max, &doc); + + if (err == JSON_PARSE_ERROR_NO_SUCH_FILE) + { + Log(log_level, "Could not open JSON file %s", filename); + return NULL; + } + + if (err != JSON_PARSE_OK || + doc == NULL) + { + Log(log_level, "Could not parse JSON file %s: %s", filename, JsonParseErrorToString(err)); + } + + return doc; +} + +VarRef *GetVariableRefFromDataKey(const char *key, const char *default_ns, + const char *default_scope, const char *source) +{ + assert(key != NULL); + assert(default_scope != NULL); + assert(source != NULL); + + VarRef *ref = VarRefParse(key); + if ((ref->ns != NULL) && (ref->scope == NULL)) + { + Log(LOG_LEVEL_ERR, "Invalid variable specification in %s: '%s'" + " (bundle name has to be specified if namespace is specified)", source, key); + VarRefDestroy(ref); + return NULL; + } + + if ((ref->ns == NULL) && (default_ns != NULL)) + { + ref->ns = xstrdup(default_ns); + } + if (ref->scope == NULL) + { + ref->scope = xstrdup(default_scope); + } + return ref; +} + +StringSet *GetTagsFromJsonTags(const char *item_type, + const char *key, + const JsonElement *json_tags, + const char *default_tag, + const char *source) +{ + StringSet *tags = NULL; + if (JSON_NOT_NULL(json_tags)) + { + if ((JsonGetType(json_tags) != JSON_TYPE_ARRAY) || + (!JsonArrayContainsOnlyPrimitives((JsonElement*) json_tags))) + { + Log(LOG_LEVEL_ERR, + "Invalid tags information for %s '%s' in %s:" + " must be a JSON array of strings", + item_type, key, source); + } + else + { + tags = JsonArrayToStringSet(json_tags); + if (tags == NULL) + { + Log(LOG_LEVEL_ERR, + "Invalid tags information for %s '%s' in %s:" + " must be a JSON array of strings", + item_type, key, source); + } + } + } + if (tags == NULL) + { + tags = StringSetNew(); + } + StringSetAdd(tags, xstrdup(default_tag)); + + return tags; +} + +const char *GetCommentFromJsonData(const char *item_type, const char *identifier, + const JsonElement *json_object, const char *source) +{ + assert(JsonGetType(json_object) == JSON_TYPE_OBJECT); + + JsonElement *json_comment = JsonObjectGet(json_object, JSON_DATA_KEY_COMMENT); + if (NULL_JSON(json_comment)) + { + return NULL; + } + + if (JsonGetType(json_comment) != JSON_TYPE_STRING) + { + Log(LOG_LEVEL_ERR, + "Invalid type of the 'comment' field for the '%s' %s in %s, must be a string", + identifier, item_type, source); + return NULL; + } + + return JsonPrimitiveGetAsString(json_comment); +} diff --git a/libpromises/host_data.h b/libpromises/host_data.h new file mode 100644 index 00000000000..a9be2f0ab26 --- /dev/null +++ b/libpromises/host_data.h @@ -0,0 +1,83 @@ +/* + Copyright 2024 Northern.tech AS + + This file is part of CFEngine 3 - written and maintained by Northern.tech AS. + + This program 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; version 3. + + This program 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 this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA + + To the extent this program is licensed as part of the Enterprise + versions of CFEngine, the applicable Commercial Open Source License + (COSL) may apply to this file if you as a licensee so wish it. See + included file COSL.txt. +*/ + +#ifndef CFENGINE_HOST_DATA_H +#define CFENGINE_HOST_DATA_H + +/* Helpers for the JSON data defining variables and classes, shared by the + * host-specific data (CMDB, host_specific.json) and augments (def.json). */ + +#include +#include /* LogLevel */ +#include /* StringSet */ +#include /* VarRef */ + +/* Keys used in the JSON data for variables and classes */ +#define JSON_DATA_KEY_VALUE "value" +#define JSON_DATA_KEY_TAGS "tags" +#define JSON_DATA_KEY_COMMENT "comment" +#define JSON_DATA_KEY_CLASS_EXPRESSIONS "class_expressions" +#define JSON_DATA_KEY_REGULAR_EXPRESSIONS "regular_expressions" + +JsonElement *ReadJsonFile(const char *filename, LogLevel log_level, size_t size_max); + +/* Helpers for parsing the variables and classes JSON data. + * + * @source describes the data in log messages, e.g. "CMDB data" or + * "augments data in '/path/to/def.json'". + */ + +/** + * Parses the name of a variable from the JSON data. + * + * @param default_ns namespace to use if not specified (or %NULL) + * @param default_scope bundle to use if not specified + * @return The variable reference or %NULL (after logging an error) if a + * namespace is specified without a bundle. + */ +VarRef *GetVariableRefFromDataKey(const char *key, const char *default_ns, + const char *default_scope, const char *source); + +/** + * Gets the set of tags from the 'tags' JSON array of a variable or class + * (@json_tags may be %NULL) and adds @default_tag to it. + * + * @return A new set of tags, never %NULL. Invalid tags are logged and ignored. + */ +StringSet *GetTagsFromJsonTags(const char *item_type, + const char *key, + const JsonElement *json_tags, + const char *default_tag, + const char *source); + +/** + * Gets the 'comment' field of a variable or class JSON object. + * + * @return The comment (owned by @json_object) or %NULL if not specified or + * invalid (logged). + */ +const char *GetCommentFromJsonData(const char *item_type, const char *identifier, + const JsonElement *json_object, const char *source); + +#endif /* CFENGINE_HOST_DATA_H */ From c12528ac45da310654aa46521550fd3d4db0df91 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 10 Nov 2026 13:32:08 +0100 Subject: [PATCH 5/7] Fixed crash on augments class expressions that are not arrays Bug already present on master. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/augments.c | 14 +++++- .../def_json/classes_exprs_not_array.cf | 49 +++++++++++++++++++ .../def_json/classes_exprs_not_array.cf.json | 20 ++++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf create mode 100644 tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf.json diff --git a/libpromises/augments.c b/libpromises/augments.c index 1ac1eff2d8f..b6736bb4c91 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -426,10 +426,22 @@ static bool ReadAugmentClasses(EvalContext *ctx, const char *filename, JsonEleme continue; } + /* NULL_JSON() above also treats a JSON null as not specified */ + const bool use_class_exprs = JSON_NOT_NULL(class_exprs); + const JsonElement *exprs = use_class_exprs ? class_exprs : reg_exprs; + if (JsonGetType(exprs) != JSON_TYPE_ARRAY) + { + Log(LOG_LEVEL_ERR, "Invalid augments class data for class '%s' in '%s':" + " \"%s\" must be a JSON array", + ckey, filename, + use_class_exprs ? JSON_DATA_KEY_CLASS_EXPRESSIONS : JSON_DATA_KEY_REGULAR_EXPRESSIONS); + continue; + } + StringSet *tags = GetTagsFromJsonTags("class", ckey, json_tags, "source=augments_file", source); const char *comment = GetCommentFromJsonData("class", ckey, data, source); - char *check = GetFirstMatchingClassExpression(ctx, class_exprs ? class_exprs : reg_exprs); + char *check = GetFirstMatchingClassExpression(ctx, exprs); if (check != NULL) { Log(LOG_LEVEL_VERBOSE, "Installing augments class '%s' (checked array entry '%s') from file '%s'", diff --git a/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf b/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf new file mode 100644 index 00000000000..cd8fab34f5a --- /dev/null +++ b/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf @@ -0,0 +1,49 @@ +# Test that augments classes whose "class_expressions" or +# "regular_expressions" are not JSON arrays are rejected with an error message +# instead of crashing, and that a JSON null value is treated as not specified. +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent test +{ + meta: + "description" -> { "CFE-3658" } + string => "Augments class expressions that are not arrays do not crash the agent"; + + methods: + "" usebundle => file_make("$(sys.inputdir)/promises.cf", ''); + + "" + usebundle => file_copy( + "$(this.promise_filename).json", "$(sys.inputdir)/def.json" + ); +} + +####################################################### +bundle agent check +{ + vars: + "command" + string => "$(sys.cf_promises) --show-classes -f $(sys.inputdir)/promises.cf 2>&1"; + + methods: + "" + usebundle => dcs_passif_output( + concat( + "(?=.*Invalid augments class data for class 'test_class_string_class_exprs' in '[^']*': \"class_expressions\" must be a JSON array)", + "(?=.*Invalid augments class data for class 'test_class_string_reg_exprs' in '[^']*': \"regular_expressions\" must be a JSON array)", + "(?=.*Invalid augments class data for class 'test_class_object_class_exprs' in '[^']*': \"class_expressions\" must be a JSON array)", + "(?=.*\ntest_class_null_class_exprs\s+source=augments_file)", + "(?=.*\ntest_class_valid\s+source=augments_file)", + ".*" + ), + ".*\n(test_class_string_class_exprs|test_class_string_reg_exprs|test_class_object_class_exprs)\s.*", + $(command), + $(this.promise_filename) + ); +} diff --git a/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf.json b/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf.json new file mode 100644 index 00000000000..65412275356 --- /dev/null +++ b/tests/acceptance/00_basics/def_json/classes_exprs_not_array.cf.json @@ -0,0 +1,20 @@ +{ + "classes": { + "test_class_string_class_exprs": { + "class_expressions": "any" + }, + "test_class_string_reg_exprs": { + "regular_expressions": "any" + }, + "test_class_object_class_exprs": { + "class_expressions": { "any": "any" } + }, + "test_class_null_class_exprs": { + "class_expressions": null, + "regular_expressions": ["any"] + }, + "test_class_valid": { + "class_expressions": ["any"] + } + } +} From 31d99585a219b98bae044eb40f985e7b1a4288b9 Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 10 Nov 2026 13:32:40 +0100 Subject: [PATCH 6/7] Fixed CanSetClass() memory leak and NULL in log message Bugs already present on master. The leak is only visible with valgrind/ASan. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/augments.c | 3 +- .../12-augments-no-override-class.cf | 57 +++++++++++++++++++ .../12-augments-no-override-class.cf.def.json | 6 ++ .../12-augments-no-override-class.cf.json | 6 ++ 4 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf create mode 100644 tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.def.json create mode 100644 tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.json diff --git a/libpromises/augments.c b/libpromises/augments.c index b6736bb4c91..1a0014e165f 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -140,8 +140,9 @@ static inline bool CanSetClass(const EvalContext *ctx, const char *class_spec) { Log(LOG_LEVEL_VERBOSE, "Cannot set class %s:%s from augments, already defined from host-specific data", - ns, class_spec); + (ns != NULL) ? ns : "default", class_spec); } + free(ns); return can_set; } diff --git a/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf new file mode 100644 index 00000000000..c2d6ea804d5 --- /dev/null +++ b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf @@ -0,0 +1,57 @@ +# Test that augments (def.json) cannot override classes defined in the +# host-specific data (host_specific.json), with and without a namespace in the +# augments class name +body common control +{ + inputs => { "../../default.sub.cf" }; + bundlesequence => { default("$(this.promise_filename)") }; + version => "1.0"; +} + +####################################################### +bundle agent init +{ + files: + "$(sys.workdir)$(const.dirsep)data$(const.dirsep)." + create => "true", + perms => mog( + "0700", "$(sys.user_data[username])", "$(sys.user_data[username])" + ); + + methods: + "prepare_host_specific_data" + usebundle => file_copy( + "$(this.promise_filename).json", + "$(sys.workdir)$(const.dirsep)data$(const.dirsep)host_specific.json" + ), + handle => "prepare_host_specific_data"; + + "def_json" + usebundle => file_copy( + "$(this.promise_filename).def.json", "$(sys.inputdir)/def.json" + ); + + "empty_promises_cf" + usebundle => file_make("$(sys.inputdir)/promises.cf", ''); +} + +####################################################### +bundle agent check +{ + vars: + "command" + string => "$(sys.cf_promises) --verbose -f $(sys.inputdir)/promises.cf 2>&1 | $(G.grep) 'Cannot set class'"; + + methods: + "" + usebundle => dcs_passif_output( + concat( + "(?=.*Cannot set class default:test_class_default_ns from augments, already defined from host-specific data)", + "(?=.*Cannot set class ns1:test_class_ns1 from augments, already defined from host-specific data)", + ".*" + ), + ".*\(null\).*", + $(command), + $(this.promise_filename) + ); +} diff --git a/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.def.json b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.def.json new file mode 100644 index 00000000000..d18bcf5186d --- /dev/null +++ b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.def.json @@ -0,0 +1,6 @@ +{ + "classes": { + "test_class_default_ns": ["any"], + "ns1:test_class_ns1": ["any"] + } +} diff --git a/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.json b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.json new file mode 100644 index 00000000000..6e33ab98295 --- /dev/null +++ b/tests/acceptance/00_basics/06_host_specific_data/12-augments-no-override-class.cf.json @@ -0,0 +1,6 @@ +{ + "classes": { + "default:test_class_default_ns": "any::", + "ns1:test_class_ns1": "any::" + } +} From b68070a2105ca33f04844f74ec3a2e23a912dc6b Mon Sep 17 00:00:00 2001 From: Victor Moene Date: Tue, 10 Nov 2026 13:32:53 +0100 Subject: [PATCH 7/7] Fixed error message for invalid "augments" key It said "augment inputs". Already present on master. Ticket: CFE-3658 Signed-off-by: Victor Moene --- libpromises/augments.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libpromises/augments.c b/libpromises/augments.c index 1a0014e165f..57245e5ba18 100644 --- a/libpromises/augments.c +++ b/libpromises/augments.c @@ -504,7 +504,7 @@ static bool ReadAugmentAugments(EvalContext *ctx, const char *filename, JsonElem JsonGetContainerType(further_augments) != JSON_CONTAINER_TYPE_ARRAY || !JsonArrayContainsOnlyPrimitives(further_augments)) { - Log(LOG_LEVEL_ERR, "Trying to augment inputs in '%s' but the value was not a list of strings", + Log(LOG_LEVEL_ERR, "Trying to load further augments files in '%s' but the value was not a list of strings", filename); return false; }