master
c 294 lines 12.3 KB
Raw
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 }