master
c 485 lines 16.7 KB
Raw
1 // SPDX-License-Identifier: GPL-3.0-or-later
2
3 #include "dyncfg-internals.h"
4 #include "dyncfg.h"
5
6 struct dyncfg_globals dyncfg_globals = { 0 };
7
8 RRDHOST *dyncfg_rrdhost_by_uuid(ND_UUID *uuid) {
9 char uuid_str[UUID_STR_LEN];
10 uuid_unparse_lower(uuid->uuid, uuid_str);
11
12 RRDHOST *host = rrdhost_find_by_guid(uuid_str);
13 if(!host)
14 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: cannot find host with UUID '%s'", uuid_str);
15
16 return host;
17 }
18
19 RRDHOST *dyncfg_rrdhost(DYNCFG *df) {
20 return dyncfg_rrdhost_by_uuid(&df->host_uuid);
21 }
22
23 void dyncfg_cleanup(DYNCFG *v) {
24 string_freez(v->dyncfg.source);
25 v->dyncfg.source = NULL;
26
27 buffer_free(v->dyncfg.payload);
28 v->dyncfg.payload = NULL;
29
30 string_freez(v->path);
31 v->path = NULL;
32
33 string_freez(v->current.source);
34 v->current.source = NULL;
35
36 string_freez(v->function);
37 v->function = NULL;
38
39 string_freez(v->template);
40 v->template = NULL;
41 }
42
43 static void dyncfg_normalize(DYNCFG *df) {
44 usec_t now_ut = now_realtime_usec();
45
46 if(!df->current.created_ut)
47 df->current.created_ut = now_ut;
48
49 if(!df->current.modified_ut)
50 df->current.modified_ut = now_ut;
51 }
52
53 static void dyncfg_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) {
54 DYNCFG *df = value;
55 dyncfg_cleanup(df);
56 }
57
58 static void dyncfg_insert_cb(const DICTIONARY_ITEM *item, void *value, void *data __maybe_unused) {
59 DYNCFG *df = value;
60 dyncfg_normalize(df);
61
62 const char *id = dictionary_acquired_item_name(item);
63 size_t buf_size = strlen(id) + 20;
64 CLEAN_CHAR_P *buf = mallocz(buf_size);
65 snprintfz(buf, buf_size, PLUGINSD_FUNCTION_CONFIG " %s", id);
66 df->function = string_strdupz(buf);
67
68 if(df->type == DYNCFG_TYPE_JOB && !df->template) {
69 const char *last_colon = strrchr(id, ':');
70 if(last_colon)
71 df->template = string_strndupz(id, last_colon - id);
72 else
73 nd_log(NDLS_DAEMON, NDLP_WARNING,
74 "DYNCFG: id '%s' is a job, but does not contain a colon to find the template", id);
75 }
76 }
77
78 static void dyncfg_react_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) {
79 DYNCFG *df = value; (void)df;
80 ;
81 }
82
83 static bool dyncfg_conflict_cb(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data) {
84 bool *overwrite_cb_ptr = data;
85 bool overwrite_cb = (overwrite_cb_ptr && *overwrite_cb_ptr);
86
87 DYNCFG *v = old_value;
88 DYNCFG *nv = new_value;
89
90 size_t changes = 0;
91
92 dyncfg_normalize(nv);
93
94 if(!UUIDeq(v->host_uuid, nv->host_uuid)) {
95 char u1[UUID_STR_LEN], u2[UUID_STR_LEN];
96 nd_uuid_unparse_lower(v->host_uuid.uuid, u1);
97 nd_uuid_unparse_lower(nv->host_uuid.uuid, u2);
98
99 nd_log(NDLS_DAEMON, NDLP_NOTICE,
100 "DYNCFG: configuration '%s' changed host id from '%s' to '%s'",
101 dictionary_acquired_item_name(item), u1, u2);
102
103 SWAP(v->host_uuid, nv->host_uuid);
104 changes++;
105 }
106
107 if(v->path != nv->path) {
108 nd_log(NDLS_DAEMON, NDLP_NOTICE,
109 "DYNCFG: configuration '%s' changed path from '%s' to '%s'",
110 dictionary_acquired_item_name(item), string2str(v->path), string2str(nv->path));
111
112 SWAP(v->path, nv->path);
113 changes++;
114 }
115
116 if(v->cmds != nv->cmds) {
117 SWAP(v->cmds, nv->cmds);
118 changes++;
119 }
120
121 if(v->type != nv->type) {
122 SWAP(v->type, nv->type);
123 changes++;
124 }
125
126 if(v->view_access != nv->view_access) {
127 SWAP(v->view_access, nv->view_access);
128 changes++;
129 }
130
131 if(v->edit_access != nv->edit_access) {
132 SWAP(v->edit_access, nv->edit_access);
133 changes++;
134 }
135
136 if(v->current.status != nv->current.status) {
137 SWAP(v->current.status, nv->current.status);
138 changes++;
139 }
140
141 if (v->current.source_type != nv->current.source_type) {
142 SWAP(v->current.source_type, nv->current.source_type);
143 changes++;
144 }
145
146 if (v->current.source != nv->current.source) {
147 SWAP(v->current.source, nv->current.source);
148 changes++;
149 }
150
151 if(nv->current.created_ut < v->current.created_ut) {
152 SWAP(v->current.created_ut, nv->current.created_ut);
153 changes++;
154 }
155
156 if(nv->current.modified_ut > v->current.modified_ut) {
157 SWAP(v->current.modified_ut, nv->current.modified_ut);
158 changes++;
159 }
160
161 if(!v->execute_cb || (overwrite_cb && nv->execute_cb && (v->execute_cb != nv->execute_cb || v->execute_cb_data != nv->execute_cb_data))) {
162 v->sync = nv->sync,
163 v->execute_cb = nv->execute_cb;
164 v->execute_cb_data = nv->execute_cb_data;
165 changes++;
166 }
167
168 dyncfg_cleanup(nv);
169
170 return changes > 0;
171 }
172
173 // ----------------------------------------------------------------------------
174
175 void dyncfg_init_low_level(bool load_saved) {
176 if(!dyncfg_globals.nodes) {
177 dyncfg_globals.nodes = dictionary_create_advanced(DICT_OPTION_FIXED_SIZE | DICT_OPTION_DONT_OVERWRITE_VALUE, &dictionary_stats_category_dyncfg, sizeof(DYNCFG));
178 dictionary_register_insert_callback(dyncfg_globals.nodes, dyncfg_insert_cb, NULL);
179 dictionary_register_react_callback(dyncfg_globals.nodes, dyncfg_react_cb, NULL);
180 dictionary_register_conflict_callback(dyncfg_globals.nodes, dyncfg_conflict_cb, NULL);
181 dictionary_register_delete_callback(dyncfg_globals.nodes, dyncfg_delete_cb, NULL);
182
183 char path[PATH_MAX];
184 snprintfz(path, sizeof(path), "%s/%s", netdata_configured_varlib_dir, "config");
185
186 if(mkdir(path, 0755) == -1) {
187 if(errno != EEXIST)
188 nd_log(NDLS_DAEMON, NDLP_CRIT, "DYNCFG: failed to create dynamic configuration directory '%s'", path);
189 }
190
191 dyncfg_globals.dir = strdupz(path);
192
193 if(load_saved)
194 dyncfg_load_all();
195 }
196 }
197
198 void dyncfg_shutdown_low_level(void) {
199 if(dyncfg_globals.nodes) {
200 dictionary_destroy(dyncfg_globals.nodes);
201 dyncfg_globals.nodes = NULL;
202 }
203
204 freez((void *)dyncfg_globals.dir);
205 dyncfg_globals.dir = NULL;
206 }
207
208 // ----------------------------------------------------------------------------
209
210 const DICTIONARY_ITEM *dyncfg_add_internal(RRDHOST *host, const char *id, const char *path,
211 DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type,
212 const char *source, DYNCFG_CMDS cmds,
213 usec_t created_ut, usec_t modified_ut,
214 bool sync, HTTP_ACCESS view_access, HTTP_ACCESS edit_access,
215 rrd_function_execute_cb_t execute_cb, void *execute_cb_data,
216 bool overwrite_cb) {
217 DYNCFG tmp = {
218 .host_uuid = host->host_id,
219 .path = string_strdupz(path),
220 .cmds = cmds,
221 .type = type,
222 .view_access = view_access,
223 .edit_access = edit_access,
224 .current = {
225 .status = status,
226 .source_type = source_type,
227 .source = string_strdupz(source),
228 .created_ut = created_ut,
229 .modified_ut = modified_ut,
230 },
231 .sync = sync,
232 .dyncfg = { 0 },
233 .execute_cb = execute_cb,
234 .execute_cb_data = execute_cb_data,
235 };
236
237 return dictionary_set_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1, &tmp, sizeof(tmp), &overwrite_cb);
238 }
239
240 static void dyncfg_send_updates(const char *id) {
241 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1);
242 if(!item) {
243 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: asked to update plugin for configuration '%s', but it is not found.", id);
244 return;
245 }
246
247 DYNCFG *df = dictionary_acquired_item_value(item);
248
249 if(df->type == DYNCFG_TYPE_SINGLE || df->type == DYNCFG_TYPE_JOB) {
250 if (df->cmds & DYNCFG_CMD_UPDATE && df->dyncfg.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && df->dyncfg.payload && buffer_strlen(df->dyncfg.payload))
251 dyncfg_echo_update(item, df, id);
252 }
253 else if(df->type == DYNCFG_TYPE_TEMPLATE && (df->cmds & DYNCFG_CMD_ADD)) {
254 STRING *template = string_strdupz(id);
255
256 size_t len = strlen(id);
257 DYNCFG *df_job;
258 dfe_start_reentrant(dyncfg_globals.nodes, df_job) {
259 const char *id_template = df_job_dfe.name;
260 if(df_job->type == DYNCFG_TYPE_JOB && // it is a job
261 df_job->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && // it is dynamically configured
262 df_job->template == template && // it has the same template name
263 strncmp(id_template, id, len) == 0 && // the template name matches (redundant)
264 id_template[len] == ':' && // immediately after the template there is ':'
265 id_template[len + 1]) { // and there is something else after the ':'
266 dyncfg_echo_add(item, df_job_dfe.item, df, df_job, id, &id_template[len + 1]);
267 }
268 }
269 dfe_done(df_job);
270
271 string_freez(template);
272 }
273
274 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
275 }
276
277 bool dyncfg_is_user_disabled(const char *id) {
278 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
279 if(!item)
280 return false;
281
282 DYNCFG *df = dictionary_acquired_item_value(item);
283 bool ret = df->dyncfg.user_disabled;
284 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
285 return ret;
286 }
287
288 bool dyncfg_job_has_registered_template(const char *id) {
289 CLEAN_CHAR_P *buf = strdupz(id);
290 char *colon = strrchr(buf, ':');
291 if(!colon)
292 return false;
293
294 *colon = '\0';
295 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, buf);
296 if(!item)
297 return false;
298
299 DYNCFG *df = dictionary_acquired_item_value(item);
300 bool ret = df->type == DYNCFG_TYPE_TEMPLATE;
301
302 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
303 return ret;
304 }
305
306 DYNCFG_CMDS dyncfg_sanitize_cmds(DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, DYNCFG_CMDS cmds) {
307 // all configurations support schema
308 cmds |= DYNCFG_CMD_SCHEMA;
309
310 // if there is either enable or disable, both are supported
311 if(cmds & (DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE))
312 cmds |= DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE;
313
314 // add
315 if(type == DYNCFG_TYPE_TEMPLATE) {
316 // templates must always support "add"
317 cmds |= DYNCFG_CMD_ADD;
318 }
319 else {
320 // only templates can have "add"
321 cmds &= ~DYNCFG_CMD_ADD;
322 }
323
324 // remove
325 if(source_type == DYNCFG_SOURCE_TYPE_DYNCFG && type == DYNCFG_TYPE_JOB) {
326 // dyncfg jobs must always be removable
327 cmds |= DYNCFG_CMD_REMOVE;
328 }
329 else {
330 // remove is only available for dyncfg jobs
331 cmds &= ~DYNCFG_CMD_REMOVE;
332 }
333
334 // data
335 if(type == DYNCFG_TYPE_TEMPLATE) {
336 // templates do not have data
337 cmds &= ~(DYNCFG_CMD_GET | DYNCFG_CMD_UPDATE);
338 }
339
340 return cmds;
341 }
342
343 bool dyncfg_add_low_level(RRDHOST *host, const char *id, const char *path,
344 DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, const char *source,
345 DYNCFG_CMDS cmds, usec_t created_ut, usec_t modified_ut, bool sync,
346 HTTP_ACCESS view_access, HTTP_ACCESS edit_access,
347 rrd_function_execute_cb_t execute_cb, void *execute_cb_data) {
348
349 if(view_access == HTTP_ACCESS_NONE)
350 view_access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_VIEW_AGENT_CONFIG;
351
352 if(edit_access == HTTP_ACCESS_NONE)
353 edit_access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_EDIT_AGENT_CONFIG | HTTP_ACCESS_COMMERCIAL_SPACE;
354
355 if(!dyncfg_is_valid_id(id)) {
356 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
357 return false;
358 }
359
360 if(type == DYNCFG_TYPE_JOB && !dyncfg_job_has_registered_template(id)) {
361 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: job id '%s' does not have a registered template. Ignoring dynamic configuration for it.", id);
362 return false;
363 }
364
365 DYNCFG_CMDS old_cmds = cmds;
366 cmds = dyncfg_sanitize_cmds(type, source_type, cmds);
367
368 if(cmds != old_cmds) {
369 CLEAN_BUFFER *t = buffer_create(1024, NULL);
370 buffer_sprintf(t, "DYNCFG: id '%s' was declared with cmds: ", id);
371 dyncfg_cmds2buffer(old_cmds, t);
372 buffer_strcat(t, ", but they have sanitized to: ");
373 dyncfg_cmds2buffer(cmds, t);
374 nd_log(NDLS_DAEMON, NDLP_NOTICE, "%s", buffer_tostring(t));
375 }
376
377 const DICTIONARY_ITEM *item = dyncfg_add_internal(host, id, path, status, type, source_type, source, cmds,
378 created_ut, modified_ut, sync, view_access, edit_access,
379 execute_cb, execute_cb_data, true);
380 DYNCFG *df = dictionary_acquired_item_value(item);
381
382 // if(df->source_type == DYNCFG_SOURCE_TYPE_DYNCFG && !df->saves)
383 // nd_log(NDLS_DAEMON, NDLP_WARNING, "DYNCFG: configuration '%s' is created with source type dyncfg, but we don't have a saved configuration for it", id);
384
385 rrd_collector_started();
386 rrd_function_add(
387 host,
388 NULL,
389 string2str(df->function),
390 120,
391 1000,
392 DYNCFG_FUNCTIONS_VERSION,
393 "Dynamic configuration",
394 "config",
395 (view_access & edit_access),
396 sync,
397 dyncfg_function_intercept_cb,
398 NULL);
399
400 if(df->type != DYNCFG_TYPE_TEMPLATE && (df->cmds & (DYNCFG_CMD_ENABLE|DYNCFG_CMD_DISABLE))) {
401 DYNCFG_CMDS status_to_send_to_plugin =
402 (df->dyncfg.user_disabled || df->current.status == DYNCFG_STATUS_DISABLED) ? DYNCFG_CMD_DISABLE : DYNCFG_CMD_ENABLE;
403
404 if (status_to_send_to_plugin == DYNCFG_CMD_ENABLE && dyncfg_is_user_disabled(string2str(df->template)))
405 status_to_send_to_plugin = DYNCFG_CMD_DISABLE;
406
407 dyncfg_echo(item, df, id, status_to_send_to_plugin);
408 }
409
410 if(!(df->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && df->type == DYNCFG_TYPE_JOB))
411 dyncfg_send_updates(id);
412
413 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
414
415 return true;
416 }
417
418 void dyncfg_del_low_level(RRDHOST *host, const char *id) {
419 if(!dyncfg_is_valid_id(id)) {
420 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
421 return;
422 }
423
424 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
425 if(item) {
426 DYNCFG *df = dictionary_acquired_item_value(item);
427 rrd_function_del(host, NULL, string2str(df->function));
428
429 bool garbage_collect = false;
430 if(df->dyncfg.saves == 0) {
431 dictionary_del(dyncfg_globals.nodes, id);
432 garbage_collect = true;
433 }
434
435 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
436
437 if(garbage_collect)
438 dictionary_garbage_collect(dyncfg_globals.nodes);
439 }
440 }
441
442 void dyncfg_status_low_level(RRDHOST *host __maybe_unused, const char *id, DYNCFG_STATUS status) {
443 if(!dyncfg_is_valid_id(id)) {
444 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
445 return;
446 }
447
448 if(status == DYNCFG_STATUS_NONE) {
449 nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: status provided to id '%s' is invalid. Ignoring it.", id);
450 return;
451 }
452
453 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
454 if(item) {
455 DYNCFG *df = dictionary_acquired_item_value(item);
456 df->current.status = status;
457 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
458 }
459 }
460
461 // ----------------------------------------------------------------------------
462
463 void dyncfg_add_streaming(BUFFER *wb) {
464 // when sending config functions to parents, we send only 1 function called 'config';
465 // the parent will send the command to the child, and the child will validate it;
466 // this way the parent does not need to receive removals of config functions;
467
468 buffer_sprintf(wb
469 , PLUGINSD_KEYWORD_FUNCTION " GLOBAL " PLUGINSD_FUNCTION_CONFIG " %d \"%s\" \"%s\" "HTTP_ACCESS_FORMAT" %d\n"
470 , 120
471 , "Dynamic configuration"
472 , "config"
473 , (unsigned)HTTP_ACCESS_ANONYMOUS_DATA
474 , 1000
475 );
476 }
477
478 bool dyncfg_available_for_rrdhost(RRDHOST *host) {
479 if(host == localhost)
480 return true;
481
482 return rrd_function_available(host, PLUGINSD_FUNCTION_CONFIG);
483 }
484
485 // ----------------------------------------------------------------------------