| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "dyncfg-internals.h" |
| 4 | #include "dyncfg.h" |
| 5 | |
| 6 | static int dyncfg_tree_compar(const void *a, const void *b) { |
| 7 | const DICTIONARY_ITEM *item1 = *(const DICTIONARY_ITEM **)a; |
| 8 | const DICTIONARY_ITEM *item2 = *(const DICTIONARY_ITEM **)b; |
| 9 | |
| 10 | DYNCFG *df1 = dictionary_acquired_item_value(item1); |
| 11 | DYNCFG *df2 = dictionary_acquired_item_value(item2); |
| 12 | |
| 13 | int rc = string_cmp(df1->path, df2->path); |
| 14 | if(rc == 0) |
| 15 | rc = strcmp(dictionary_acquired_item_name(item1), dictionary_acquired_item_name(item2)); |
| 16 | |
| 17 | return rc; |
| 18 | } |
| 19 | |
| 20 | static void dyncfg_to_json(DYNCFG *df, const char *id, BUFFER *wb, bool anonymous) { |
| 21 | buffer_json_member_add_object(wb, id); |
| 22 | { |
| 23 | buffer_json_member_add_string(wb, "type", dyncfg_id2type(df->type)); |
| 24 | |
| 25 | if(df->type == DYNCFG_TYPE_JOB) |
| 26 | buffer_json_member_add_string(wb, "template", string2str(df->template)); |
| 27 | |
| 28 | buffer_json_member_add_string(wb, "status", dyncfg_id2status(df->current.status)); |
| 29 | dyncfg_cmds2json_array(df->current.status == DYNCFG_STATUS_ORPHAN ? DYNCFG_CMD_REMOVE : df->cmds, "cmds", wb); |
| 30 | buffer_json_member_add_object(wb, "access"); |
| 31 | { |
| 32 | http_access2buffer_json_array(wb, "view", df->view_access); |
| 33 | http_access2buffer_json_array(wb, "edit", df->edit_access); |
| 34 | } |
| 35 | buffer_json_object_close(wb); |
| 36 | buffer_json_member_add_string(wb, "source_type", dyncfg_id2source_type(df->current.source_type)); |
| 37 | buffer_json_member_add_string(wb, "source", df->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && anonymous ? "User details hidden in anonymous mode. Sign in to access configuration details." : string2str(df->current.source)); |
| 38 | buffer_json_member_add_boolean(wb, "sync", df->sync); |
| 39 | buffer_json_member_add_boolean(wb, "user_disabled", df->dyncfg.user_disabled); |
| 40 | buffer_json_member_add_boolean(wb, "restart_required", df->dyncfg.restart_required); |
| 41 | buffer_json_member_add_boolean(wb, "plugin_rejected", df->dyncfg.plugin_rejected); |
| 42 | buffer_json_member_add_object(wb, "payload"); |
| 43 | { |
| 44 | if (df->dyncfg.payload && buffer_strlen(df->dyncfg.payload)) { |
| 45 | buffer_json_member_add_boolean(wb, "available", true); |
| 46 | buffer_json_member_add_string(wb, "status", dyncfg_id2status(df->dyncfg.status)); |
| 47 | buffer_json_member_add_string(wb, "source_type", dyncfg_id2source_type(df->dyncfg.source_type)); |
| 48 | buffer_json_member_add_string(wb, "source", df->dyncfg.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && anonymous ? "User details hidden in anonymous mode. Sign in to access configuration details." :string2str(df->dyncfg.source)); |
| 49 | buffer_json_member_add_uint64(wb, "created_ut", df->dyncfg.created_ut); |
| 50 | buffer_json_member_add_uint64(wb, "modified_ut", df->dyncfg.modified_ut); |
| 51 | buffer_json_member_add_string(wb, "content_type", content_type_id2string(df->dyncfg.payload->content_type)); |
| 52 | buffer_json_member_add_uint64(wb, "content_length", df->dyncfg.payload->len); |
| 53 | } else |
| 54 | buffer_json_member_add_boolean(wb, "available", false); |
| 55 | } |
| 56 | buffer_json_object_close(wb); // payload |
| 57 | buffer_json_member_add_uint64(wb, "saves", df->dyncfg.saves); |
| 58 | buffer_json_member_add_uint64(wb, "created_ut", df->current.created_ut); |
| 59 | buffer_json_member_add_uint64(wb, "modified_ut", df->current.modified_ut); |
| 60 | } |
| 61 | buffer_json_object_close(wb); |
| 62 | } |
| 63 | |
| 64 | static void dyncfg_tree_for_host(RRDHOST *host, BUFFER *wb, const char *path, const char *id, bool anonymous) { |
| 65 | size_t entries = dictionary_entries(dyncfg_globals.nodes); |
| 66 | size_t used = 0; |
| 67 | const DICTIONARY_ITEM **items = entries ? callocz(entries, sizeof(*items)) : NULL; |
| 68 | size_t restart_required = 0, plugin_rejected = 0, status_incomplete = 0, status_failed = 0; |
| 69 | |
| 70 | STRING *template = NULL; |
| 71 | if(id && *id) |
| 72 | template = string_strdupz(id); |
| 73 | |
| 74 | size_t path_len = strlen(path); |
| 75 | DYNCFG *df; |
| 76 | dfe_start_read(dyncfg_globals.nodes, df) { |
| 77 | if(!UUIDeq(df->host_uuid, host->host_id)) |
| 78 | continue; |
| 79 | |
| 80 | if(strncmp(string2str(df->path), path, path_len) != 0) |
| 81 | continue; |
| 82 | |
| 83 | if(!rrd_function_available(host, string2str(df->function))) |
| 84 | df->current.status = DYNCFG_STATUS_ORPHAN; |
| 85 | |
| 86 | if((id && strcmp(id, df_dfe.name) != 0) && (template && df->template != template)) |
| 87 | continue; |
| 88 | |
| 89 | items[used++] = dictionary_acquired_item_dup(dyncfg_globals.nodes, df_dfe.item); |
| 90 | } |
| 91 | dfe_done(df); |
| 92 | |
| 93 | if(used > 1) |
| 94 | qsort(items, used, sizeof(const DICTIONARY_ITEM *), dyncfg_tree_compar); |
| 95 | |
| 96 | buffer_flush(wb); |
| 97 | buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); |
| 98 | |
| 99 | buffer_json_member_add_uint64(wb, "version", 1); |
| 100 | |
| 101 | buffer_json_member_add_object(wb, "tree"); |
| 102 | { |
| 103 | STRING *last_path = NULL; |
| 104 | for (size_t i = 0; i < used; i++) { |
| 105 | df = dictionary_acquired_item_value(items[i]); |
| 106 | if (df->path != last_path) { |
| 107 | last_path = df->path; |
| 108 | |
| 109 | if (i) |
| 110 | buffer_json_object_close(wb); |
| 111 | |
| 112 | buffer_json_member_add_object(wb, string2str(last_path)); |
| 113 | } |
| 114 | |
| 115 | dyncfg_to_json(df, dictionary_acquired_item_name(items[i]), wb, anonymous); |
| 116 | |
| 117 | if (df->dyncfg.plugin_rejected) |
| 118 | plugin_rejected++; |
| 119 | |
| 120 | if(df->current.status != DYNCFG_STATUS_ORPHAN) { |
| 121 | if (df->dyncfg.restart_required) |
| 122 | restart_required++; |
| 123 | |
| 124 | if (df->current.status == DYNCFG_STATUS_FAILED) |
| 125 | status_failed++; |
| 126 | |
| 127 | if (df->current.status == DYNCFG_STATUS_INCOMPLETE) |
| 128 | status_incomplete++; |
| 129 | } |
| 130 | } |
| 131 | |
| 132 | if (used) |
| 133 | buffer_json_object_close(wb); |
| 134 | } |
| 135 | buffer_json_object_close(wb); // tree |
| 136 | |
| 137 | buffer_json_member_add_object(wb, "attention"); |
| 138 | { |
| 139 | buffer_json_member_add_boolean(wb, "degraded", restart_required + plugin_rejected + status_failed + status_incomplete > 0); |
| 140 | buffer_json_member_add_uint64(wb, "restart_required", restart_required); |
| 141 | buffer_json_member_add_uint64(wb, "plugin_rejected", plugin_rejected); |
| 142 | buffer_json_member_add_uint64(wb, "status_failed", status_failed); |
| 143 | buffer_json_member_add_uint64(wb, "status_incomplete", status_incomplete); |
| 144 | } |
| 145 | buffer_json_object_close(wb); // attention |
| 146 | |
| 147 | buffer_json_agents_v2(wb, NULL, 0, false, false, 0); |
| 148 | |
| 149 | buffer_json_finalize(wb); |
| 150 | |
| 151 | for(size_t i = 0; i < used ;i++) |
| 152 | dictionary_acquired_item_release(dyncfg_globals.nodes, items[i]); |
| 153 | |
| 154 | freez(items); |
| 155 | } |
| 156 | |
| 157 | static int dyncfg_config_execute_cb(struct rrd_function_execute *rfe, void *data) { |
| 158 | RRDHOST *host = data; |
| 159 | int code; |
| 160 | |
| 161 | CLEAN_CHAR_P *buf = strdupz(rfe->function); |
| 162 | |
| 163 | char *words[MAX_FUNCTION_PARAMETERS]; // an array of pointers for the words in this line |
| 164 | size_t num_words = quoted_strings_splitter_whitespace(buf, words, MAX_FUNCTION_PARAMETERS); |
| 165 | |
| 166 | const char *config = get_word(words, num_words, 0); |
| 167 | const char *action = get_word(words, num_words, 1); |
| 168 | const char *path = get_word(words, num_words, 2); |
| 169 | const char *id = get_word(words, num_words, 3); |
| 170 | |
| 171 | if(!config || !*config || strcmp(config, PLUGINSD_FUNCTION_CONFIG) != 0) { |
| 172 | char *msg = "invalid function call, expected: config"; |
| 173 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG TREE: function call '%s': %s", rfe->function, msg); |
| 174 | code = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg); |
| 175 | goto cleanup; |
| 176 | } |
| 177 | |
| 178 | if(!action || !*action) { |
| 179 | char *msg = "invalid function call, expected: config tree"; |
| 180 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG TREE: function call '%s': %s", rfe->function, msg); |
| 181 | code = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg); |
| 182 | goto cleanup; |
| 183 | } |
| 184 | |
| 185 | if(strcmp(action, "tree") == 0) { |
| 186 | if(!path || !*path) |
| 187 | path = "/"; |
| 188 | |
| 189 | if(!id || !*id) |
| 190 | id = NULL; |
| 191 | else if(!dyncfg_is_valid_id(id)) { |
| 192 | char *msg = "invalid id given"; |
| 193 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG TREE: function call '%s': %s", rfe->function, msg); |
| 194 | code = dyncfg_default_response(rfe->result.wb, HTTP_RESP_BAD_REQUEST, msg); |
| 195 | goto cleanup; |
| 196 | } |
| 197 | |
| 198 | code = HTTP_RESP_OK; |
| 199 | dyncfg_tree_for_host(host, rfe->result.wb, path, id, !(rfe->user_access & HTTP_ACCESS_SENSITIVE_DATA)); |
| 200 | } |
| 201 | else { |
| 202 | const char *name = id; |
| 203 | id = action; |
| 204 | action = path; |
| 205 | path = NULL; |
| 206 | |
| 207 | DYNCFG_CMDS cmd = dyncfg_cmds2id(action); |
| 208 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id); |
| 209 | if(!item) { |
| 210 | item = dyncfg_get_template_of_new_job(id); |
| 211 | |
| 212 | if(item && (!name || !*name)) { |
| 213 | const char *n = dictionary_acquired_item_name(item); |
| 214 | if(strncmp(id, n, strlen(n)) == 0 && id[strlen(n)] == ':') |
| 215 | name = &id[strlen(n) + 1]; |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | if(item) { |
| 220 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 221 | |
| 222 | if(!rrd_function_available(host, string2str(df->function))) |
| 223 | df->current.status = DYNCFG_STATUS_ORPHAN; |
| 224 | |
| 225 | if(cmd == DYNCFG_CMD_REMOVE) { |
| 226 | bool delete = (df->current.status == DYNCFG_STATUS_ORPHAN); |
| 227 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 228 | item = NULL; |
| 229 | |
| 230 | if(delete) { |
| 231 | if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->edit_access)) { |
| 232 | code = dyncfg_default_response( |
| 233 | rfe->result.wb, HTTP_RESP_FORBIDDEN, |
| 234 | "dyncfg: you don't have enough edit permissions to execute this command"); |
| 235 | goto cleanup; |
| 236 | } |
| 237 | |
| 238 | dictionary_del(dyncfg_globals.nodes, id); |
| 239 | dyncfg_file_delete(id); |
| 240 | code = dyncfg_default_response(rfe->result.wb, 200, ""); |
| 241 | goto cleanup; |
| 242 | } |
| 243 | } |
| 244 | else if((cmd == DYNCFG_CMD_USERCONFIG || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_GET) && df->current.status != DYNCFG_STATUS_ORPHAN) { |
| 245 | const char *old_rfe_function = rfe->function; |
| 246 | char buf2[2048]; |
| 247 | if(cmd == DYNCFG_CMD_GET) |
| 248 | snprintfz(buf2, sizeof(buf2), "config %s %s", dictionary_acquired_item_name(item), action); |
| 249 | else |
| 250 | snprintfz(buf2, sizeof(buf2), "config %s %s %s", dictionary_acquired_item_name(item), action, name?name:""); |
| 251 | rfe->function = buf2; |
| 252 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 253 | item = NULL; |
| 254 | code = dyncfg_function_intercept_cb(rfe, data); |
| 255 | rfe->function = old_rfe_function; |
| 256 | return code; |
| 257 | } |
| 258 | |
| 259 | if(item) |
| 260 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 261 | } |
| 262 | |
| 263 | code = HTTP_RESP_NOT_FOUND; |
| 264 | nd_log(NDLS_DAEMON, NDLP_ERR, |
| 265 | "DYNCFG: unknown config id '%s' in call: '%s'. " |
| 266 | "This can happen if the plugin that registered the dynamic configuration is not running now.", |
| 267 | id, rfe->function); |
| 268 | |
| 269 | rrd_call_function_error( |
| 270 | rfe->result.wb, |
| 271 | "Unknown config id given.", code); |
| 272 | } |
| 273 | |
| 274 | cleanup: |
| 275 | if(rfe->result.cb) |
| 276 | rfe->result.cb(rfe->result.wb, code, rfe->result.data); |
| 277 | |
| 278 | return code; |
| 279 | } |
| 280 | |
| 281 | // ---------------------------------------------------------------------------- |
| 282 | // this adds a 'config' function to all leaf nodes (localhost and virtual nodes) |
| 283 | // which is used to serve the tree and act as a catch-all for all config calls |
| 284 | // for which there is no id overloaded. |
| 285 | |
| 286 | void dyncfg_host_init(RRDHOST *host) { |
| 287 | // IMPORTANT: |
| 288 | // This function needs to be async, although it is internal. |
| 289 | // The reason is that it can call by itself another function that may or may not be internal (sync). |
| 290 | |
| 291 | rrd_function_add(host, NULL, PLUGINSD_FUNCTION_CONFIG, 120, 1000, DYNCFG_FUNCTIONS_VERSION, |
| 292 | "Dynamic configuration", "config", HTTP_ACCESS_ANONYMOUS_DATA, |
| 293 | false, dyncfg_config_execute_cb, host); |
| 294 | } |