@cryptotaxi247 / netdata-1 / commits / faecca9e5

DYNCFG: add userconfig action (#17692)

* add useconfig action to dyncfg * propage name from parent to child for action test and userconfig * cross compatibility fix for not stripping the name twice * add application/yaml * return raw payload

Costa Tsaousis committed May 17, 2024 at 20:25 UTC faecca9e51cb23904b3da69cd061d6dc8226f33b
10 files changed +232 -14
src/daemon/config/dyncfg-intercept.c
+17 -7
@@ -211,7 +211,6 @@ int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __
211 int rc = HTTP_RESP_INTERNAL_SERVER_ERROR;
212 DYNCFG_CMDS cmd;
213 const DICTIONARY_ITEM *item = NULL;
214 - const char *add_name = NULL;
214
215 char buf[strlen(rfe->function) + 1];
216 memcpy(buf, rfe->function, sizeof(buf));
@@ -223,6 +222,7 @@ int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __
222 char *config = get_word(words, num_words, i++);
223 char *id = get_word(words, num_words, i++);
224 char *cmd_str = get_word(words, num_words, i++);
225 + char *add_name = get_word(words, num_words, i++);
226
227 if(!config || !*config || strcmp(config, PLUGINSD_FUNCTION_CONFIG) != 0)
228 return dyncfg_intercept_early_error(
@@ -235,8 +235,17 @@ int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __
235 rfe, HTTP_RESP_BAD_REQUEST,
236 "dyncfg functions intercept: invalid command received");
237
238 - if(cmd == DYNCFG_CMD_ADD) {
239 - add_name = get_word(words, num_words, i++);
238 + if(cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) {
239 + if(cmd == DYNCFG_CMD_TEST && (!add_name || !*add_name)) {
240 + // backwards compatibility for TEST without a name
241 + char *colon = strrchr(id, ':');
242 + if(colon) {
243 + *colon = '\0';
244 + add_name = ++colon;
245 + }
246 + else
247 + add_name = "test";
248 + }
249
250 if(!add_name || !*add_name)
251 return dyncfg_intercept_early_error(
@@ -247,26 +256,26 @@ int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __
256 char nid[strlen(id) + strlen(add_name) + 2];
257 snprintfz(nid, sizeof(nid), "%s:%s", id, add_name);
258
250 - if (dictionary_get(dyncfg_globals.nodes, nid))
259 + if (cmd == DYNCFG_CMD_ADD && dictionary_get(dyncfg_globals.nodes, nid))
260 return dyncfg_intercept_early_error(
261 rfe, HTTP_RESP_BAD_REQUEST,
262 "dyncfg functions intercept: a configuration with this name already exists");
263 }
264 }
265
257 - if((cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_UPDATE || cmd == DYNCFG_CMD_TEST) && !has_payload)
266 + if((cmd == DYNCFG_CMD_ADD || cmd == DYNCFG_CMD_UPDATE || cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) && !has_payload)
267 return dyncfg_intercept_early_error(
268 rfe, HTTP_RESP_BAD_REQUEST,
269 "dyncfg functions intercept: this action requires a payload");
270
262 - if((cmd != DYNCFG_CMD_ADD && cmd != DYNCFG_CMD_UPDATE && cmd != DYNCFG_CMD_TEST) && has_payload)
271 + if((cmd != DYNCFG_CMD_ADD && cmd != DYNCFG_CMD_UPDATE && cmd != DYNCFG_CMD_TEST && cmd != DYNCFG_CMD_USERCONFIG) && has_payload)
272 return dyncfg_intercept_early_error(
273 rfe, HTTP_RESP_BAD_REQUEST,
274 "dyncfg functions intercept: this action does not require a payload");
275
276 item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
277 if(!item) {
269 - if(cmd == DYNCFG_CMD_TEST) {
278 + if(cmd == DYNCFG_CMD_TEST || cmd == DYNCFG_CMD_USERCONFIG) {
279 // this may be a test on a new job
280 item = dyncfg_get_template_of_new_job(id);
281 }
@@ -284,6 +293,7 @@ int dyncfg_function_intercept_cb(struct rrd_function_execute *rfe, void *data __
293 switch(cmd) {
294 case DYNCFG_CMD_GET:
295 case DYNCFG_CMD_SCHEMA:
296 + case DYNCFG_CMD_USERCONFIG:
297 if(!http_access_user_has_enough_access_level_for_endpoint(rfe->user_access, df->view_access)) {
298 make_the_call_to_plugin = false;
299 rc = dyncfg_default_response(
src/daemon/config/dyncfg-tree.c
+11 -3
@@ -200,15 +200,23 @@ static int dyncfg_config_execute_cb(struct rrd_function_execute *rfe, void *data
200 dyncfg_tree_for_host(host, rfe->result.wb, path, id);
201 }
202 else {
203 + const char *name = id;
204 id = action;
205 action = path;
206 path = NULL;
207
208 DYNCFG_CMDS cmd = dyncfg_cmds2id(action);
209 const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
209 - if(!item)
210 + if(!item) {
211 item = dyncfg_get_template_of_new_job(id);
212
213 + if(item && (!name || !*name)) {
214 + const char *n = dictionary_acquired_item_name(item);
215 + if(strncmp(id, n, strlen(n)) == 0 && id[strlen(n)] == ':')
216 + name = &id[strlen(n) + 1];
217 + }
218 + }
219 +
220 if(item) {
221 DYNCFG *df = dictionary_acquired_item_value(item);
222
@@ -234,10 +242,10 @@ static int dyncfg_config_execute_cb(struct rrd_function_execute *rfe, void *data
242 goto cleanup;
243 }
244 }
237 - else if(cmd == DYNCFG_CMD_TEST && df->type == DYNCFG_TYPE_TEMPLATE && df->current.status != DYNCFG_STATUS_ORPHAN) {
245 + else if((cmd == DYNCFG_CMD_USERCONFIG || cmd == DYNCFG_CMD_TEST) && df->current.status != DYNCFG_STATUS_ORPHAN) {
246 const char *old_rfe_function = rfe->function;
247 char buf2[2048];
240 - snprintfz(buf2, sizeof(buf2), "config %s %s", dictionary_acquired_item_name(item), action);
248 + snprintfz(buf2, sizeof(buf2), "config %s %s %s", dictionary_acquired_item_name(item), action, name?name:"");
249 rfe->function = buf2;
250 dictionary_acquired_item_release(dyncfg_globals.nodes, item);
251 item = NULL;
src/health/health_dyncfg.c
+168 -2
@@ -370,6 +370,148 @@ void health_prototype_to_json(BUFFER *wb, RRD_ALERT_PROTOTYPE *ap, bool for_hash
370
371 // ---------------------------------------------------------------------------------------------------------------------
372
373 +static inline void dyncfg_user_config_print_duration(BUFFER *wb, const char *prefix, int seconds) {
374 + if((seconds % 3600) == 0)
375 + buffer_sprintf(wb, "%s%dh", prefix?prefix:"", seconds / 3600);
376 + else if((seconds % 60) == 0)
377 + buffer_sprintf(wb, "%s%dm", prefix?prefix:"", seconds / 60);
378 + else
379 + buffer_sprintf(wb, "%s%ds", prefix?prefix:"", seconds);
380 +}
381 +
382 +int dyncfg_health_prototype_to_conf(BUFFER *wb, RRD_ALERT_PROTOTYPE *ap, const char *name) {
383 + buffer_flush(wb);
384 + wb->content_type = CT_TEXT_PLAIN;
385 + wb->expires = now_realtime_sec();
386 +
387 + for(RRD_ALERT_PROTOTYPE *nap = ap; nap ; nap = nap->_internal.next) {
388 + if(nap->match.is_template) {
389 + buffer_sprintf(wb, "\n%13s: %s\n", "template", name);
390 + buffer_sprintf(wb, "%13s: %s\n", "on", string2str(nap->match.on.context));
391 + }
392 + else {
393 + buffer_sprintf(wb, "\n%13s: %s\n", "alarm", name);
394 + buffer_sprintf(wb, "%13s: %s\n", "on", string2str(nap->match.on.chart));
395 + }
396 +
397 + if(nap->config.classification)
398 + buffer_sprintf(wb, "%13s: %s\n", "class", string2str(nap->config.classification));
399 +
400 + if(nap->config.type)
401 + buffer_sprintf(wb, "%13s: %s\n", "type", string2str(nap->config.type));
402 +
403 + if(nap->config.component)
404 + buffer_sprintf(wb, "%13s: %s\n", "component", string2str(nap->config.component));
405 +
406 + if(nap->match.host_labels)
407 + buffer_sprintf(wb, "%13s: %s\n", "host labels", string2str(nap->match.host_labels));
408 +
409 + if(nap->match.chart_labels)
410 + buffer_sprintf(wb, "%13s: %s\n", "chart labels", string2str(nap->match.chart_labels));
411 +
412 + if(nap->config.after) {
413 + buffer_sprintf(wb, "%13s: %s", "lookup", time_grouping_tostring(nap->config.time_group));
414 + switch(nap->config.time_group) {
415 + case RRDR_GROUPING_PERCENTILE:
416 + case RRDR_GROUPING_TRIMMED_MEAN:
417 + case RRDR_GROUPING_TRIMMED_MEDIAN:
418 + buffer_sprintf(wb, "(%0.2f)", nap->config.time_group_value);
419 + break;
420 +
421 + case RRDR_GROUPING_COUNTIF:
422 + buffer_sprintf(wb, "(%s%0.2f)", alerts_group_conditions_id2txt(nap->config.time_group_condition), nap->config.time_group_value);
423 + break;
424 +
425 + default:
426 + break;
427 + }
428 +
429 + dyncfg_user_config_print_duration(wb, " ", nap->config.after);
430 +
431 + if(nap->config.before)
432 + dyncfg_user_config_print_duration(wb, " at ", nap->config.before);
433 +
434 + if(nap->config.options) {
435 + buffer_strcat(wb, " ");
436 + rrdr_options_to_buffer(wb, nap->config.options);
437 + }
438 +
439 + if(nap->config.dimensions)
440 + buffer_sprintf(wb, " of %s", string2str(nap->config.dimensions));
441 +
442 + buffer_strcat(wb, "\n");
443 + }
444 +
445 + if(nap->config.calculation)
446 + buffer_sprintf(wb, "%13s: %s\n", "calc", expression_source(nap->config.calculation));
447 +
448 + if(nap->config.units)
449 + buffer_sprintf(wb, "%13s: %s\n", "units", string2str(nap->config.units));
450 +
451 + if(nap->config.update_every) {
452 + buffer_sprintf(wb, "%13s: ", "every");
453 + dyncfg_user_config_print_duration(wb, NULL, nap->config.update_every);
454 + buffer_strcat(wb, "\n");
455 + }
456 +
457 + if(nap->config.warning)
458 + buffer_sprintf(wb, "%13s: %s\n", "warn", expression_source(nap->config.warning));
459 +
460 + if(nap->config.critical)
461 + buffer_sprintf(wb, "%13s: %s\n", "crit", expression_source(nap->config.critical));
462 +
463 + if(nap->config.delay_up_duration || nap->config.delay_down_duration) {
464 + buffer_sprintf(wb, "%13s:", "delay");
465 +
466 + if(nap->config.delay_up_duration)
467 + dyncfg_user_config_print_duration(wb, " up ", nap->config.delay_up_duration);
468 +
469 + if(nap->config.delay_down_duration)
470 + dyncfg_user_config_print_duration(wb, " down ", nap->config.delay_down_duration);
471 +
472 + if(nap->config.delay_multiplier)
473 + buffer_sprintf(wb, " multiplier %0.2f", nap->config.delay_multiplier);
474 +
475 + if(nap->config.delay_max_duration)
476 + dyncfg_user_config_print_duration(wb, " max ", nap->config.delay_max_duration);
477 +
478 + buffer_strcat(wb, "\n");
479 + }
480 +
481 + if(nap->config.alert_action_options) {
482 + buffer_sprintf(wb, "%13s:", "options");
483 + alert_action_options_to_buffer(wb, nap->config.alert_action_options);
484 + buffer_strcat(wb, "\n");
485 + }
486 +
487 + if(nap->config.has_custom_repeat_config) {
488 + if(!nap->config.crit_repeat_every && !nap->config.warn_repeat_every)
489 + buffer_sprintf(wb, "%13s: off\n", "repeat");
490 + else {
491 + dyncfg_user_config_print_duration(wb, " warning ", (int)nap->config.warn_repeat_every);
492 + dyncfg_user_config_print_duration(wb, " critical ", (int)nap->config.crit_repeat_every);
493 + buffer_strcat(wb, "\n");
494 + }
495 + }
496 +
497 + if(nap->config.summary)
498 + buffer_sprintf(wb, "%13s: %s\n", "summary", string2str(nap->config.summary));
499 +
500 + if(nap->config.info)
501 + buffer_sprintf(wb, "%13s: %s\n", "info", string2str(nap->config.info));
502 +
503 + if(nap->config.exec && nap->config.exec != localhost->health.health_default_exec)
504 + buffer_sprintf(wb, "%13s: %s\n", "exec", string2str(nap->config.exec));
505 +
506 + if(nap->config.recipient)
507 + buffer_sprintf(wb, "%13s: %s\n", "to", string2str(nap->config.recipient));
508 + }
509 +
510 + return 200;
511 +}
512 +
513 +// ---------------------------------------------------------------------------------------------------------------------
514 +
515 static size_t dyncfg_health_remove_all_rrdcalc_of_prototype(STRING *alert_name) {
516 size_t removed = 0;
517
@@ -430,6 +572,18 @@ static int dyncfg_health_prototype_template_action(BUFFER *result, DYNCFG_CMDS c
572 }
573 break;
574
575 + case DYNCFG_CMD_USERCONFIG: {
576 + CLEAN_BUFFER *error = buffer_create(0, NULL);
577 + RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, add_name);
578 + if(!nap)
579 + code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
580 + else {
581 + code = dyncfg_health_prototype_to_conf(result, nap, add_name);
582 + health_prototype_free(nap);
583 + }
584 + }
585 + break;
586 +
587 case DYNCFG_CMD_SCHEMA:
588 code = dyncfg_default_response(result, HTTP_RESP_NOT_IMPLEMENTED, "schema not implemented yet for prototype templates");
589 break;
@@ -534,6 +688,18 @@ static int dyncfg_health_prototype_job_action(BUFFER *result, DYNCFG_CMDS cmd, B
688 }
689 break;
690
691 + case DYNCFG_CMD_USERCONFIG: {
692 + CLEAN_BUFFER *error = buffer_create(0, NULL);
693 + RRD_ALERT_PROTOTYPE *nap = health_prototype_payload_parse(buffer_tostring(payload), buffer_strlen(payload), error, alert_name);
694 + if(!nap)
695 + code = dyncfg_default_response(result, HTTP_RESP_BAD_REQUEST, buffer_tostring(error));
696 + else {
697 + code = dyncfg_health_prototype_to_conf(result, nap, alert_name);
698 + health_prototype_free(nap);
699 + }
700 + }
701 + break;
702 +
703 case DYNCFG_CMD_REMOVE:
704 dyncfg_health_remove_all_rrdcalc_of_prototype(ap->config.name);
705 dictionary_del(health_globals.prototypes.dict, dictionary_acquired_item_name(item));
@@ -621,7 +787,7 @@ static void health_dyncfg_register_prototype(RRD_ALERT_PROTOTYPE *ap) {
787 ap->_internal.enabled ? DYNCFG_STATUS_ACCEPTED : DYNCFG_STATUS_DISABLED, DYNCFG_TYPE_JOB,
788 ap->config.source_type, string2str(ap->config.source),
789 DYNCFG_CMD_SCHEMA | DYNCFG_CMD_GET | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE |
624 - DYNCFG_CMD_UPDATE |
790 + DYNCFG_CMD_UPDATE | DYNCFG_CMD_USERCONFIG |
791 (ap->config.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && !ap->_internal.is_on_disk ? DYNCFG_CMD_REMOVE : 0),
792 HTTP_ACCESS_NONE,
793 HTTP_ACCESS_NONE,
@@ -654,7 +820,7 @@ void health_dyncfg_register_all_prototypes(void) {
820 DYNCFG_HEALTH_ALERT_PROTOTYPE_PREFIX, "/health/alerts/prototypes",
821 DYNCFG_STATUS_ACCEPTED, DYNCFG_TYPE_TEMPLATE,
822 DYNCFG_SOURCE_TYPE_INTERNAL, "internal",
657 - DYNCFG_CMD_SCHEMA | DYNCFG_CMD_ADD | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE,
823 + DYNCFG_CMD_SCHEMA | DYNCFG_CMD_ADD | DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE | DYNCFG_CMD_USERCONFIG,
824 HTTP_ACCESS_NONE,
825 HTTP_ACCESS_NONE,
826 dyncfg_health_cb, NULL);
src/health/health_internals.h
+1
@@ -44,6 +44,7 @@
44 #define HEALTH_CHART_LABEL_KEY "chart labels"
45
46 void alert_action_options_to_buffer_json_array(BUFFER *wb, const char *key, ALERT_ACTION_OPTIONS options);
47 +void alert_action_options_to_buffer(BUFFER *wb, ALERT_ACTION_OPTIONS options);
48 ALERT_ACTION_OPTIONS alert_action_options_parse(char *o);
49 ALERT_ACTION_OPTIONS alert_action_options_parse_one(const char *o);
50
src/health/health_prototypes.c
+15
@@ -176,6 +176,21 @@ void alert_action_options_to_buffer_json_array(BUFFER *wb, const char *key, ALER
176 buffer_json_array_close(wb);
177 }
178
179 +void alert_action_options_to_buffer(BUFFER *wb, ALERT_ACTION_OPTIONS options) {
180 + RRDR_OPTIONS used = 0; // to prevent adding duplicates
181 + for(int i = 0; alert_action_options[i].name ; i++) {
182 + if (unlikely((alert_action_options[i].value & options) && !(alert_action_options[i].value & used))) {
183 + if(used != 0)
184 + buffer_strcat(wb, " ");
185 +
186 + const char *name = alert_action_options[i].name;
187 + used |= alert_action_options[i].value;
188 +
189 + buffer_strcat(wb, name);
190 + }
191 + }
192 +}
193 +
194 static void alert_action_options_init(void) {
195 for(int i = 0; alert_action_options[i].name ; i++)
196 alert_action_options[i].hash = simple_hash(alert_action_options[i].name);
src/libnetdata/config/dyncfg.c
+2 -1
@@ -124,7 +124,8 @@ static struct {
124 { .cmd = DYNCFG_CMD_REMOVE, .name = "remove" },
125 { .cmd = DYNCFG_CMD_ENABLE, .name = "enable" },
126 { .cmd = DYNCFG_CMD_DISABLE, .name = "disable" },
127 - { .cmd = DYNCFG_CMD_RESTART, .name = "restart" }
127 + { .cmd = DYNCFG_CMD_RESTART, .name = "restart" },
128 + { .cmd = DYNCFG_CMD_USERCONFIG, .name = "userconfig" },
129 };
130
131 const char *dyncfg_id2cmd_one(DYNCFG_CMDS cmd) {
src/libnetdata/config/dyncfg.h
+2
@@ -52,7 +52,9 @@ typedef enum __attribute__((packed)) {
52 DYNCFG_CMD_ENABLE = (1 << 6),
53 DYNCFG_CMD_DISABLE = (1 << 7),
54 DYNCFG_CMD_RESTART = (1 << 8),
55 + DYNCFG_CMD_USERCONFIG = (1 << 9),
56 } DYNCFG_CMDS;
57 +
58 DYNCFG_CMDS dyncfg_cmds2id(const char *cmds);
59 void dyncfg_cmds2buffer(DYNCFG_CMDS cmds, struct web_buffer *wb);
60 void dyncfg_cmds2json_array(DYNCFG_CMDS cmds, const char *key, struct web_buffer *wb);
src/libnetdata/http/content_type.c
+1
@@ -14,6 +14,7 @@ static struct {
14 { .format = "text/plain", CT_TEXT_PLAIN, true },
15 { .format = "text/css", CT_TEXT_CSS, true },
16 { .format = "text/yaml", CT_TEXT_YAML, true },
17 + { .format = "application/yaml", CT_APPLICATION_YAML, true },
18 { .format = "text/xml", CT_TEXT_XML, true },
19 { .format = "text/xsl", CT_TEXT_XSL, true },
20 { .format = "application/json", CT_APPLICATION_JSON, true },
src/libnetdata/http/content_type.h
+1
@@ -33,6 +33,7 @@ typedef enum __attribute__ ((__packed__)) {
33 CT_APPLICATION_PDF,
34 CT_APPLICATION_ZIP,
35 CT_TEXT_YAML,
36 + CT_APPLICATION_YAML,
37 } HTTP_CONTENT_TYPE;
38
39 HTTP_CONTENT_TYPE content_type_string2id(const char *format);
src/web/api/web_api_v1.c
+14 -1
@@ -1581,11 +1581,24 @@ static int web_client_api_request_v1_config(RRDHOST *host, struct web_client *w,
1581 rrd_call_function_error(w->response.data, "invalid id given", HTTP_RESP_BAD_REQUEST);
1582 return HTTP_RESP_BAD_REQUEST;
1583 }
1584 +
1585 if(c == DYNCFG_CMD_NONE) {
1586 rrd_call_function_error(w->response.data, "invalid action given", HTTP_RESP_BAD_REQUEST);
1587 return HTTP_RESP_BAD_REQUEST;
1588 }
1588 - else if(c == DYNCFG_CMD_ADD) {
1589 +
1590 + if(c == DYNCFG_CMD_ADD || c == DYNCFG_CMD_USERCONFIG || c == DYNCFG_CMD_TEST) {
1591 + if(c == DYNCFG_CMD_TEST && (!add_name || !*add_name)) {
1592 + // backwards compatibility for TEST without a name
1593 + char *colon = strrchr(id, ':');
1594 + if(colon) {
1595 + *colon = '\0';
1596 + add_name = ++colon;
1597 + }
1598 + else
1599 + add_name = "test";
1600 + }
1601 +
1602 if(!add_name || !*add_name || !dyncfg_is_valid_id(add_name)) {
1603 rrd_call_function_error(w->response.data, "invalid name given", HTTP_RESP_BAD_REQUEST);
1604 return HTTP_RESP_BAD_REQUEST;