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;