@cryptotaxi247 / netdata-1 / commits / 6dfc99a2e

Dyncfg add streaming support (#15791)

* dyncfg fncnames as constants * add helper macros to know parser streaming/plugin * plugins dictionary per RRDHOST * api_request_v2_config add support for /host/ * streamify pluginsd_register_plugin * streamify pluginsd_register_module * streamify report_job_status * streamify dyncfg get functions * module_type2str * add job type and flags * add DYNCFG_REGISTER_JOB * implement register job * push all to parent at startup * add helper function is_dyncfg_function * forward virtual functions trough streaming * separate job2json * add api/v2/job_statuses * do cleanup on streaming * streamify set functions * support FUNCTION_PAYLOAD trough streaming * WIP tests * dont attempt loading non-localhost configs * move cfg persistence to proper place * prevent race * properly update job state at runtime * cleanup 1 * job2json add missing reason * add tests * correct HTTP code * add test * streamify delete_job_cb * add DELETE_JOB keyword * job delete over streaming * add tests for create and delete job over parent * rrdpush common checks to macro * add missing forwarders * fix jobs according to test results * more tests * review comment 1 * codacy remove valid warning * codacy ruby fixes * fix wrong rc check * minimal test plugin for child * add test * dict walk insted of master lock * minor - english spelling fixes * thiago comments 1 * minor - rename folder to dynconf * enable only when built with -DNETDATA_TEST_DYNCFG * minor - compiler warning * create dir post daemonization * stricter URL check

Timotej S committed Sep 29, 2023 at 17:13 UTC 6dfc99a2e0cf893c9ac2fd56d7986982738a9579
20 files changed +2033 -345
collectors/plugins.d/gperf-config.txt
+13 -11
@@ -36,20 +36,22 @@ SET, 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_
36 VARIABLE, 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19
37 DYNCFG_ENABLE, 101, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20
38 DYNCFG_REGISTER_MODULE, 102, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21
39 -REPORT_JOB_STATUS, 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22
39 +DYNCFG_REGISTER_JOB, 103, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22
40 +REPORT_JOB_STATUS, 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23
41 +DELETE_JOB, 111, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24
42 #
43 # Streaming only keywords
44 #
43 -CLAIMED_ID, 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23
44 -BEGIN2, 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24
45 -SET2, 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25
46 -END2, 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26
45 +CLAIMED_ID, 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25
46 +BEGIN2, 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26
47 +SET2, 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27
48 +END2, 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28
49 #
50 # Streaming Replication keywords
51 #
50 -CHART_DEFINITION_END, 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27
51 -RBEGIN, 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28
52 -RDSTATE, 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29
53 -REND, 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30
54 -RSET, 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31
55 -RSSTATE, 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32
52 +CHART_DEFINITION_END, 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29
53 +RBEGIN, 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30
54 +RDSTATE, 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31
55 +REND, 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32
56 +RSET, 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 33
57 +RSSTATE, 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 34
collectors/plugins.d/gperf-hashtable.h
+74 -72
@@ -30,12 +30,12 @@
30 #endif
31
32
33 -#define GPERF_PARSER_TOTAL_KEYWORDS 32
33 +#define GPERF_PARSER_TOTAL_KEYWORDS 34
34 #define GPERF_PARSER_MIN_WORD_LENGTH 3
35 #define GPERF_PARSER_MAX_WORD_LENGTH 22
36 #define GPERF_PARSER_MIN_HASH_VALUE 3
37 -#define GPERF_PARSER_MAX_HASH_VALUE 41
38 -/* maximum key range = 39, duplicates = 0 */
37 +#define GPERF_PARSER_MAX_HASH_VALUE 36
38 +/* maximum key range = 34, duplicates = 0 */
39
40 #ifdef __GNUC__
41 __inline
@@ -49,32 +49,32 @@ gperf_keyword_hash_function (register const char *str, register size_t len)
49 {
50 static unsigned char asso_values[] =
51 {
52 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
53 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
54 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
55 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
56 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
57 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
58 - 42, 42, 42, 42, 42, 16, 7, 2, 11, 0,
59 - 8, 42, 3, 9, 42, 42, 9, 42, 0, 2,
60 - 42, 42, 1, 3, 42, 7, 17, 42, 27, 2,
61 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
62 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
63 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
64 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
65 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
66 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
67 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
68 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
69 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
70 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
71 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
72 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
73 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
74 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
75 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
76 - 42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
77 - 42, 42, 42, 42, 42, 42
52 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
53 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
54 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
55 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
56 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
57 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
58 + 37, 37, 37, 37, 37, 12, 28, 5, 2, 0,
59 + 0, 37, 3, 13, 37, 37, 14, 37, 0, 2,
60 + 37, 37, 1, 3, 37, 6, 10, 37, 32, 2,
61 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
62 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
63 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
64 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
65 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
66 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
67 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
68 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
69 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
70 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
71 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
72 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
73 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
74 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
75 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
76 + 37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
77 + 37, 37, 37, 37, 37, 37
78 };
79 return len + asso_values[(unsigned char)str[1]] + asso_values[(unsigned char)str[0]];
80 }
@@ -84,70 +84,72 @@ static PARSER_KEYWORD gperf_keywords[] =
84 {(char*)0}, {(char*)0}, {(char*)0},
85 #line 30 "gperf-config.txt"
86 {"END", 13, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 13},
87 -#line 46 "gperf-config.txt"
88 - {"END2", 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26},
89 -#line 53 "gperf-config.txt"
90 - {"REND", 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30},
87 +#line 48 "gperf-config.txt"
88 + {"END2", 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28},
89 +#line 55 "gperf-config.txt"
90 + {"REND", 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32},
91 #line 35 "gperf-config.txt"
92 {"SET", 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 18},
93 -#line 45 "gperf-config.txt"
94 - {"SET2", 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25},
95 -#line 54 "gperf-config.txt"
96 - {"RSET", 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31},
93 +#line 47 "gperf-config.txt"
94 + {"SET2", 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27},
95 +#line 56 "gperf-config.txt"
96 + {"RSET", 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 33},
97 #line 18 "gperf-config.txt"
98 {"HOST", 71, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 4},
99 +#line 54 "gperf-config.txt"
100 + {"RDSTATE", 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31},
101 +#line 57 "gperf-config.txt"
102 + {"RSSTATE", 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 34},
103 +#line 41 "gperf-config.txt"
104 + {"DELETE_JOB", 111, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24},
105 #line 26 "gperf-config.txt"
106 {"CHART", 32, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 9},
101 -#line 55 "gperf-config.txt"
102 - {"RSSTATE", 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32},
103 -#line 25 "gperf-config.txt"
104 - {"BEGIN", 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8},
105 -#line 44 "gperf-config.txt"
106 - {"BEGIN2", 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24},
107 -#line 51 "gperf-config.txt"
108 - {"RBEGIN", 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28},
107 +#line 31 "gperf-config.txt"
108 + {"FUNCTION", 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14},
109 #line 21 "gperf-config.txt"
110 {"HOST_LABEL", 74, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 7},
111 #line 19 "gperf-config.txt"
112 {"HOST_DEFINE", 72, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 5},
113 -#line 27 "gperf-config.txt"
114 - {"CLABEL", 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10},
115 -#line 39 "gperf-config.txt"
116 - {"REPORT_JOB_STATUS", 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22},
117 -#line 52 "gperf-config.txt"
118 - {"RDSTATE", 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29},
119 -#line 20 "gperf-config.txt"
120 - {"HOST_DEFINE_END", 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6},
121 -#line 43 "gperf-config.txt"
122 - {"CLAIMED_ID", 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23},
123 -#line 15 "gperf-config.txt"
124 - {"FLUSH", 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1},
125 -#line 31 "gperf-config.txt"
126 - {"FUNCTION", 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14},
127 -#line 28 "gperf-config.txt"
128 - {"CLABEL_COMMIT", 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11},
129 -#line 50 "gperf-config.txt"
130 - {"CHART_DEFINITION_END", 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27},
113 #line 37 "gperf-config.txt"
114 {"DYNCFG_ENABLE", 101, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20},
133 -#line 16 "gperf-config.txt"
134 - {"DISABLE", 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2},
115 +#line 40 "gperf-config.txt"
116 + {"REPORT_JOB_STATUS", 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23},
117 +#line 15 "gperf-config.txt"
118 + {"FLUSH", 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1},
119 +#line 20 "gperf-config.txt"
120 + {"HOST_DEFINE_END", 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6},
121 #line 34 "gperf-config.txt"
122 {"OVERWRITE", 52, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 17},
123 +#line 16 "gperf-config.txt"
124 + {"DISABLE", 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2},
125 +#line 39 "gperf-config.txt"
126 + {"DYNCFG_REGISTER_JOB", 103, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22},
127 #line 29 "gperf-config.txt"
128 {"DIMENSION", 31, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 12},
139 -#line 33 "gperf-config.txt"
140 - {"LABEL", 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16},
141 -#line 17 "gperf-config.txt"
142 - {"EXIT", 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3},
143 - {(char*)0}, {(char*)0}, {(char*)0},
129 +#line 27 "gperf-config.txt"
130 + {"CLABEL", 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10},
131 #line 38 "gperf-config.txt"
132 {"DYNCFG_REGISTER_MODULE", 102, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21},
133 #line 32 "gperf-config.txt"
134 {"FUNCTION_RESULT_BEGIN", 42, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 15},
148 - {(char*)0}, {(char*)0}, {(char*)0}, {(char*)0},
135 +#line 52 "gperf-config.txt"
136 + {"CHART_DEFINITION_END", 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29},
137 +#line 45 "gperf-config.txt"
138 + {"CLAIMED_ID", 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25},
139 #line 36 "gperf-config.txt"
150 - {"VARIABLE", 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19}
140 + {"VARIABLE", 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19},
141 +#line 33 "gperf-config.txt"
142 + {"LABEL", 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16},
143 +#line 28 "gperf-config.txt"
144 + {"CLABEL_COMMIT", 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11},
145 +#line 25 "gperf-config.txt"
146 + {"BEGIN", 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8},
147 +#line 46 "gperf-config.txt"
148 + {"BEGIN2", 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26},
149 +#line 53 "gperf-config.txt"
150 + {"RBEGIN", 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30},
151 +#line 17 "gperf-config.txt"
152 + {"EXIT", 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3}
153 };
154
155 PARSER_KEYWORD *
collectors/plugins.d/plugins_d.h
+10
@@ -10,6 +10,16 @@
10 #define PLUGINSD_CMD_MAX (FILENAME_MAX*2)
11 #define PLUGINSD_STOCK_PLUGINS_DIRECTORY_PATH 0
12
13 +#define PLUGINSD_KEYWORD_FUNCTION_PAYLOAD "FUNCTION_PAYLOAD"
14 +#define PLUGINSD_KEYWORD_FUNCTION_PAYLOAD_END "FUNCTION_PAYLOAD_END"
15 +
16 +#define PLUGINSD_KEYWORD_DYNCFG_ENABLE "DYNCFG_ENABLE"
17 +#define PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE "DYNCFG_REGISTER_MODULE"
18 +#define PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB "DYNCFG_REGISTER_JOB"
19 +
20 +#define PLUGINSD_KEYWORD_REPORT_JOB_STATUS "REPORT_JOB_STATUS"
21 +#define PLUGINSD_KEYWORD_DELETE_JOB "DELETE_JOB"
22 +
23 #define PLUGINSD_LINE_MAX_SSL_READ 512
24
25 #define PLUGINSD_MAX_WORDS 20
collectors/plugins.d/pluginsd_parser.c
+426 -68
@@ -4,6 +4,9 @@
4
5 #define LOG_FUNCTIONS false
6
7 +#define SERVING_STREAMING(parser) (parser->repertoire == PARSER_INIT_STREAMING)
8 +#define SERVING_PLUGINSD(parser) (parser->repertoire == PARSER_INIT_PLUGINSD)
9 +
10 static ssize_t send_to_plugin(const char *txt, void *data) {
11 PARSER *parser = data;
12
@@ -742,6 +745,7 @@ struct inflight_function {
745 usec_t sent_ut;
746 const char *payload;
747 PARSER *parser;
748 + bool virtual;
749 };
750
751 static void inflight_functions_insert_callback(const DICTIONARY_ITEM *item, void *func, void *parser_ptr) {
@@ -808,16 +812,83 @@ static bool inflight_functions_conflict_callback(const DICTIONARY_ITEM *item __m
812 return false;
813 }
814
811 -static void inflight_functions_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func, void *parser_ptr __maybe_unused) {
815 +void delete_job_finalize(struct parser *parser, struct configurable_plugin *plug, const char *fnc_sig, int code) {
816 + if (code != DYNCFG_VFNC_RET_CFG_ACCEPTED)
817 + return;
818 +
819 + char *params_local = strdupz(fnc_sig);
820 + char *words[DYNCFG_MAX_WORDS];
821 + size_t words_c = quoted_strings_splitter(params_local, words, DYNCFG_MAX_WORDS, isspace_map_pluginsd);
822 +
823 + if (words_c != 3) {
824 + netdata_log_error("PLUGINSD_PARSER: invalid number of parameters for delete_job");
825 + freez(params_local);
826 + return;
827 + }
828 +
829 + const char *module = words[1];
830 + const char *job = words[2];
831 +
832 + delete_job(plug, module, job);
833 +
834 + unlink_job(plug->name, module, job);
835 +
836 + rrdpush_send_job_deleted(localhost, plug->name, module, job);
837 +
838 + freez(params_local);
839 +}
840 +
841 +void set_job_finalize(struct parser *parser, struct configurable_plugin *plug, const char *fnc_sig, int code) {
842 + if (code != DYNCFG_VFNC_RET_CFG_ACCEPTED)
843 + return;
844 +
845 + char *params_local = strdupz(fnc_sig);
846 + char *words[DYNCFG_MAX_WORDS];
847 + size_t words_c = quoted_strings_splitter(params_local, words, DYNCFG_MAX_WORDS, isspace_map_pluginsd);
848 +
849 + if (words_c != 3) {
850 + netdata_log_error("PLUGINSD_PARSER: invalid number of parameters for set_job_config");
851 + freez(params_local);
852 + return;
853 + }
854 +
855 + const char *module_name = get_word(words, words_c, 1);
856 + const char *job_name = get_word(words, words_c, 2);
857 +
858 + if (register_job(parser->user.host->configurable_plugins, parser->user.cd->configuration->name, module_name, job_name, JOB_TYPE_USER, JOB_FLG_USER_CREATED, 1)) {
859 + freez(params_local);
860 + return;
861 + }
862 +
863 + // only send this if it is not existing already (register_job cares for that)
864 + rrdpush_send_dyncfg_reg_job(localhost, parser->user.cd->configuration->name, module_name, job_name, JOB_TYPE_USER, JOB_FLG_USER_CREATED);
865 +
866 + freez(params_local);
867 +}
868 +
869 +static void inflight_functions_delete_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func, void *parser_ptr) {
870 struct inflight_function *pf = func;
871 + struct parser *parser = (struct parser *)parser_ptr;
872
873 internal_error(LOG_FUNCTIONS,
874 "FUNCTION '%s' result of transaction '%s' received from collector (%zu bytes, request %"PRIu64" usec, response %"PRIu64" usec)",
875 string2str(pf->function), dictionary_acquired_item_name(item),
876 buffer_strlen(pf->result_body_wb), pf->sent_ut - pf->started_ut, now_realtime_usec() - pf->sent_ut);
877
878 + if (pf->virtual && SERVING_PLUGINSD(parser)) {
879 + if (pf->payload) {
880 + if (strncmp(string2str(pf->function), FUNCTION_NAME_SET_JOB_CONFIG, strlen(FUNCTION_NAME_SET_JOB_CONFIG)) == 0)
881 + set_job_finalize(parser, parser->user.cd->configuration, string2str(pf->function), pf->code);
882 + dyn_conf_store_config(string2str(pf->function), pf->payload, parser->user.cd->configuration);
883 + } else if (strncmp(string2str(pf->function), FUNCTION_NAME_DELETE_JOB, strlen(FUNCTION_NAME_DELETE_JOB)) == 0) {
884 + delete_job_finalize(parser, parser->user.cd->configuration, string2str(pf->function), pf->code);
885 + }
886 + }
887 +
888 pf->result_cb(pf->result_body_wb, pf->code, pf->result_cb_data);
889 +
890 string_freez(pf->function);
891 + freez(pf->payload);
892 }
893
894 void inflight_functions_init(PARSER *parser) {
@@ -1947,9 +2018,81 @@ static void virt_fnc_got_data_cb(BUFFER *wb __maybe_unused, int code, void *call
2018 }
2019
2020 #define VIRT_FNC_TIMEOUT 1
2021 +#define VIRT_FNC_BUF_SIZE (4096)
2022 +void call_virtual_function_async(BUFFER *wb, RRDHOST *host, const char *name, const char *payload, rrd_function_result_callback_t callback, void *callback_data) {
2023 + PARSER *parser = NULL;
2024 +
2025 + //TODO simplify (as we really need only first parameter to get plugin name maybe we can avoid parsing all)
2026 + char *words[PLUGINSD_MAX_WORDS];
2027 + char *function_with_params = strdupz(name);
2028 + size_t num_words = quoted_strings_splitter(function_with_params, words, PLUGINSD_MAX_WORDS, isspace_map_pluginsd);
2029 +
2030 + if (num_words < 2) {
2031 + netdata_log_error("PLUGINSD: virtual function name is empty.");
2032 + freez(function_with_params);
2033 + return;
2034 + }
2035 +
2036 + const DICTIONARY_ITEM *cpi = dictionary_get_and_acquire_item(host->configurable_plugins, get_word(words, num_words, 1));
2037 + if (unlikely(cpi == NULL)) {
2038 + netdata_log_error("PLUGINSD: virtual function plugin '%s' not found.", name);
2039 + freez(function_with_params);
2040 + return;
2041 + }
2042 + struct configurable_plugin *cp = dictionary_acquired_item_value(cpi);
2043 + parser = (PARSER *)cp->cb_usr_ctx;
2044 +
2045 + BUFFER *function_out = buffer_create(VIRT_FNC_BUF_SIZE, NULL);
2046 + // if we are forwarding this to a plugin (as opposed to streaming/child) we have to remove the first parameter (plugin_name)
2047 + buffer_strcat(function_out, get_word(words, num_words, 0));
2048 + for (size_t i = 1; i < num_words; i++) {
2049 + if (i == 1 && SERVING_PLUGINSD(parser))
2050 + continue;
2051 + buffer_sprintf(function_out, " %s", get_word(words, num_words, i));
2052 + }
2053 + freez(function_with_params);
2054 +
2055 + usec_t now = now_realtime_usec();
2056 +
2057 + struct inflight_function tmp = {
2058 + .started_ut = now,
2059 + .timeout_ut = now + VIRT_FNC_TIMEOUT + USEC_PER_SEC,
2060 + .result_body_wb = wb,
2061 + .timeout = VIRT_FNC_TIMEOUT * 10,
2062 + .function = string_strdupz(buffer_tostring(function_out)),
2063 + .result_cb = callback,
2064 + .result_cb_data = callback_data,
2065 + .payload = payload != NULL ? strdupz(payload) : NULL,
2066 + .virtual = true,
2067 + };
2068 + buffer_free(function_out);
2069 +
2070 + uuid_t uuid;
2071 + uuid_generate_time(uuid);
2072 +
2073 + char key[UUID_STR_LEN];
2074 + uuid_unparse_lower(uuid, key);
2075 +
2076 + dictionary_write_lock(parser->inflight.functions);
2077 +
2078 + // if there is any error, our dictionary callbacks will call the caller callback to notify
2079 + // the caller about the error - no need for error handling here.
2080 + dictionary_set(parser->inflight.functions, key, &tmp, sizeof(struct inflight_function));
2081 +
2082 + if(!parser->inflight.smaller_timeout || tmp.timeout_ut < parser->inflight.smaller_timeout)
2083 + parser->inflight.smaller_timeout = tmp.timeout_ut;
2084 +
2085 + // garbage collect stale inflight functions
2086 + if(parser->inflight.smaller_timeout < now)
2087 + inflight_functions_garbage_collect(parser, now);
2088 +
2089 + dictionary_write_unlock(parser->inflight.functions);
2090 +}
2091 +
2092 +
2093 dyncfg_config_t call_virtual_function_blocking(PARSER *parser, const char *name, int *rc, const char *payload) {
2094 usec_t now = now_realtime_usec();
1952 - BUFFER *wb = buffer_create(4096, NULL);
2095 + BUFFER *wb = buffer_create(VIRT_FNC_BUF_SIZE, NULL);
2096
2097 struct mutex_cond cond = {
2098 .lock = PTHREAD_MUTEX_INITIALIZER,
@@ -1964,7 +2107,8 @@ dyncfg_config_t call_virtual_function_blocking(PARSER *parser, const char *name,
2107 .function = string_strdupz(name),
2108 .result_cb = virt_fnc_got_data_cb,
2109 .result_cb_data = &cond,
1967 - .payload = payload,
2110 + .payload = payload != NULL ? strdupz(payload) : NULL,
2111 + .virtual = true,
2112 };
2113
2114 uuid_t uuid;
@@ -2011,98 +2155,188 @@ dyncfg_config_t call_virtual_function_blocking(PARSER *parser, const char *name,
2155 return cfg;
2156 }
2157
2014 -static dyncfg_config_t get_plugin_config_cb(void *usr_ctx)
2158 +#define CVF_MAX_LEN (1024)
2159 +static dyncfg_config_t get_plugin_config_cb(void *usr_ctx, const char *plugin_name)
2160 {
2161 PARSER *parser = usr_ctx;
2017 - return call_virtual_function_blocking(parser, "get_plugin_config", NULL, NULL);
2162 +
2163 + if (SERVING_STREAMING(parser)) {
2164 + char buf[CVF_MAX_LEN + 1];
2165 + snprintfz(buf, CVF_MAX_LEN, FUNCTION_NAME_GET_PLUGIN_CONFIG " %s", plugin_name);
2166 + return call_virtual_function_blocking(parser, buf, NULL, NULL);
2167 + }
2168 +
2169 + return call_virtual_function_blocking(parser, FUNCTION_NAME_GET_PLUGIN_CONFIG, NULL, NULL);
2170 }
2171
2020 -static dyncfg_config_t get_plugin_config_schema_cb(void *usr_ctx)
2172 +static dyncfg_config_t get_plugin_config_schema_cb(void *usr_ctx, const char *plugin_name)
2173 {
2174 PARSER *parser = usr_ctx;
2175 +
2176 + if (SERVING_STREAMING(parser)) {
2177 + char buf[CVF_MAX_LEN + 1];
2178 + snprintfz(buf, CVF_MAX_LEN, FUNCTION_NAME_GET_PLUGIN_CONFIG_SCHEMA " %s", plugin_name);
2179 + return call_virtual_function_blocking(parser, buf, NULL, NULL);
2180 + }
2181 +
2182 return call_virtual_function_blocking(parser, "get_plugin_config_schema", NULL, NULL);
2183 }
2184
2026 -static dyncfg_config_t get_module_config_cb(void *usr_ctx, const char *module_name)
2185 +static dyncfg_config_t get_module_config_cb(void *usr_ctx, const char *plugin_name, const char *module_name)
2186 {
2187 PARSER *parser = usr_ctx;
2029 - char buf[1024];
2030 - snprintfz(buf, sizeof(buf), "get_module_config %s", module_name);
2031 - return call_virtual_function_blocking(parser, buf, NULL, NULL);
2188 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2189 +
2190 + buffer_strcat(wb, FUNCTION_NAME_GET_MODULE_CONFIG);
2191 + if (SERVING_STREAMING(parser))
2192 + buffer_sprintf(wb, " %s", plugin_name);
2193 +
2194 + buffer_sprintf(wb, " %s", module_name);
2195 +
2196 + dyncfg_config_t ret = call_virtual_function_blocking(parser, buffer_tostring(wb), NULL, NULL);
2197 +
2198 + buffer_free(wb);
2199 +
2200 + return ret;
2201 }
2202
2034 -static dyncfg_config_t get_module_config_schema_cb(void *usr_ctx, const char *module_name)
2203 +static dyncfg_config_t get_module_config_schema_cb(void *usr_ctx, const char *plugin_name, const char *module_name)
2204 {
2205 PARSER *parser = usr_ctx;
2037 - char buf[1024];
2038 - snprintfz(buf, sizeof(buf), "get_module_config_schema %s", module_name);
2039 - return call_virtual_function_blocking(parser, buf, NULL, NULL);
2206 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2207 +
2208 + buffer_strcat(wb, FUNCTION_NAME_GET_MODULE_CONFIG_SCHEMA);
2209 + if (SERVING_STREAMING(parser))
2210 + buffer_sprintf(wb, " %s", plugin_name);
2211 +
2212 + buffer_sprintf(wb, " %s", module_name);
2213 +
2214 + dyncfg_config_t ret = call_virtual_function_blocking(parser, buffer_tostring(wb), NULL, NULL);
2215 +
2216 + buffer_free(wb);
2217 +
2218 + return ret;
2219 }
2220
2042 -static dyncfg_config_t get_job_config_schema_cb(void *usr_ctx, const char *module_name)
2221 +static dyncfg_config_t get_job_config_schema_cb(void *usr_ctx, const char *plugin_name, const char *module_name)
2222 {
2223 PARSER *parser = usr_ctx;
2045 - char buf[1024];
2046 - snprintfz(buf, sizeof(buf), "get_job_config_schema %s", module_name);
2047 - return call_virtual_function_blocking(parser, buf, NULL, NULL);
2224 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2225 +
2226 + buffer_strcat(wb, FUNCTION_NAME_GET_JOB_CONFIG_SCHEMA);
2227 +
2228 + if (SERVING_STREAMING(parser))
2229 + buffer_sprintf(wb, " %s", plugin_name);
2230 +
2231 + buffer_sprintf(wb, " %s", module_name);
2232 +
2233 + dyncfg_config_t ret = call_virtual_function_blocking(parser, buffer_tostring(wb), NULL, NULL);
2234 +
2235 + buffer_free(wb);
2236 +
2237 + return ret;
2238 }
2239
2050 -static dyncfg_config_t get_job_config_cb(void *usr_ctx, const char *module_name, const char* job_name)
2240 +static dyncfg_config_t get_job_config_cb(void *usr_ctx, const char *plugin_name, const char *module_name, const char* job_name)
2241 {
2242 PARSER *parser = usr_ctx;
2053 - char buf[1024];
2054 - snprintfz(buf, sizeof(buf), "get_job_config %s %s", module_name, job_name);
2055 - return call_virtual_function_blocking(parser, buf, NULL, NULL);
2243 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2244 +
2245 + buffer_strcat(wb, FUNCTION_NAME_GET_JOB_CONFIG);
2246 +
2247 + if (SERVING_STREAMING(parser))
2248 + buffer_sprintf(wb, " %s", plugin_name);
2249 +
2250 + buffer_sprintf(wb, " %s %s", module_name, job_name);
2251 +
2252 + dyncfg_config_t ret = call_virtual_function_blocking(parser, buffer_tostring(wb), NULL, NULL);
2253 +
2254 + buffer_free(wb);
2255 +
2256 + return ret;
2257 }
2258
2058 -enum set_config_result set_plugin_config_cb(void *usr_ctx, dyncfg_config_t *cfg)
2259 +enum set_config_result set_plugin_config_cb(void *usr_ctx, const char *plugin_name, dyncfg_config_t *cfg)
2260 {
2261 PARSER *parser = usr_ctx;
2262 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2263 +
2264 + buffer_strcat(wb, FUNCTION_NAME_SET_PLUGIN_CONFIG);
2265 +
2266 + if (SERVING_STREAMING(parser))
2267 + buffer_sprintf(wb, " %s", plugin_name);
2268 +
2269 int rc;
2062 - call_virtual_function_blocking(parser, "set_plugin_config", &rc, cfg->data);
2063 - if(rc != 1)
2270 + call_virtual_function_blocking(parser, buffer_tostring(wb), &rc, cfg->data);
2271 +
2272 + buffer_free(wb);
2273 + if(rc != DYNCFG_VFNC_RET_CFG_ACCEPTED)
2274 return SET_CONFIG_REJECTED;
2275 return SET_CONFIG_ACCEPTED;
2276 }
2277
2068 -enum set_config_result set_module_config_cb(void *usr_ctx, const char *module_name, dyncfg_config_t *cfg)
2278 +enum set_config_result set_module_config_cb(void *usr_ctx, const char *plugin_name, const char *module_name, dyncfg_config_t *cfg)
2279 {
2280 PARSER *parser = usr_ctx;
2281 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2282 +
2283 + buffer_strcat(wb, FUNCTION_NAME_SET_MODULE_CONFIG);
2284 +
2285 + if (SERVING_STREAMING(parser))
2286 + buffer_sprintf(wb, " %s", plugin_name);
2287 +
2288 + buffer_sprintf(wb, " %s", module_name);
2289 +
2290 int rc;
2291 + call_virtual_function_blocking(parser, buffer_tostring(wb), &rc, cfg->data);
2292
2073 - char buf[1024];
2074 - snprintfz(buf, sizeof(buf), "set_module_config %s", module_name);
2075 - call_virtual_function_blocking(parser, buf, &rc, cfg->data);
2293 + buffer_free(wb);
2294
2077 - if(rc != 1)
2295 + if(rc != DYNCFG_VFNC_RET_CFG_ACCEPTED)
2296 return SET_CONFIG_REJECTED;
2297 return SET_CONFIG_ACCEPTED;
2298 }
2299
2082 -enum set_config_result set_job_config_cb(void *usr_ctx, const char *module_name, const char *job_name, dyncfg_config_t *cfg)
2300 +enum set_config_result set_job_config_cb(void *usr_ctx, const char *plugin_name, const char *module_name, const char *job_name, dyncfg_config_t *cfg)
2301 {
2302 PARSER *parser = usr_ctx;
2303 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2304 +
2305 + buffer_strcat(wb, FUNCTION_NAME_SET_JOB_CONFIG);
2306 +
2307 + if (SERVING_STREAMING(parser))
2308 + buffer_sprintf(wb, " %s", plugin_name);
2309 +
2310 + buffer_sprintf(wb, " %s %s", module_name, job_name);
2311 +
2312 int rc;
2313 + call_virtual_function_blocking(parser, buffer_tostring(wb), &rc, cfg->data);
2314
2087 - char buf[1024];
2088 - snprintfz(buf, sizeof(buf), "set_job_config %s %s", module_name, job_name);
2089 - call_virtual_function_blocking(parser, buf, &rc, cfg->data);
2315 + buffer_free(wb);
2316
2091 - if(rc != 1)
2317 + if(rc != DYNCFG_VFNC_RET_CFG_ACCEPTED)
2318 return SET_CONFIG_REJECTED;
2319 return SET_CONFIG_ACCEPTED;
2320 }
2321
2096 -enum set_config_result delete_job_cb(void *usr_ctx, const char *module_name, const char *job_name)
2322 +enum set_config_result delete_job_cb(void *usr_ctx, const char *plugin_name ,const char *module_name, const char *job_name)
2323 {
2324 PARSER *parser = usr_ctx;
2325 + BUFFER *wb = buffer_create(CVF_MAX_LEN, NULL);
2326 +
2327 + buffer_strcat(wb, FUNCTION_NAME_DELETE_JOB);
2328 +
2329 + if (SERVING_STREAMING(parser))
2330 + buffer_sprintf(wb, " %s", plugin_name);
2331 +
2332 + buffer_sprintf(wb, " %s %s", module_name, job_name);
2333 +
2334 int rc;
2335 + call_virtual_function_blocking(parser, buffer_tostring(wb), &rc, NULL);
2336
2101 - char buf[1024];
2102 - snprintfz(buf, sizeof(buf), "delete_job %s %s", module_name, job_name);
2103 - call_virtual_function_blocking(parser, buf, &rc, NULL);
2337 + buffer_free(wb);
2338
2105 - if(rc != 1)
2339 + if(rc != DYNCFG_VFNC_RET_CFG_ACCEPTED)
2340 return SET_CONFIG_REJECTED;
2341 return SET_CONFIG_ACCEPTED;
2342 }
@@ -2122,37 +2356,65 @@ static inline PARSER_RC pluginsd_register_plugin(char **words __maybe_unused, si
2356 cfg->get_config_schema_cb = get_plugin_config_schema_cb;
2357 cfg->cb_usr_ctx = parser;
2358
2125 - parser->user.cd->cfg_dict_item = register_plugin(cfg);
2126 -
2127 - if (unlikely(parser->user.cd->cfg_dict_item == NULL)) {
2359 + const DICTIONARY_ITEM *di = register_plugin(parser->user.host->configurable_plugins, cfg, SERVING_PLUGINSD(parser));
2360 + if (unlikely(di == NULL)) {
2361 freez(cfg->name);
2362 freez(cfg);
2363 return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_ENABLE, "error registering plugin");
2364 }
2365
2133 - parser->user.cd->configuration = cfg;
2366 + if (SERVING_PLUGINSD(parser)) {
2367 + // this is optimization for pluginsd to avoid extra dictionary lookup
2368 + // as we know which plugin is comunicating with us
2369 + parser->user.cd->cfg_dict_item = di;
2370 + parser->user.cd->configuration = cfg;
2371 + } else {
2372 + // register_plugin keeps the item acquired, so we need to release it
2373 + dictionary_acquired_item_release(parser->user.host->configurable_plugins, di);
2374 + }
2375 +
2376 + rrdpush_send_dyncfg_enable(parser->user.host, cfg->name);
2377 +
2378 return PARSER_RC_OK;
2379 }
2380
2381 +#define LOG_MSG_SIZE (1024)
2382 +#define MODULE_NAME_IDX (SERVING_PLUGINSD(parser) ? 1 : 2)
2383 +#define MODULE_TYPE_IDX (SERVING_PLUGINSD(parser) ? 2 : 3)
2384 static inline PARSER_RC pluginsd_register_module(char **words __maybe_unused, size_t num_words __maybe_unused, PARSER *parser __maybe_unused) {
2385 netdata_log_info("PLUGINSD: DYNCFG_REG_MODULE");
2386
2140 - struct configurable_plugin *plug_cfg = parser->user.cd->configuration;
2141 - if (unlikely(plug_cfg == NULL))
2142 - return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "you have to enable dynamic configuration first using " PLUGINSD_KEYWORD_DYNCFG_ENABLE);
2387 + size_t expected_num_words = SERVING_PLUGINSD(parser) ? 3 : 4;
2388
2144 - if (unlikely(num_words != 3))
2145 - return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "expected 2 parameters module_name followed by module_type");
2389 + if (unlikely(num_words != expected_num_words)) {
2390 + char log[LOG_MSG_SIZE + 1];
2391 + snprintfz(log, LOG_MSG_SIZE, "expected %zu (got %zu) parameters: %smodule_name module_type", expected_num_words - 1, num_words - 1, SERVING_PLUGINSD(parser) ? "" : "plugin_name ");
2392 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, log);
2393 + }
2394 +
2395 + struct configurable_plugin *plug_cfg;
2396 + const DICTIONARY_ITEM *di = NULL;
2397 + if (SERVING_PLUGINSD(parser)) {
2398 + plug_cfg = parser->user.cd->configuration;
2399 + if (unlikely(plug_cfg == NULL))
2400 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "you have to enable dynamic configuration first using " PLUGINSD_KEYWORD_DYNCFG_ENABLE);
2401 + } else {
2402 + di = dictionary_get_and_acquire_item(parser->user.host->configurable_plugins, words[1]);
2403 + if (unlikely(di == NULL))
2404 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "plugin not found");
2405 +
2406 + plug_cfg = (struct configurable_plugin *)dictionary_acquired_item_value(di);
2407 + }
2408
2409 struct module *mod = callocz(1, sizeof(struct module));
2410
2149 - mod->type = str2_module_type(words[2]);
2411 + mod->type = str2_module_type(words[MODULE_TYPE_IDX]);
2412 if (unlikely(mod->type == MOD_TYPE_UNKNOWN)) {
2413 freez(mod);
2414 return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "unknown module type (allowed: job_array, single)");
2415 }
2416
2155 - mod->name = strdupz(words[1]);
2417 + mod->name = strdupz(words[MODULE_NAME_IDX]);
2418
2419 mod->set_config_cb = set_module_config_cb;
2420 mod->get_config_cb = get_module_config_cb;
@@ -2165,27 +2427,111 @@ static inline PARSER_RC pluginsd_register_module(char **words __maybe_unused, si
2427 mod->delete_job_cb = delete_job_cb;
2428 mod->job_config_cb_usr_ctx = parser;
2429
2168 - register_module(plug_cfg, mod);
2430 + register_module(parser->user.host->configurable_plugins, plug_cfg, mod, SERVING_PLUGINSD(parser));
2431 +
2432 + if (di != NULL)
2433 + dictionary_acquired_item_release(parser->user.host->configurable_plugins, di);
2434 +
2435 + rrdpush_send_dyncfg_reg_module(parser->user.host, plug_cfg->name, mod->name, mod->type);
2436 +
2437 return PARSER_RC_OK;
2438 }
2439
2172 -// job_status <module_name> <job_name> <status_code> <state> <message>
2173 -static inline PARSER_RC pluginsd_job_status(char **words, size_t num_words, PARSER *parser)
2174 -{
2175 - if (unlikely(num_words != 6 && num_words != 5))
2176 - return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "expected 4 or 5 parameters: module_name, job_name, status_code, state, [optional: message]");
2440 +static inline PARSER_RC pluginsd_register_job_common(char **words __maybe_unused, size_t num_words __maybe_unused, PARSER *parser __maybe_unused, const char *plugin_name) {
2441 + if (atol(words[3]) < 0)
2442 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB, "invalid flags");
2443 + dyncfg_job_flg_t flags = atol(words[3]);
2444 + if (SERVING_PLUGINSD(parser))
2445 + flags |= JOB_FLG_PLUGIN_PUSHED;
2446 + else
2447 + flags |= JOB_FLG_STREAMING_PUSHED;
2448 +
2449 + enum job_type job_type = str2job_type(words[2]);
2450 + if (job_type == JOB_TYPE_UNKNOWN)
2451 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB, "unknown job type");
2452 + if (SERVING_PLUGINSD(parser) && job_type == JOB_TYPE_USER)
2453 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB, "plugins cannot push jobs of type \"user\" (this is allowed only in streaming)");
2454 +
2455 + if (register_job(parser->user.host->configurable_plugins, plugin_name, words[0], words[1], job_type, flags, 0)) // ignore existing is off as this is explicitly called register job
2456 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB, "error registering job");
2457 +
2458 + rrdpush_send_dyncfg_reg_job(parser->user.host, plugin_name, words[0], words[1], job_type, flags);
2459 + return PARSER_RC_OK;
2460 +}
2461 +
2462 +static inline PARSER_RC pluginsd_register_job(char **words __maybe_unused, size_t num_words __maybe_unused, PARSER *parser __maybe_unused) {
2463 + size_t expected_num_words = SERVING_PLUGINSD(parser) ? 5 : 6;
2464 +
2465 + if (unlikely(num_words != expected_num_words)) {
2466 + char log[LOG_MSG_SIZE + 1];
2467 + snprintfz(log, LOG_MSG_SIZE, "expected %zu (got %zu) parameters: %smodule_name job_name job_type", expected_num_words - 1, num_words - 1, SERVING_PLUGINSD(parser) ? "" : "plugin_name ");
2468 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB, log);
2469 + }
2470
2178 - int state = atoi(words[4]);
2471 + if (SERVING_PLUGINSD(parser)) {
2472 + return pluginsd_register_job_common(&words[1], num_words - 1, parser, parser->user.cd->configuration->name);
2473 + }
2474 + return pluginsd_register_job_common(&words[2], num_words - 2, parser, words[1]);
2475 +}
2476
2180 - enum job_status job_status = str2job_state(words[3]);
2181 - if (unlikely(job_status == JOB_STATUS_UNKNOWN))
2182 - return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "unknown job state");
2477 +static inline PARSER_RC pluginsd_job_status_common(char **words, size_t num_words, PARSER *parser, const char *plugin_name) {
2478 + int state = str2i(words[3]);
2479 +
2480 + enum job_status status = str2job_state(words[2]);
2481 + if (unlikely(SERVING_PLUGINSD(parser) && status == JOB_STATUS_UNKNOWN))
2482 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "unknown job status");
2483
2484 char *message = NULL;
2185 - if (num_words == 6)
2186 - message = strdupz(words[5]);
2485 + if (num_words == 5)
2486 + message = words[4];
2487 +
2488 + const DICTIONARY_ITEM *plugin_item;
2489 + DICTIONARY *job_dict;
2490 + const DICTIONARY_ITEM *job_item = report_job_status_acq_lock(parser->user.host->configurable_plugins, &plugin_item, &job_dict, plugin_name, words[0], words[1], status, state, message);
2491 +
2492 + if (job_item != NULL) {
2493 + struct job *job = dictionary_acquired_item_value(job_item);
2494 + rrdpush_send_job_status_update(parser->user.host, plugin_name, words[0], job);
2495 +
2496 + pthread_mutex_unlock(&job->lock);
2497 + dictionary_acquired_item_release(job_dict, job_item);
2498 + dictionary_acquired_item_release(parser->user.host->configurable_plugins, plugin_item);
2499 + }
2500
2188 - report_job_status(parser->user.cd->configuration, words[1], words[2], job_status, state, message);
2501 + return PARSER_RC_OK;
2502 +}
2503 +
2504 +// job_status [plugin_name if streaming] <module_name> <job_name> <status_code> <state> [message]
2505 +static PARSER_RC pluginsd_job_status(char **words, size_t num_words, PARSER *parser) {
2506 + if (SERVING_PLUGINSD(parser)) {
2507 + if (unlikely(num_words != 5 && num_words != 6))
2508 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "expected 4 or 5 parameters: module_name, job_name, status_code, state, [optional: message]");
2509 + } else {
2510 + if (unlikely(num_words != 6 && num_words != 7))
2511 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "expected 5 or 6 parameters: plugin_name, module_name, job_name, status_code, state, [optional: message]");
2512 + }
2513 +
2514 + if (SERVING_PLUGINSD(parser)) {
2515 + return pluginsd_job_status_common(&words[1], num_words - 1, parser, parser->user.cd->configuration->name);
2516 + }
2517 + return pluginsd_job_status_common(&words[2], num_words - 2, parser, words[1]);
2518 +}
2519 +
2520 +static PARSER_RC pluginsd_delete_job(char **words, size_t num_words, PARSER *parser) {
2521 + // this can confuse a bit but there is a diference between KEYWORD_DELETE_JOB and actual delete_job function
2522 + // they are of opossite direction
2523 + if (num_words != 4)
2524 + return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DELETE_JOB, "expected 2 parameters: plugin_name, module_name, job_name");
2525 +
2526 + const char *plugin_name = get_word(words, num_words, 1);
2527 + const char *module_name = get_word(words, num_words, 2);
2528 + const char *job_name = get_word(words, num_words, 3);
2529 +
2530 + if (SERVING_STREAMING(parser))
2531 + delete_job_pname(parser->user.host->configurable_plugins, plugin_name, module_name, job_name);
2532 +
2533 + // forward to parent if any
2534 + rrdpush_send_job_deleted(parser->user.host, plugin_name, module_name, job_name);
2535 return PARSER_RC_OK;
2536 }
2537
@@ -2506,8 +2852,14 @@ PARSER_RC parser_execute(PARSER *parser, PARSER_KEYWORD *keyword, char **words,
2852 case 102:
2853 return pluginsd_register_module(words, num_words, parser);
2854
2855 + case 103:
2856 + return pluginsd_register_job(words, num_words, parser);
2857 +
2858 case 110:
2859 return pluginsd_job_status(words, num_words, parser);
2860 +
2861 + case 111:
2862 + return pluginsd_delete_job(words, num_words, parser);
2863
2864 default:
2865 fatal("Unknown keyword '%s' with id %zu", keyword->keyword, keyword->id);
@@ -2525,14 +2877,20 @@ void parser_init_repertoire(PARSER *parser, PARSER_REPERTOIRE repertoire) {
2877 }
2878 }
2879
2880 +static void parser_destroy_dyncfg(PARSER *parser) {
2881 + if (parser->user.cd != NULL && parser->user.cd->configuration != NULL) {
2882 + unregister_plugin(parser->user.host->configurable_plugins, parser->user.cd->cfg_dict_item);
2883 + parser->user.cd->configuration = NULL;
2884 + } else if (parser->user.host != NULL && SERVING_STREAMING(parser) && parser->user.host != localhost){
2885 + dictionary_flush(parser->user.host->configurable_plugins);
2886 + }
2887 +}
2888 +
2889 void parser_destroy(PARSER *parser) {
2890 if (unlikely(!parser))
2891 return;
2892
2532 - if (parser->user.cd != NULL && parser->user.cd->configuration != NULL) {
2533 - unregister_plugin(parser->user.cd->cfg_dict_item);
2534 - parser->user.cd->configuration = NULL;
2535 - }
2893 + parser_destroy_dyncfg(parser);
2894
2895 dictionary_destroy(parser->inflight.functions);
2896 freez(parser);
daemon/main.c
+2 -2
@@ -1935,8 +1935,6 @@ int main(int argc, char **argv) {
1935 signals_block();
1936 signals_init(); // setup the signals we want to use
1937
1938 - dyn_conf_init();
1939 -
1938 // --------------------------------------------------------------------
1939 // check which threads are enabled and initialize them
1940
@@ -2002,6 +2000,8 @@ int main(int argc, char **argv) {
2000 if(become_daemon(dont_fork, user) == -1)
2001 fatal("Cannot daemonize myself.");
2002
2003 + dyn_conf_init();
2004 +
2005 netdata_log_info("netdata started on pid %d.", getpid());
2006
2007 delta_startup_time("initialize threads after fork");
database/rrd.h
+2
@@ -1254,6 +1254,8 @@ struct rrdhost {
1254 netdata_mutex_t aclk_state_lock;
1255 aclk_rrdhost_state aclk_state;
1256
1257 + DICTIONARY *configurable_plugins; // configurable plugins for this host
1258 +
1259 struct rrdhost *next;
1260 struct rrdhost *prev;
1261 };
database/rrdfunctions.c
+9 -1
@@ -951,12 +951,14 @@ static inline int rrd_call_function_async(struct rrd_function_inflight *r, bool
951 return rrd_call_function_async_and_dont_wait(r);
952 }
953
954 +
955 +void call_virtual_function_async(BUFFER *wb, RRDHOST *host, const char *name, const char *payload, rrd_function_result_callback_t callback, void *callback_data);
956 // ----------------------------------------------------------------------------
957
958 int rrd_function_run(RRDHOST *host, BUFFER *result_wb, int timeout, const char *cmd,
959 bool wait, const char *transaction,
960 rrd_function_result_callback_t result_cb, void *result_cb_data,
959 - rrd_function_is_cancelled_cb_t is_cancelled_cb, void *is_cancelled_cb_data) {
961 + rrd_function_is_cancelled_cb_t is_cancelled_cb, void *is_cancelled_cb_data, const char *payload) {
962
963 int code;
964 char sanitized_cmd[PLUGINSD_LINE_MAX + 1];
@@ -966,6 +968,12 @@ int rrd_function_run(RRDHOST *host, BUFFER *result_wb, int timeout, const char *
968 // find the function
969
970 size_t sanitized_cmd_length = sanitize_function_text(sanitized_cmd, cmd, PLUGINSD_LINE_MAX);
971 +
972 + if (is_dyncfg_function(sanitized_cmd, DYNCFG_FUNCTION_TYPE_ALL)) {
973 + call_virtual_function_async(result_wb, host, sanitized_cmd, payload, result_cb, result_cb_data);
974 + return HTTP_RESP_OK;
975 + }
976 +
977 code = rrd_call_function_find(host, result_wb, sanitized_cmd, sanitized_cmd_length, &host_function_acquired);
978 if(code != HTTP_RESP_OK)
979 return code;
database/rrdfunctions.h
+1 -1
@@ -30,7 +30,7 @@ void rrd_function_add(RRDHOST *host, RRDSET *st, const char *name, int timeout,
30 int rrd_function_run(RRDHOST *host, BUFFER *result_wb, int timeout, const char *cmd,
31 bool wait, const char *transaction,
32 rrd_function_result_callback_t result_cb, void *result_cb_data,
33 - rrd_function_is_cancelled_cb_t is_cancelled_cb, void *is_cancelled_cb_data);
33 + rrd_function_is_cancelled_cb_t is_cancelled_cb, void *is_cancelled_cb_data, const char *payload);
34
35 // cancel a running function, to be run from anywhere
36 void rrd_function_cancel(const char *transaction);
database/rrdhost.c
+3
@@ -562,6 +562,9 @@ int is_legacy = 1;
562 , string2str(host->health.health_default_recipient)
563 );
564
565 + host->configurable_plugins = dyncfg_dictionary_create();
566 + dictionary_register_delete_callback(host->configurable_plugins, plugin_del_cb, NULL);
567 +
568 if(!archived) {
569 metaqueue_host_update_info(host);
570 rrdhost_load_rrdcontext_data(host);
libnetdata/dyn_conf/dyn_conf.c
+336 -161
@@ -3,7 +3,7 @@
3 #include "dyn_conf.h"
4
5 #define DYN_CONF_PATH_MAX (4096)
6 -#define DYN_CONF_DIR VARLIB_DIR "/etc"
6 +#define DYN_CONF_DIR VARLIB_DIR "/dynconf"
7
8 #define DYN_CONF_JOB_SCHEMA "job_schema"
9 #define DYN_CONF_SCHEMA "schema"
@@ -11,9 +11,20 @@
11 #define DYN_CONF_JOB_LIST "jobs"
12 #define DYN_CONF_CFG_EXT ".cfg"
13
14 -DICTIONARY *plugins_dict = NULL;
14 +void job_flags_wallkthrough(dyncfg_job_flg_t flags, void (*cb)(const char *str, void *data), void *data)
15 +{
16 + if (flags & JOB_FLG_PS_LOADED)
17 + cb("JOB_FLG_PS_LOADED", data);
18 + if (flags & JOB_FLG_PLUGIN_PUSHED)
19 + cb("JOB_FLG_PLUGIN_PUSHED", data);
20 + if (flags & JOB_FLG_STREAMING_PUSHED)
21 + cb("JOB_FLG_STREAMING_PUSHED", data);
22 + if (flags & JOB_FLG_USER_CREATED)
23 + cb("JOB_FLG_USER_CREATED", data);
24 +}
25
26 struct deferred_cfg_send {
27 + DICTIONARY *plugins_dict;
28 char *plugin_name;
29 char *module_name;
30 char *job_name;
@@ -33,7 +44,7 @@ static void deferred_config_free(struct deferred_cfg_send *dcs)
44 freez(dcs);
45 }
46
36 -static void deferred_config_push_back(const char *plugin_name, const char *module_name, const char *job_name)
47 +static void deferred_config_push_back(DICTIONARY *plugins_dict, const char *plugin_name, const char *module_name, const char *job_name)
48 {
49 struct deferred_cfg_send *deferred = callocz(1, sizeof(struct deferred_cfg_send));
50 deferred->plugin_name = strdupz(plugin_name);
@@ -42,6 +53,7 @@ static void deferred_config_push_back(const char *plugin_name, const char *modul
53 if (job_name != NULL)
54 deferred->job_name = strdupz(job_name);
55 }
56 + deferred->plugins_dict = plugins_dict;
57 pthread_mutex_lock(&deferred_configs_lock);
58 if (dyncfg_shutdown) {
59 pthread_mutex_unlock(&deferred_configs_lock);
@@ -95,7 +107,7 @@ static int _get_list_of_plugins_json_cb(const DICTIONARY_ITEM *item, void *entry
107 return 0;
108 }
109
98 -json_object *get_list_of_plugins_json()
110 +json_object *get_list_of_plugins_json(DICTIONARY *plugins_dict)
111 {
112 json_object *obj = json_object_new_array();
113
@@ -114,18 +126,7 @@ static int _get_list_of_modules_json_cb(const DICTIONARY_ITEM *item, void *entry
126
127 json_object *json_item = json_object_new_string(module->name);
128 json_object_object_add(json_module, "name", json_item);
117 - const char *module_type;
118 - switch (module->type) {
119 - case MOD_TYPE_SINGLE:
120 - module_type = "single";
121 - break;
122 - case MOD_TYPE_ARRAY:
123 - module_type = "job_array";
124 - break;
125 - default:
126 - module_type = "unknown";
127 - break;
128 - }
129 + const char *module_type = module_type2str(module->type);
130 json_item = json_object_new_string(module_type);
131 json_object_object_add(json_module, "type", json_item);
132
@@ -163,17 +164,31 @@ const char *job_status2str(enum job_status status)
164 }
165 }
166
166 -static int _get_list_of_jobs_json_cb(const DICTIONARY_ITEM *item, void *entry, void *data)
167 +static void _job_flags2str_cb(const char *str, void *data)
168 {
168 - UNUSED(item);
169 - json_object *obj = (json_object *)data;
170 - struct job *job = (struct job *)entry;
169 + json_object *json_item = json_object_new_string(str);
170 + json_object_array_add((json_object *)data, json_item);
171 +}
172
173 +json_object *job2json(struct job *job) {
174 json_object *json_job = json_object_new_object();
175 +
176 json_object *json_item = json_object_new_string(job->name);
177 json_object_object_add(json_job, "name", json_item);
178 +
179 + json_item = json_object_new_string(job_type2str(job->type));
180 + json_object_object_add(json_job, "type", json_item);
181 +
182 + netdata_mutex_lock(&job->lock);
183 json_item = json_object_new_string(job_status2str(job->status));
184 + json_object_object_add(json_job, "status", json_item);
185 +
186 + json_item = json_object_new_int(job->state);
187 json_object_object_add(json_job, "state", json_item);
188 +
189 + json_item = job->reason == NULL ? NULL : json_object_new_string(job->reason);
190 + json_object_object_add(json_job, "reason", json_item);
191 +
192 int64_t last_state_update_s = job->last_state_update / USEC_PER_SEC;
193 int64_t last_state_update_us = job->last_state_update % USEC_PER_SEC;
194
@@ -183,6 +198,22 @@ static int _get_list_of_jobs_json_cb(const DICTIONARY_ITEM *item, void *entry, v
198 json_item = json_object_new_int64(last_state_update_us);
199 json_object_object_add(json_job, "last_state_update_us", json_item);
200
201 + json_item = json_object_new_array();
202 + job_flags_wallkthrough(job->flags, _job_flags2str_cb, json_item);
203 + json_object_object_add(json_job, "flags", json_item);
204 +
205 + netdata_mutex_unlock(&job->lock);
206 +
207 + return json_job;
208 +}
209 +
210 +static int _get_list_of_jobs_json_cb(const DICTIONARY_ITEM *item, void *entry, void *data)
211 +{
212 + UNUSED(item);
213 + json_object *obj = (json_object *)data;
214 +
215 + json_object *json_job = job2json((struct job *)entry);
216 +
217 json_object_array_add(obj, json_job);
218
219 return 0;
@@ -206,24 +237,57 @@ struct job *get_job_by_name(struct module *module, const char *job_name)
237 return dictionary_get(module->jobs, job_name);
238 }
239
209 -int remove_job(struct module *module, struct job *job)
240 +void unlink_job(const char *plugin_name, const char *module_name, const char *job_name)
241 {
242 // as we are going to do unlink here we better make sure we have all to build proper path
212 - if (unlikely(job->name == NULL || module == NULL || module->name == NULL || module->plugin == NULL || module->plugin->name == NULL))
213 - return 0;
243 + if (unlikely(job_name == NULL || module_name == NULL || plugin_name == NULL))
244 + return;
245 + BUFFER *buffer = buffer_create(DYN_CONF_PATH_MAX, NULL);
246 + buffer_sprintf(buffer, DYN_CONF_DIR "/%s/%s/%s" DYN_CONF_CFG_EXT, plugin_name, module_name, job_name);
247 + unlink(buffer_tostring(buffer));
248 + buffer_free(buffer);
249 +}
250 +
251 +void delete_job(struct configurable_plugin *plugin, const char *module_name, const char *job_name)
252 +{
253 + struct module *module = get_module_by_name(plugin, module_name);
254 + if (module == NULL) {
255 + error_report("DYNCFG module \"%s\" not found", module_name);
256 + return;
257 + }
258 +
259 + struct job *job_item = get_job_by_name(module, job_name);
260 + if (job_item == NULL) {
261 + error_report("DYNCFG job \"%s\" not found", job_name);
262 + return;
263 + }
264 +
265 + dictionary_del(module->jobs, job_name);
266 +}
267 +
268 +void delete_job_pname(DICTIONARY *plugins_dict, const char *plugin_name, const char *module_name, const char *job_name)
269 +{
270 + const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, plugin_name);
271 + if (plugin_item == NULL) {
272 + error_report("DYNCFG plugin \"%s\" not found", plugin_name);
273 + return;
274 + }
275 + struct configurable_plugin *plugin = dictionary_acquired_item_value(plugin_item);
276 +
277 + delete_job(plugin, module_name, job_name);
278 +
279 + dictionary_acquired_item_release(plugins_dict, plugin_item);
280 +}
281
215 - enum set_config_result rc = module->delete_job_cb(module->job_config_cb_usr_ctx, module->name, job->name);
282 +int remove_job(struct module *module, struct job *job)
283 +{
284 + enum set_config_result rc = module->delete_job_cb(module->job_config_cb_usr_ctx, module->plugin->name, module->name, job->name);
285
286 if (rc != SET_CONFIG_ACCEPTED) {
287 error_report("DYNCFG module \"%s\" rejected delete job for \"%s\"", module->name, job->name);
288 return 0;
289 }
221 -
222 - BUFFER *buffer = buffer_create(DYN_CONF_PATH_MAX, NULL);
223 - buffer_sprintf(buffer, DYN_CONF_DIR "/%s/%s/%s" DYN_CONF_CFG_EXT, module->plugin->name, module->name, job->name);
224 - unlink(buffer_tostring(buffer));
225 - buffer_free(buffer);
226 - return dictionary_del(module->jobs, job->name);
290 + return 1;
291 }
292
293 struct module *get_module_by_name(struct configurable_plugin *plugin, const char *module_name)
@@ -231,7 +295,7 @@ struct module *get_module_by_name(struct configurable_plugin *plugin, const char
295 return dictionary_get(plugin->modules, module_name);
296 }
297
234 -inline struct configurable_plugin *get_plugin_by_name(const char *name)
298 +inline struct configurable_plugin *get_plugin_by_name(DICTIONARY *plugins_dict, const char *name)
299 {
300 return dictionary_get(plugins_dict, name);
301 }
@@ -282,6 +346,59 @@ static int store_config(const char *module_name, const char *submodule_name, con
346 return 0;
347 }
348
349 +#ifdef NETDATA_DEV_MODE
350 +#define netdata_dev_fatal(...) fatal(__VA_ARGS__)
351 +#else
352 +#define netdata_dev_fatal(...) error_report(__VA_ARGS__)
353 +#endif
354 +
355 +void dyn_conf_store_config(const char *function, const char *payload, struct configurable_plugin *plugin) {
356 + dyncfg_config_t config = {
357 + .data = (char*)payload,
358 + .data_size = strlen(payload)
359 + };
360 +
361 + char *fnc = strdupz(function);
362 + // split fnc to words
363 + char *words[DYNCFG_MAX_WORDS];
364 + size_t words_c = quoted_strings_splitter(fnc, words, DYNCFG_MAX_WORDS, isspace_map_pluginsd);
365 +
366 + const char *fnc_name = get_word(words, words_c, 0);
367 + if (fnc_name == NULL) {
368 + error_report("Function name expected \"%s\"", function);
369 + goto CLEANUP;
370 + }
371 + if (strncmp(fnc_name, FUNCTION_NAME_SET_PLUGIN_CONFIG, strlen(FUNCTION_NAME_SET_PLUGIN_CONFIG)) == 0) {
372 + store_config(plugin->name, NULL, NULL, config);
373 + goto CLEANUP;
374 + }
375 +
376 + if (words_c < 2) {
377 + error_report("Module name expected \"%s\"", function);
378 + goto CLEANUP;
379 + }
380 + const char *module_name = get_word(words, words_c, 1);
381 + if (strncmp(fnc_name, FUNCTION_NAME_SET_MODULE_CONFIG, strlen(FUNCTION_NAME_SET_MODULE_CONFIG)) == 0) {
382 + store_config(plugin->name, module_name, NULL, config);
383 + goto CLEANUP;
384 + }
385 +
386 + if (words_c < 3) {
387 + error_report("Job name expected \"%s\"", function);
388 + goto CLEANUP;
389 + }
390 + const char *job_name = get_word(words, words_c, 2);
391 + if (strncmp(fnc_name, FUNCTION_NAME_SET_JOB_CONFIG, strlen(FUNCTION_NAME_SET_JOB_CONFIG)) == 0) {
392 + store_config(plugin->name, module_name, job_name, config);
393 + goto CLEANUP;
394 + }
395 +
396 + netdata_dev_fatal("Unknown function \"%s\"", function);
397 +
398 +CLEANUP:
399 + freez(fnc);
400 +}
401 +
402 dyncfg_config_t load_config(const char *plugin_name, const char *module_name, const char *job_id)
403 {
404 BUFFER *filename = buffer_create(DYN_CONF_PATH_MAX, NULL);
@@ -310,16 +427,12 @@ dyncfg_config_t load_config(const char *plugin_name, const char *module_name, co
427
428 char *set_plugin_config(struct configurable_plugin *plugin, dyncfg_config_t cfg)
429 {
313 - enum set_config_result rc = plugin->set_config_cb(plugin->cb_usr_ctx, &cfg);
430 + enum set_config_result rc = plugin->set_config_cb(plugin->cb_usr_ctx, plugin->name, &cfg);
431 if (rc != SET_CONFIG_ACCEPTED) {
432 error_report("DYNCFG plugin \"%s\" rejected config", plugin->name);
433 return "plugin rejected config";
434 }
435
319 - if (store_config(plugin->name, NULL, NULL, cfg)) {
320 - error_report("DYNCFG could not store config for module \"%s\"", plugin->name);
321 - return "could not store config on disk";
322 - }
436 return NULL;
437 }
438
@@ -327,62 +440,38 @@ static char *set_module_config(struct module *mod, dyncfg_config_t cfg)
440 {
441 struct configurable_plugin *plugin = mod->plugin;
442
330 - enum set_config_result rc = mod->set_config_cb(mod->config_cb_usr_ctx, mod->name, &cfg);
443 + enum set_config_result rc = mod->set_config_cb(mod->config_cb_usr_ctx, plugin->name, mod->name, &cfg);
444 if (rc != SET_CONFIG_ACCEPTED) {
445 error_report("DYNCFG module \"%s\" rejected config", plugin->name);
446 return "module rejected config";
447 }
448
336 - if (store_config(plugin->name, mod->name, NULL, cfg)) {
337 - error_report("DYNCFG could not store config for module \"%s\"", mod->name);
338 - return "could not store config on disk";
339 - }
340 -
449 return NULL;
450 }
451
344 -struct job *job_new()
452 +struct job *job_new(const char *job_id)
453 {
454 struct job *job = callocz(1, sizeof(struct job));
455 job->state = JOB_STATUS_UNKNOWN;
456 job->last_state_update = now_realtime_usec();
457 + job->name = strdupz(job_id);
458 + netdata_mutex_init(&job->lock);
459 return job;
460 }
461
352 -static int set_job_config(struct job *job, dyncfg_config_t cfg)
462 +static inline void job_del(struct job *job)
463 {
354 - struct module *mod = job->module;
355 - enum set_config_result rt = mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, job->name, &cfg);
356 -
357 - if (rt != SET_CONFIG_ACCEPTED) {
358 - error_report("DYNCFG module \"%s\" rejected config for job \"%s\"", mod->name, job->name);
359 - return 1;
360 - }
361 -
362 - if (store_config(mod->plugin->name, mod->name, job->name, cfg)) {
363 - error_report("DYNCFG could not store config for module \"%s\"", mod->name);
364 - return 1;
365 - }
366 -
367 - return 0;
464 + netdata_mutex_destroy(&job->lock);
465 + freez(job->reason);
466 + freez((void*)job->name);
467 + freez(job);
468 }
469
370 -struct job *add_job(struct module *mod, const char *job_id, dyncfg_config_t cfg)
470 +void job_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
471 {
372 - struct job *job = job_new();
373 - job->name = strdupz(job_id);
374 - job->module = mod;
375 -
376 - if (set_job_config(job, cfg)) {
377 - freez(job->name);
378 - freez(job);
379 - return NULL;
380 - }
381 -
382 - dictionary_set(mod->jobs, job->name, job, sizeof(job));
383 -
384 - return job;
385 -
472 + UNUSED(item);
473 + UNUSED(data);
474 + job_del((struct job *)value);
475 }
476
477 void module_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
@@ -395,10 +484,9 @@ void module_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
484 freez(mod);
485 }
486
398 -
399 -const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin)
487 +const DICTIONARY_ITEM *register_plugin(DICTIONARY *plugins_dict, struct configurable_plugin *plugin, bool localhost)
488 {
401 - if (get_plugin_by_name(plugin->name) != NULL) {
489 + if (get_plugin_by_name(plugins_dict, plugin->name) != NULL) {
490 error_report("DYNCFG plugin \"%s\" already registered", plugin->name);
491 return NULL;
492 }
@@ -413,7 +501,8 @@ const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin)
501 plugin->modules = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
502 dictionary_register_delete_callback(plugin->modules, module_del_cb, NULL);
503
416 - deferred_config_push_back(plugin->name, NULL, NULL);
504 + if (localhost)
505 + deferred_config_push_back(plugins_dict, plugin->name, NULL, NULL);
506
507 dictionary_set(plugins_dict, plugin->name, plugin, sizeof(plugin));
508
@@ -421,24 +510,14 @@ const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin)
510 return dictionary_get_and_acquire_item(plugins_dict, plugin->name);
511 }
512
424 -void unregister_plugin(const DICTIONARY_ITEM *plugin)
513 +void unregister_plugin(DICTIONARY *plugins_dict, const DICTIONARY_ITEM *plugin)
514 {
515 struct configurable_plugin *plug = dictionary_acquired_item_value(plugin);
516 dictionary_acquired_item_release(plugins_dict, plugin);
517 dictionary_del(plugins_dict, plug->name);
518 }
519
431 -void job_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
432 -{
433 - UNUSED(item);
434 - UNUSED(data);
435 - struct job *job = (struct job *)value;
436 - freez(job->reason);
437 - freez(job->name);
438 - freez(job);
439 -}
440 -
441 -int register_module(struct configurable_plugin *plugin, struct module *module)
520 +int register_module(DICTIONARY *plugins_dict, struct configurable_plugin *plugin, struct module *module, bool localhost)
521 {
522 if (get_module_by_name(plugin, module->name) != NULL) {
523 error_report("DYNCFG module \"%s\" already registered", module->name);
@@ -447,7 +526,8 @@ int register_module(struct configurable_plugin *plugin, struct module *module)
526
527 pthread_mutex_init(&module->lock, NULL);
528
450 - deferred_config_push_back(plugin->name, module->name, NULL);
529 + if (localhost)
530 + deferred_config_push_back(plugins_dict, plugin->name, module->name, NULL);
531
532 module->plugin = plugin;
533
@@ -455,34 +535,38 @@ int register_module(struct configurable_plugin *plugin, struct module *module)
535 module->jobs = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
536 dictionary_register_delete_callback(module->jobs, job_del_cb, NULL);
537
458 - // load all jobs from disk
459 - BUFFER *path = buffer_create(DYN_CONF_PATH_MAX, NULL);
460 - buffer_sprintf(path, "%s/%s/%s", DYN_CONF_DIR, plugin->name, module->name);
461 - DIR *dir = opendir(buffer_tostring(path));
462 - if (dir != NULL) {
463 - struct dirent *ent;
464 - while ((ent = readdir(dir)) != NULL) {
465 - if (ent->d_name[0] == '.')
466 - continue;
467 - if (ent->d_type != DT_REG)
468 - continue;
469 - size_t len = strnlen(ent->d_name, NAME_MAX);
470 - if (len <= strlen(DYN_CONF_CFG_EXT))
471 - continue;
472 - if (strcmp(ent->d_name + len - strlen(DYN_CONF_CFG_EXT), DYN_CONF_CFG_EXT) != 0)
473 - continue;
474 - ent->d_name[len - strlen(DYN_CONF_CFG_EXT)] = '\0';
475 -
476 - struct job *job = job_new();
477 - job->name = strdupz(ent->d_name);
478 - job->module = module;
479 - dictionary_set(module->jobs, job->name, job, sizeof(job));
480 -
481 - deferred_config_push_back(plugin->name, module->name, job->name);
538 + if (localhost) {
539 + // load all jobs from disk
540 + BUFFER *path = buffer_create(DYN_CONF_PATH_MAX, NULL);
541 + buffer_sprintf(path, "%s/%s/%s", DYN_CONF_DIR, plugin->name, module->name);
542 + DIR *dir = opendir(buffer_tostring(path));
543 + if (dir != NULL) {
544 + struct dirent *ent;
545 + while ((ent = readdir(dir)) != NULL) {
546 + if (ent->d_name[0] == '.')
547 + continue;
548 + if (ent->d_type != DT_REG)
549 + continue;
550 + size_t len = strnlen(ent->d_name, NAME_MAX);
551 + if (len <= strlen(DYN_CONF_CFG_EXT))
552 + continue;
553 + if (strcmp(ent->d_name + len - strlen(DYN_CONF_CFG_EXT), DYN_CONF_CFG_EXT) != 0)
554 + continue;
555 + ent->d_name[len - strlen(DYN_CONF_CFG_EXT)] = '\0';
556 +
557 + struct job *job = job_new(ent->d_name);
558 + job->module = module;
559 + job->flags = JOB_FLG_PS_LOADED;
560 + job->type = JOB_TYPE_USER;
561 +
562 + dictionary_set(module->jobs, job->name, job, sizeof(job));
563 +
564 + deferred_config_push_back(plugins_dict, plugin->name, module->name, ent->d_name);
565 + }
566 + closedir(dir);
567 }
483 - closedir(dir);
568 + buffer_free(path);
569 }
485 - buffer_free(path);
570 }
571
572 dictionary_set(plugin->modules, module->name, module, sizeof(module));
@@ -490,11 +574,48 @@ int register_module(struct configurable_plugin *plugin, struct module *module)
574 return 0;
575 }
576
577 +int register_job(DICTIONARY *plugins_dict, const char *plugin_name, const char *module_name, const char *job_name, enum job_type job_type, dyncfg_job_flg_t flags, int ignore_existing)
578 +{
579 + int rc = 1;
580 + const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, plugin_name);
581 + if (plugin_item == NULL) {
582 + error_report("plugin \"%s\" not registered", plugin_name);
583 + return rc;
584 + }
585 + struct configurable_plugin *plugin = dictionary_acquired_item_value(plugin_item);
586 + struct module *mod = get_module_by_name(plugin, module_name);
587 + if (mod == NULL) {
588 + error_report("module \"%s\" not registered", module_name);
589 + goto ERR_EXIT;
590 + }
591 + if (mod->type != MOD_TYPE_ARRAY) {
592 + error_report("module \"%s\" is not an array", module_name);
593 + goto ERR_EXIT;
594 + }
595 + if (get_job_by_name(mod, job_name) != NULL) {
596 + if (!ignore_existing)
597 + error_report("job \"%s\" already registered", job_name);
598 + goto ERR_EXIT;
599 + }
600 +
601 + struct job *job = job_new(job_name);
602 + job->module = mod;
603 + job->flags = flags;
604 + job->type = job_type;
605 +
606 + dictionary_set(mod->jobs, job->name, job, sizeof(job));
607 +
608 + rc = 0;
609 +ERR_EXIT:
610 + dictionary_acquired_item_release(plugins_dict, plugin_item);
611 + return rc;
612 +}
613 +
614 void freez_dyncfg(void *ptr) {
615 freez(ptr);
616 }
617
497 -static void handle_dyncfg_root(struct uni_http_response *resp, int method)
618 +static void handle_dyncfg_root(DICTIONARY *plugins_dict, struct uni_http_response *resp, int method)
619 {
620 if (method != HTTP_METHOD_GET) {
621 resp->content = "method not allowed";
@@ -502,7 +623,7 @@ static void handle_dyncfg_root(struct uni_http_response *resp, int method)
623 resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
624 return;
625 }
505 - json_object *obj = get_list_of_plugins_json();
626 + json_object *obj = get_list_of_plugins_json(plugins_dict);
627 json_object *wrapper = json_object_new_object();
628 json_object_object_add(wrapper, "configurable_plugins", obj);
629 resp->content = strdupz(json_object_to_json_string_ext(wrapper, JSON_C_TO_STRING_PRETTY));
@@ -518,7 +639,7 @@ static void handle_plugin_root(struct uni_http_response *resp, int method, struc
639 switch(method) {
640 case HTTP_METHOD_GET:
641 {
521 - dyncfg_config_t cfg = plugin->get_config_cb(plugin->cb_usr_ctx);
642 + dyncfg_config_t cfg = plugin->get_config_cb(plugin->cb_usr_ctx, plugin->name);
643 resp->content = mallocz(cfg.data_size);
644 memcpy(resp->content, cfg.data, cfg.data_size);
645 resp->status = HTTP_RESP_OK;
@@ -561,8 +682,8 @@ static void handle_plugin_root(struct uni_http_response *resp, int method, struc
682
683 void handle_module_root(struct uni_http_response *resp, int method, struct configurable_plugin *plugin, const char *module, void *post_payload, size_t post_payload_size)
684 {
564 - if (strncmp(module, DYN_CONF_SCHEMA, strlen(DYN_CONF_SCHEMA)) == 0) {
565 - dyncfg_config_t cfg = plugin->get_config_schema_cb(plugin->cb_usr_ctx);
685 + if (strncmp(module, DYN_CONF_SCHEMA, sizeof(DYN_CONF_SCHEMA)) == 0) {
686 + dyncfg_config_t cfg = plugin->get_config_schema_cb(plugin->cb_usr_ctx, plugin->name);
687 resp->content = mallocz(cfg.data_size);
688 memcpy(resp->content, cfg.data, cfg.data_size);
689 resp->status = HTTP_RESP_OK;
@@ -570,7 +691,7 @@ void handle_module_root(struct uni_http_response *resp, int method, struct confi
691 resp->content_length = cfg.data_size;
692 return;
693 }
573 - if (strncmp(module, DYN_CONF_MODULE_LIST, strlen(DYN_CONF_MODULE_LIST)) == 0) {
694 + if (strncmp(module, DYN_CONF_MODULE_LIST, sizeof(DYN_CONF_MODULE_LIST)) == 0) {
695 if (method != HTTP_METHOD_GET) {
696 resp->content = "method not allowed (only GET)";
697 resp->content_length = strlen(resp->content);
@@ -596,7 +717,7 @@ void handle_module_root(struct uni_http_response *resp, int method, struct confi
717 return;
718 }
719 if (method == HTTP_METHOD_GET) {
599 - dyncfg_config_t cfg = mod->get_config_cb(mod->config_cb_usr_ctx, mod->name);
720 + dyncfg_config_t cfg = mod->get_config_cb(mod->config_cb_usr_ctx, plugin->name, mod->name);
721 resp->content = mallocz(cfg.data_size);
722 memcpy(resp->content, cfg.data, cfg.data_size);
723 resp->status = HTTP_RESP_OK;
@@ -651,8 +772,7 @@ static inline void _handle_job_root(struct uni_http_response *resp, int method,
772 .data = post_payload,
773 .data_size = post_payload_size
774 };
654 - job = add_job(mod, job_id, cont);
655 - if (job == NULL) {
775 + if (mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->plugin->name, mod->name, job_id, &cont)) {
776 resp->content = "failed to add job";
777 resp->content_length = strlen(resp->content);
778 resp->status = HTTP_RESP_INTERNAL_SERVER_ERROR;
@@ -672,7 +792,7 @@ static inline void _handle_job_root(struct uni_http_response *resp, int method,
792 switch (method) {
793 case HTTP_METHOD_GET:
794 {
675 - dyncfg_config_t cfg = mod->get_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, job->name);
795 + dyncfg_config_t cfg = mod->get_job_config_cb(mod->job_config_cb_usr_ctx, mod->plugin->name, mod->name, job->name);
796 resp->content = mallocz(cfg.data_size);
797 memcpy(resp->content, cfg.data, cfg.data_size);
798 resp->status = HTTP_RESP_OK;
@@ -692,10 +812,11 @@ static inline void _handle_job_root(struct uni_http_response *resp, int method,
812 .data = post_payload,
813 .data_size = post_payload_size
814 };
695 - if(set_job_config(job, cont)) {
696 - resp->status = HTTP_RESP_BAD_REQUEST;
815 + if (mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->plugin->name, mod->name, job->name, &cont) != SET_CONFIG_ACCEPTED) {
816 + error_report("DYNCFG module \"%s\" rejected config for job \"%s\"", mod->name, job->name);
817 resp->content = "failed to set job config";
818 resp->content_length = strlen(resp->content);
819 + resp->status = HTTP_RESP_INTERNAL_SERVER_ERROR;
820 return;
821 }
822 resp->status = HTTP_RESP_OK;
@@ -726,8 +847,8 @@ static inline void _handle_job_root(struct uni_http_response *resp, int method,
847
848 void handle_job_root(struct uni_http_response *resp, int method, struct module *mod, const char *job_id, void *post_payload, size_t post_payload_size)
849 {
729 - if (strncmp(job_id, DYN_CONF_SCHEMA, strlen(DYN_CONF_SCHEMA)) == 0) {
730 - dyncfg_config_t cfg = mod->get_config_schema_cb(mod->config_cb_usr_ctx, mod->name);
850 + if (strncmp(job_id, DYN_CONF_SCHEMA, sizeof(DYN_CONF_SCHEMA)) == 0) {
851 + dyncfg_config_t cfg = mod->get_config_schema_cb(mod->config_cb_usr_ctx, mod->plugin->name, mod->name);
852 resp->content = mallocz(cfg.data_size);
853 memcpy(resp->content, cfg.data, cfg.data_size);
854 resp->status = HTTP_RESP_OK;
@@ -735,8 +856,8 @@ void handle_job_root(struct uni_http_response *resp, int method, struct module *
856 resp->content_length = cfg.data_size;
857 return;
858 }
738 - if (strncmp(job_id, DYN_CONF_JOB_SCHEMA, strlen(DYN_CONF_JOB_SCHEMA)) == 0) {
739 - dyncfg_config_t cfg = mod->get_job_config_schema_cb(mod->job_config_cb_usr_ctx, mod->name);
859 + if (strncmp(job_id, DYN_CONF_JOB_SCHEMA, sizeof(DYN_CONF_JOB_SCHEMA)) == 0) {
860 + dyncfg_config_t cfg = mod->get_job_config_schema_cb(mod->job_config_cb_usr_ctx, mod->plugin->name, mod->name);
861 resp->content = mallocz(cfg.data_size);
862 memcpy(resp->content, cfg.data, cfg.data_size);
863 resp->status = HTTP_RESP_OK;
@@ -744,7 +865,7 @@ void handle_job_root(struct uni_http_response *resp, int method, struct module *
865 resp->content_length = cfg.data_size;
866 return;
867 }
747 - if (strncmp(job_id, DYN_CONF_JOB_LIST, strlen(DYN_CONF_JOB_LIST)) == 0) {
868 + if (strncmp(job_id, DYN_CONF_JOB_LIST, sizeof(DYN_CONF_JOB_LIST)) == 0) {
869 if (mod->type != MOD_TYPE_ARRAY) {
870 resp->content = "module type is not job_array (can't get the list of jobs)";
871 resp->content_length = strlen(resp->content);
@@ -776,7 +897,7 @@ void handle_job_root(struct uni_http_response *resp, int method, struct module *
897 dictionary_acquired_item_release(mod->jobs, job_item);
898 }
899
779 -struct uni_http_response dyn_conf_process_http_request(int method, const char *plugin, const char *module, const char *job_id, void *post_payload, size_t post_payload_size)
900 +struct uni_http_response dyn_conf_process_http_request(DICTIONARY *plugins_dict, int method, const char *plugin, const char *module, const char *job_id, void *post_payload, size_t post_payload_size)
901 {
902 struct uni_http_response resp = {
903 .status = HTTP_RESP_INTERNAL_SERVER_ERROR,
@@ -785,8 +906,14 @@ struct uni_http_response dyn_conf_process_http_request(int method, const char *p
906 .content_free = NULL,
907 .content_length = 0
908 };
909 +#ifndef NETDATA_TEST_DYNCFG
910 + resp.content = "DYNCFG is disabled (as it is for now developer only feature). This will be enabled by default when ready for technical preview.";
911 + resp.content_length = strlen(resp.content);
912 + resp.status = HTTP_RESP_PRECOND_FAIL;
913 + return resp;
914 +#endif
915 if (plugin == NULL) {
789 - handle_dyncfg_root(&resp, method);
916 + handle_dyncfg_root(plugins_dict, &resp, method);
917 return resp;
918 }
919 const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, plugin);
@@ -814,9 +941,9 @@ struct uni_http_response dyn_conf_process_http_request(int method, const char *p
941 goto EXIT_PLUGIN;
942 }
943 if (mod->type != MOD_TYPE_ARRAY) {
817 - resp.content = "module is not array";
944 + resp.content = "400 - this module is not array type";
945 resp.content_length = strlen(resp.content);
819 - resp.status = HTTP_RESP_NOT_FOUND;
946 + resp.status = HTTP_RESP_BAD_REQUEST;
947 goto EXIT_PLUGIN;
948 }
949 handle_job_root(&resp, method, mod, job_id, post_payload, post_payload_size);
@@ -836,45 +963,53 @@ void plugin_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
963 freez(plugin);
964 }
965
839 -void report_job_status(struct configurable_plugin *plugin, const char *module_name, const char *job_name, enum job_status status, int status_code, char *reason)
966 +// on failure - return NULL - all unlocked, nothing acquired
967 +// on success - return pointer to job item - keep job and plugin acquired and locked!!!
968 +// for caller convenience (to prevent another lock and races)
969 +// caller is responsible to unlock the job and release it when not needed anymore
970 +// this also avoids dependency creep
971 +const DICTIONARY_ITEM *report_job_status_acq_lock(DICTIONARY *plugins_dict, const DICTIONARY_ITEM **plugin_acq_item, DICTIONARY **job_dict, const char *plugin_name, const char *module_name, const char *job_name, enum job_status status, int status_code, char *reason)
972 {
841 - struct job *job = NULL;
842 - const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(plugins_dict, plugin->name);
843 - if (item == NULL) {
844 - netdata_log_error("plugin %s not found", plugin->name);
845 - freez(reason);
846 - return;
973 + *plugin_acq_item = dictionary_get_and_acquire_item(plugins_dict, plugin_name);
974 + if (*plugin_acq_item == NULL) {
975 + netdata_log_error("plugin %s not found", plugin_name);
976 + return NULL;
977 }
848 - struct configurable_plugin *plug = dictionary_acquired_item_value(item);
978 +
979 + struct configurable_plugin *plug = dictionary_acquired_item_value(*plugin_acq_item);
980 struct module *mod = get_module_by_name(plug, module_name);
981 if (mod == NULL) {
982 netdata_log_error("module %s not found", module_name);
852 - goto EXIT_PLUGIN;
983 + dictionary_acquired_item_release(plugins_dict, *plugin_acq_item);
984 + return NULL;
985 }
986 if (mod->type != MOD_TYPE_ARRAY) {
987 netdata_log_error("module %s is not array", module_name);
856 - goto EXIT_PLUGIN;
988 + dictionary_acquired_item_release(plugins_dict, *plugin_acq_item);
989 + return NULL;
990 }
991 + *job_dict = mod->jobs;
992 const DICTIONARY_ITEM *job_item = dictionary_get_and_acquire_item(mod->jobs, job_name);
993 if (job_item == NULL) {
994 netdata_log_error("job %s not found", job_name);
861 - goto EXIT_PLUGIN;
995 + dictionary_acquired_item_release(plugins_dict, *plugin_acq_item);
996 + return NULL;
997 }
863 - job = dictionary_acquired_item_value(job_item);
998 + struct job *job = dictionary_acquired_item_value(job_item);
999 +
1000 + pthread_mutex_lock(&job->lock);
1001 job->status = status;
1002 job->state = status_code;
1003 if (job->reason != NULL) {
1004 freez(job->reason);
1005 }
869 - job->reason = reason;
1006 + job->reason = reason != NULL ? strdupz(reason) : NULL; // reason is optional
1007 job->last_state_update = now_realtime_usec();
1008
872 - dictionary_acquired_item_release(mod->jobs, job_item);
1009 + job->dirty = true;
1010
874 -EXIT_PLUGIN:
875 - if (!job)
876 - freez(reason);
877 - dictionary_acquired_item_release(plugins_dict, item);
1011 + // no unlock and acquired_item_release on success on purpose
1012 + return job_item;
1013 }
1014
1015 int dyn_conf_init(void)
@@ -886,9 +1021,6 @@ int dyn_conf_init(void)
1021 }
1022 }
1023
889 - plugins_dict = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
890 - dictionary_register_delete_callback(plugins_dict, plugin_del_cb, NULL);
891 -
1024 return 0;
1025 }
1026
@@ -916,6 +1048,15 @@ void *dyncfg_main(void *ptr)
1048
1049 while (!netdata_exit) {
1050 struct deferred_cfg_send *dcs = deferred_config_pop(ptr);
1051 + DICTIONARY *plugins_dict = dcs->plugins_dict;
1052 +#ifdef NETDATA_INTERNAL_CHECKS
1053 + if (plugins_dict == NULL) {
1054 + fatal("DYNCFG, plugins_dict is NULL");
1055 + deferred_config_free(dcs);
1056 + continue;
1057 + }
1058 +#endif
1059 +
1060 const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, dcs->plugin_name);
1061 if (plugin_item == NULL) {
1062 error_report("DYNCFG, plugin %s not found", dcs->plugin_name);
@@ -926,21 +1067,21 @@ void *dyncfg_main(void *ptr)
1067 if (dcs->module_name == NULL) {
1068 dyncfg_config_t cfg = load_config(dcs->plugin_name, NULL, NULL);
1069 if (cfg.data != NULL) {
929 - plugin->set_config_cb(plugin->cb_usr_ctx, &cfg);
1070 + plugin->set_config_cb(plugin->cb_usr_ctx, plugin->name, &cfg);
1071 freez(cfg.data);
1072 }
1073 } else if (dcs->job_name == NULL) {
1074 dyncfg_config_t cfg = load_config(dcs->plugin_name, dcs->module_name, NULL);
1075 if (cfg.data != NULL) {
1076 struct module *mod = get_module_by_name(plugin, dcs->module_name);
936 - mod->set_config_cb(mod->config_cb_usr_ctx, mod->name, &cfg);
1077 + mod->set_config_cb(mod->config_cb_usr_ctx, plugin->name, mod->name, &cfg);
1078 freez(cfg.data);
1079 }
1080 } else {
1081 dyncfg_config_t cfg = load_config(dcs->plugin_name, dcs->module_name, dcs->job_name);
1082 if (cfg.data != NULL) {
1083 struct module *mod = get_module_by_name(plugin, dcs->module_name);
943 - mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, dcs->job_name, &cfg);
1084 + mod->set_job_config_cb(mod->job_config_cb_usr_ctx, plugin->name, mod->name, dcs->job_name, &cfg);
1085 freez(cfg.data);
1086 }
1087 }
@@ -951,3 +1092,37 @@ void *dyncfg_main(void *ptr)
1092 netdata_thread_cleanup_pop(1);
1093 return NULL;
1094 }
1095 +
1096 +bool is_dyncfg_function(const char *function_name, uint8_t type) {
1097 + // TODO add hash to speed things up
1098 + if (type & (DYNCFG_FUNCTION_TYPE_GET | DYNCFG_FUNCTION_TYPE_REGULAR)) {
1099 + if (strncmp(function_name, FUNCTION_NAME_GET_PLUGIN_CONFIG, strlen(FUNCTION_NAME_GET_PLUGIN_CONFIG)) == 0)
1100 + return true;
1101 + if (strncmp(function_name, FUNCTION_NAME_GET_PLUGIN_CONFIG_SCHEMA, strlen(FUNCTION_NAME_GET_PLUGIN_CONFIG_SCHEMA)) == 0)
1102 + return true;
1103 + if (strncmp(function_name, FUNCTION_NAME_GET_MODULE_CONFIG, strlen(FUNCTION_NAME_GET_MODULE_CONFIG)) == 0)
1104 + return true;
1105 + if (strncmp(function_name, FUNCTION_NAME_GET_MODULE_CONFIG_SCHEMA, strlen(FUNCTION_NAME_GET_MODULE_CONFIG_SCHEMA)) == 0)
1106 + return true;
1107 + if (strncmp(function_name, FUNCTION_NAME_GET_JOB_CONFIG, strlen(FUNCTION_NAME_GET_JOB_CONFIG)) == 0)
1108 + return true;
1109 + if (strncmp(function_name, FUNCTION_NAME_GET_JOB_CONFIG_SCHEMA, strlen(FUNCTION_NAME_GET_JOB_CONFIG_SCHEMA)) == 0)
1110 + return true;
1111 + }
1112 +
1113 + if (type & (DYNCFG_FUNCTION_TYPE_SET | DYNCFG_FUNCTION_TYPE_PAYLOAD)) {
1114 + if (strncmp(function_name, FUNCTION_NAME_SET_PLUGIN_CONFIG, strlen(FUNCTION_NAME_SET_PLUGIN_CONFIG)) == 0)
1115 + return true;
1116 + if (strncmp(function_name, FUNCTION_NAME_SET_MODULE_CONFIG, strlen(FUNCTION_NAME_SET_MODULE_CONFIG)) == 0)
1117 + return true;
1118 + if (strncmp(function_name, FUNCTION_NAME_SET_JOB_CONFIG, strlen(FUNCTION_NAME_SET_JOB_CONFIG)) == 0)
1119 + return true;
1120 + }
1121 +
1122 + if (type & (DYNCFG_FUNCTION_TYPE_DELETE | DYNCFG_FUNCTION_TYPE_REGULAR)) {
1123 + if (strncmp(function_name, FUNCTION_NAME_DELETE_JOB, strlen(FUNCTION_NAME_DELETE_JOB)) == 0)
1124 + return true;
1125 + }
1126 +
1127 + return false;
1128 +}
libnetdata/dyn_conf/dyn_conf.h
+122 -21
@@ -5,6 +5,21 @@
5
6 #include "../libnetdata.h"
7
8 +#define FUNCTION_NAME_GET_PLUGIN_CONFIG "get_plugin_config"
9 +#define FUNCTION_NAME_GET_PLUGIN_CONFIG_SCHEMA "get_plugin_config_schema"
10 +#define FUNCTION_NAME_GET_MODULE_CONFIG "get_module_config"
11 +#define FUNCTION_NAME_GET_MODULE_CONFIG_SCHEMA "get_module_config_schema"
12 +#define FUNCTION_NAME_GET_JOB_CONFIG "get_job_config"
13 +#define FUNCTION_NAME_GET_JOB_CONFIG_SCHEMA "get_job_config_schema"
14 +#define FUNCTION_NAME_SET_PLUGIN_CONFIG "set_plugin_config"
15 +#define FUNCTION_NAME_SET_MODULE_CONFIG "set_module_config"
16 +#define FUNCTION_NAME_SET_JOB_CONFIG "set_job_config"
17 +#define FUNCTION_NAME_DELETE_JOB "delete_job"
18 +
19 +#define DYNCFG_MAX_WORDS 5
20 +
21 +#define DYNCFG_VFNC_RET_CFG_ACCEPTED (1)
22 +
23 enum module_type {
24 MOD_TYPE_UNKNOWN = 0,
25 MOD_TYPE_ARRAY,
@@ -20,6 +35,18 @@ static inline enum module_type str2_module_type(const char *type_name)
35 return MOD_TYPE_UNKNOWN;
36 }
37
38 +static inline const char *module_type2str(enum module_type type)
39 +{
40 + switch (type) {
41 + case MOD_TYPE_ARRAY:
42 + return "job_array";
43 + case MOD_TYPE_SINGLE:
44 + return "single";
45 + default:
46 + return "unknown";
47 + }
48 +}
49 +
50 struct dyncfg_config {
51 void *data;
52 size_t data_size;
@@ -47,24 +74,75 @@ static inline enum job_status str2job_state(const char *state_name) {
74 return JOB_STATUS_UNKNOWN;
75 }
76
77 +const char *job_status2str(enum job_status status);
78 +
79 enum set_config_result {
80 SET_CONFIG_ACCEPTED = 0,
81 SET_CONFIG_REJECTED,
82 SET_CONFIG_DEFFER
83 };
84
85 +typedef uint32_t dyncfg_job_flg_t;
86 +enum job_flags {
87 + JOB_FLG_PS_LOADED = 1 << 0, // PS abbr. Persistent Storage
88 + JOB_FLG_PLUGIN_PUSHED = 1 << 1, // got it from plugin (e.g. autodiscovered job)
89 + JOB_FLG_STREAMING_PUSHED = 1 << 2, // got it through streaming
90 + JOB_FLG_USER_CREATED = 1 << 3, // user created this job during agent runtime
91 +};
92 +
93 +enum job_type {
94 + JOB_TYPE_UNKNOWN = 0,
95 + JOB_TYPE_STOCK = 1,
96 + JOB_TYPE_USER = 2,
97 + JOB_TYPE_AUTODISCOVERED = 3,
98 +};
99 +
100 +static inline const char* job_type2str(enum job_type type)
101 +{
102 + switch (type) {
103 + case JOB_TYPE_STOCK:
104 + return "stock";
105 + case JOB_TYPE_USER:
106 + return "user";
107 + case JOB_TYPE_AUTODISCOVERED:
108 + return "autodiscovered";
109 + case JOB_TYPE_UNKNOWN:
110 + default:
111 + return "unknown";
112 + }
113 +}
114 +
115 +static inline enum job_type str2job_type(const char *type_name)
116 +{
117 + if (strcmp(type_name, "stock") == 0)
118 + return JOB_TYPE_STOCK;
119 + else if (strcmp(type_name, "user") == 0)
120 + return JOB_TYPE_USER;
121 + else if (strcmp(type_name, "autodiscovered") == 0)
122 + return JOB_TYPE_AUTODISCOVERED;
123 + error_report("Unknown job type: %s", type_name);
124 + return JOB_TYPE_UNKNOWN;
125 +}
126 +
127 struct job
128 {
58 - char *name;
129 + const char *name;
130 + enum job_type type;
131 + struct module *module;
132 +
133 + pthread_mutex_t lock;
134 + // lock protexts only fields below (which are modified during job existence)
135 + // others are static during lifetime of job
136 +
137 + int dirty; // this relates to rrdpush, true if parent has different data than us
138
139 // state reported by plugin
140 + usec_t last_state_update;
141 enum job_status status; // reported by plugin, enum as this has to be interpreted by UI
142 int state; // code reported by plugin which can mean anything plugin wants
143 char *reason; // reported by plugin, can be NULL (optional)
144
65 - usec_t last_state_update;
66 -
67 - struct module *module;
145 + dyncfg_job_flg_t flags;
146 };
147
148 struct module
@@ -76,18 +154,18 @@ struct module
154 struct configurable_plugin *plugin;
155
156 // module config
79 - enum set_config_result (*set_config_cb)(void *usr_ctx, const char *module_name, dyncfg_config_t *cfg);
80 - dyncfg_config_t (*get_config_cb)(void *usr_ctx, const char *name);
81 - dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx, const char *name);
157 + enum set_config_result (*set_config_cb)(void *usr_ctx, const char *plugin_name, const char *module_name, dyncfg_config_t *cfg);
158 + dyncfg_config_t (*get_config_cb)(void *usr_ctx, const char *plugin_name, const char *module_name);
159 + dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx, const char *plugin_name, const char *module_name);
160 void *config_cb_usr_ctx;
161
162 DICTIONARY *jobs;
163
164 // jobs config
87 - dyncfg_config_t (*get_job_config_cb)(void *usr_ctx, const char *module_name, const char *job_name);
88 - dyncfg_config_t (*get_job_config_schema_cb)(void *usr_ctx, const char *module_name);
89 - enum set_config_result (*set_job_config_cb)(void *usr_ctx, const char *module_name, const char *job_name, dyncfg_config_t *cfg);
90 - enum set_config_result (*delete_job_cb)(void *usr_ctx, const char *module_name, const char *job_name);
165 + dyncfg_config_t (*get_job_config_cb)(void *usr_ctx, const char *plugin_name, const char *module_name, const char *job_name);
166 + dyncfg_config_t (*get_job_config_schema_cb)(void *usr_ctx, const char *plugin_name, const char *module_name);
167 + enum set_config_result (*set_job_config_cb)(void *usr_ctx, const char *plugin_name, const char *module_name, const char *job_name, dyncfg_config_t *cfg);
168 + enum set_config_result (*delete_job_cb)(void *usr_ctx, const char *plugin_name, const char *module_name, const char *job_name);
169 void *job_config_cb_usr_ctx;
170 };
171
@@ -97,26 +175,34 @@ struct configurable_plugin {
175 DICTIONARY *modules;
176 const char *schema;
177
100 - dyncfg_config_t (*get_config_cb)(void *usr_ctx);
101 - dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx);
102 - enum set_config_result (*set_config_cb)(void *usr_ctx, dyncfg_config_t *cfg);
178 + dyncfg_config_t (*get_config_cb)(void *usr_ctx, const char *plugin_name);
179 + dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx, const char *plugin_name);
180 + enum set_config_result (*set_config_cb)(void *usr_ctx, const char *plugin_name, dyncfg_config_t *cfg);
181 void *cb_usr_ctx; // context for all callbacks (split if needed in future)
182 };
183
184 // API to be used by plugins
107 -const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin);
108 -void unregister_plugin(const DICTIONARY_ITEM *plugin);
109 -int register_module(struct configurable_plugin *plugin, struct module *module);
185 +const DICTIONARY_ITEM *register_plugin(DICTIONARY *plugins_dict, struct configurable_plugin *plugin, bool localhost);
186 +void unregister_plugin(DICTIONARY *plugins_dict, const DICTIONARY_ITEM *plugin);
187 +int register_module(DICTIONARY *plugins_dict, struct configurable_plugin *plugin, struct module *module, bool localhost);
188 +int register_job(DICTIONARY *plugins_dict, const char *plugin_name, const char *module_name, const char *job_name, enum job_type job_type, dyncfg_job_flg_t flags, int ignore_existing);
189 +
190 +const DICTIONARY_ITEM *report_job_status_acq_lock(DICTIONARY *plugins_dict, const DICTIONARY_ITEM **plugin_acq_item, DICTIONARY **job_dict, const char *plugin_name, const char *module_name, const char *job_name, enum job_status status, int status_code, char *reason);
191
111 -void report_job_status(struct configurable_plugin *plugin, const char *module_name, const char *job_name, enum job_status status, int status_code, char *reason);
192 +void dyn_conf_store_config(const char *function, const char *payload, struct configurable_plugin *plugin);
193 +void unlink_job(const char *plugin_name, const char *module_name, const char *job_name);
194 +void delete_job(struct configurable_plugin *plugin, const char *module_name, const char *job_name);
195 +void delete_job_pname(DICTIONARY *plugins_dict, const char *plugin_name, const char *module_name, const char *job_name);
196
197 // API to be used by the web server(s)
114 -json_object *get_list_of_plugins_json();
115 -struct configurable_plugin *get_plugin_by_name(const char *name);
198 +json_object *get_list_of_plugins_json(DICTIONARY *plugins_dict);
199 +struct configurable_plugin *get_plugin_by_name(DICTIONARY *plugins_dict, const char *name);
200
201 json_object *get_list_of_modules_json(struct configurable_plugin *plugin);
202 struct module *get_module_by_name(struct configurable_plugin *plugin, const char *module_name);
203
204 +json_object *job2json(struct job *job);
205 +
206 // helper struct to make interface between internal webserver and h2o same
207 struct uni_http_response {
208 int status;
@@ -126,11 +212,26 @@ struct uni_http_response {
212 void (*content_free)(void *);
213 };
214
129 -struct uni_http_response dyn_conf_process_http_request(int method, const char *plugin, const char *module, const char *job_id, void *payload, size_t payload_size);
215 +struct uni_http_response dyn_conf_process_http_request(DICTIONARY *plugins_dict, int method, const char *plugin, const char *module, const char *job_id, void *payload, size_t payload_size);
216
217 // API to be used by main netdata process, initialization and destruction etc.
218 int dyn_conf_init(void);
219 void freez_dyncfg(void *ptr);
220 +
221 +#define dyncfg_dictionary_create() dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE)
222 +
223 +void plugin_del_cb(const DICTIONARY_ITEM *item, void *value, void *data);
224 +
225 void *dyncfg_main(void *in);
226
227 +#define DYNCFG_FUNCTION_TYPE_REGULAR (1 << 0)
228 +#define DYNCFG_FUNCTION_TYPE_PAYLOAD (1 << 1)
229 +#define DYNCFG_FUNCTION_TYPE_GET (1 << 2)
230 +#define DYNCFG_FUNCTION_TYPE_SET (1 << 3)
231 +#define DYNCFG_FUNCTION_TYPE_DELETE (1 << 4)
232 +#define DYNCFG_FUNCTION_TYPE_ALL \
233 + (DYNCFG_FUNCTION_TYPE_REGULAR | DYNCFG_FUNCTION_TYPE_PAYLOAD | DYNCFG_FUNCTION_TYPE_GET | DYNCFG_FUNCTION_TYPE_SET | DYNCFG_FUNCTION_TYPE_DELETE)
234 +
235 +bool is_dyncfg_function(const char *function_name, uint8_t type);
236 +
237 #endif //DYN_CONF_H
libnetdata/dyn_conf/tests/sample_test_config.json new
+22
@@ -0,0 +1,22 @@
1 +{
2 + "http_endpoints": {
3 + "parent": {
4 + "host": "127.0.0.1",
5 + "mguid": null,
6 + "port": 20001,
7 + "ssl": false
8 + },
9 + "child": {
10 + "host": "127.0.0.1",
11 + "mguid": "3bc2f7de-1445-11ee-9ed7-3c7c3f21784c",
12 + "port": 19999,
13 + "ssl": false
14 + }
15 + },
16 + "global": {
17 + "test_plugin_name": "external_plugin",
18 + "test_array_module_name": "module_of_the_future",
19 + "test_single_module_name": "module_of_the_future_single_type",
20 + "test_job_name": "fixed_job"
21 + }
22 +}
libnetdata/dyn_conf/tests/sub_tests/test_parent_child.rb new
+192
@@ -0,0 +1,192 @@
1 +class ParentChildTest
2 + @@plugin_cfg = <<~HEREDOC
3 +{ "test" : "true" }
4 +HEREDOC
5 + @@plugin_cfg2 = <<~HEREDOC
6 +{ "asdfgh" : "asdfgh" }
7 +HEREDOC
8 +
9 + @@job_cfg = <<~HEREDOC
10 +{ "i am newly created job" : "true" }
11 +HEREDOC
12 +
13 + def initialize
14 + @parent = $config[:http_endpoints][:parent]
15 + @child = $config[:http_endpoints][:child]
16 + @plugin = $config[:global][:test_plugin_name]
17 + @arry_mod = $config[:global][:test_array_module_name]
18 + @single_mod = $config[:global][:test_single_module_name]
19 + @test_job = $config[:global][:test_job_name]
20 + end
21 + def check_test_plugin_modules_list(host, child = nil)
22 + rc = DynCfgHttpClient.get_plugin_module_list(host, @plugin, child)
23 + assert_eq(rc.code, 200, "as HTTP code for get_module_list request on plugin \"#{@plugin}\"")
24 + modules = nil
25 + assert_nothing_raised do
26 + modules = JSON.parse(rc.parsed_response, symbolize_names: true)
27 + end
28 + assert_has_key?(modules, :modules)
29 + assert_eq(modules[:modules].count, 2, "as number of modules in plugin \"#{@plugin}\"")
30 + modules[:modules].each do |m|
31 + assert_has_key?(m, :name)
32 + assert_has_key?(m, :type)
33 + assert_is_one_of(m[:type], "job_array", "single")
34 + end
35 + assert_eq_str(modules[:modules][0][:name], @arry_mod, "name of first module in plugin \"#{@plugin}\"")
36 + assert_eq_str(modules[:modules][1][:name], @single_mod, "name of second module in plugin \"#{@plugin}\"")
37 + end
38 + def run
39 + TEST_SUITE("Parent/Child plugin config")
40 +
41 + TEST("parent/child/get_plugin_list", "Get child (hops:1) plugin list trough parent")
42 + plugins = DynCfgHttpClient.get_plugin_list(@parent, @child)
43 + assert_eq(plugins.code, 200, "as HTTP code for get_plugin_list request")
44 + assert_nothing_raised do
45 + plugins = JSON.parse(plugins.parsed_response, symbolize_names: true)
46 + end
47 + assert_has_key?(plugins, :configurable_plugins)
48 + assert_array_include?(plugins[:configurable_plugins], @plugin)
49 + PASS()
50 +
51 + TEST("parent/child/(set/get)plugin_config", "Set then get and compare child (hops:1) plugin config trough parent")
52 + rc = DynCfgHttpClient.set_plugin_config(@parent, @plugin, @@plugin_cfg, @child)
53 + assert_eq(rc.code, 200, "as HTTP code for set_plugin_config request")
54 +
55 + rc = DynCfgHttpClient.get_plugin_config(@parent, @plugin, @child)
56 + assert_eq(rc.code, 200, "as HTTP code for get_plugin_config request")
57 + assert_eq_str(rc.parsed_response.chomp!, @@plugin_cfg, "as plugin config")
58 +
59 + # We do this twice with different configs to ensure first config was not loaded from persistent storage (from previous tests)
60 + rc = DynCfgHttpClient.set_plugin_config(@parent, @plugin, @@plugin_cfg2, @child)
61 + assert_eq(rc.code, 200, "as HTTP code for set_plugin_config request 2")
62 +
63 + rc = DynCfgHttpClient.get_plugin_config(@parent, @plugin, @child)
64 + assert_eq(rc.code, 200, "as HTTP code for get_plugin_config request 2")
65 + assert_eq_str(rc.parsed_response.chomp!, @@plugin_cfg2, "set/get plugin config 2")
66 + PASS()
67 +
68 + TEST("child/get_plugin_config", "Get child (hops:0) plugin config and compare with what we got trough parent (set_plugin_config from previous test)")
69 + rc = DynCfgHttpClient.get_plugin_config(@child, @plugin, nil)
70 + assert_eq(rc.code, 200, "as HTTP code for get_plugin_config request")
71 + assert_eq_str(rc.parsed_response.chomp!, @@plugin_cfg2.chomp, "as plugin config")
72 + PASS()
73 +
74 + TEST("parent/child/plugin_module_list", "Get child (hops:1) plugin module list trough parent and check its contents")
75 + check_test_plugin_modules_list(@parent, @child)
76 + PASS()
77 +
78 + TEST("child/plugin_module_list", "Get child (hops:0) plugin module list directly and check its contents")
79 + check_test_plugin_modules_list(@child, nil)
80 + PASS()
81 +
82 + TEST("parent/child/module/jobs", "Get list of jobs from child (hops:1) trough parent and check its contents, check job updates")
83 + rc = DynCfgHttpClient.get_job_list(@parent, @plugin, @arry_mod, @child)
84 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
85 + jobs = nil
86 + assert_nothing_raised do
87 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
88 + end
89 + assert_has_key?(jobs, :jobs)
90 + new_job = jobs[:jobs].find {|i| i[:name] == @test_job}
91 + assert_not_nil(new_job)
92 + assert_has_key?(new_job, :status)
93 + assert_not_eq_str(new_job[:status], "unknown", "job status is other than unknown")
94 + assert_has_key?(new_job, :flags)
95 + assert_array_include?(new_job[:flags], "JOB_FLG_STREAMING_PUSHED")
96 + PASS()
97 +
98 + TEST("child/module/jobs", "Get list of jobs direct from child (hops:0) and check its contents, check job updates")
99 + rc = DynCfgHttpClient.get_job_list(@child, @plugin, @arry_mod, nil)
100 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
101 + jobs = nil
102 + assert_nothing_raised do
103 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
104 + end
105 + assert_has_key?(jobs, :jobs)
106 + new_job = jobs[:jobs].find {|i| i[:name] == @test_job}
107 + assert_not_nil(new_job)
108 + assert_has_key?(new_job, :status)
109 + assert_not_eq_str(new_job[:status], "unknown", "job status is other than unknown")
110 + assert_has_key?(new_job, :flags)
111 +
112 + assert_array_not_include?(new_job[:flags], "JOB_FLG_STREAMING_PUSHED") # this is plugin directly at child so it should not show this flag
113 + PASS()
114 +
115 + TEST("parent/child/single_module/jobs", "Attempt getting list of jobs from child (hops:1) trough parent on single module. Check it fails properly")
116 + rc = DynCfgHttpClient.get_job_list(@parent, @plugin, @single_mod, @child)
117 + assert_eq(rc.code, 400, "as HTTP code for get_jobs request")
118 + assert_eq_str(rc.parsed_response, '400 - this module is not array type', "as HTTP code for get_jobs request on single module")
119 + PASS()
120 +
121 + created_job = SecureRandom.uuid
122 + TEST("parent/child/module/cr_del_job", "Create and delete job on child (hops:1) trough parent")
123 + # create new job
124 + rc = DynCfgHttpClient.create_job(@parent, @plugin, @arry_mod, created_job, @@job_cfg, @child)
125 + assert_eq_http_code(rc, 200, "as HTTP code for create_job request")
126 + # check this job is in job list @parent
127 + rc = DynCfgHttpClient.get_job_list(@parent, @plugin, @arry_mod, @child)
128 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
129 + jobs = nil
130 + assert_nothing_raised do
131 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
132 + end
133 + assert_has_key?(jobs, :jobs)
134 + new_job = jobs[:jobs].find {|i| i[:name] == created_job}
135 + assert_not_nil(new_job)
136 + # check this job is in job list @child
137 + rc = DynCfgHttpClient.get_job_list(@child, @plugin, @arry_mod, nil)
138 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
139 + jobs = nil
140 + assert_nothing_raised do
141 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
142 + end
143 + assert_has_key?(jobs, :jobs)
144 + new_job = jobs[:jobs].find {|i| i[:name] == created_job}
145 + assert_not_nil(new_job)
146 + # check we can get job config back
147 + rc = DynCfgHttpClient.get_job_config(@parent, @plugin, @arry_mod, created_job, @child)
148 + assert_eq(rc.code, 200, "as HTTP code for get_job_config request")
149 + assert_eq_str(rc.parsed_response.chomp!, @@job_cfg, "as job config")
150 + # delete job
151 + rc = DynCfgHttpClient.delete_job(@parent, @plugin, @arry_mod, created_job, @child)
152 + assert_eq(rc.code, 200, "as HTTP code for delete_job request")
153 + # Check it is not in parents job list anymore
154 + rc = DynCfgHttpClient.get_job_list(@parent, @plugin, @arry_mod, @child)
155 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
156 + jobs = nil
157 + assert_nothing_raised do
158 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
159 + end
160 + assert_has_key?(jobs, :jobs)
161 + new_job = jobs[:jobs].find {|i| i[:name] == created_job}
162 + assert_nil(new_job)
163 + # Check it is not in childs job list anymore
164 + rc = DynCfgHttpClient.get_job_list(@child, @plugin, @arry_mod, nil)
165 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
166 + jobs = nil
167 + assert_nothing_raised do
168 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
169 + end
170 + assert_has_key?(jobs, :jobs)
171 + new_job = jobs[:jobs].find {|i| i[:name] == created_job}
172 + assert_nil(new_job)
173 + PASS()
174 +
175 + TEST("parent/child/module/del_undeletable_job", "Try delete job on child (child rejects), check failure case works (hops:1)")
176 + # test if plugin rejects job deletion the job still remains in list as it should
177 + rc = DynCfgHttpClient.delete_job(@parent, @plugin, @arry_mod, @test_job, @child)
178 + assert_eq(rc.code, 500, "as HTTP code for delete_job request")
179 + rc = DynCfgHttpClient.get_job_list(@parent, @plugin, @arry_mod, @child)
180 + assert_eq(rc.code, 200, "as HTTP code for get_jobs request")
181 + jobs = nil
182 + assert_nothing_raised do
183 + jobs = JSON.parse(rc.parsed_response, symbolize_names: true)
184 + end
185 + assert_has_key?(jobs, :jobs)
186 + job = jobs[:jobs].find {|i| i[:name] == @test_job}
187 + assert_not_nil(job)
188 + PASS()
189 + end
190 +end
191 +
192 +ParentChildTest.new.run()
libnetdata/dyn_conf/tests/test_dyncfg.rb new
+266
@@ -0,0 +1,266 @@
1 +#!/usr/bin/env ruby
2 +
3 +require 'json'
4 +require 'httparty'
5 +require 'pastel'
6 +require 'securerandom'
7 +
8 +ARGV.length == 1 or raise "Usage: #{$0} <config file>"
9 +config_file = ARGV[0]
10 +
11 +File.exist?(config_file) or raise "File not found: #{config_file}"
12 +
13 +$config = JSON.parse(File.read(config_file), symbolize_names: true)
14 +
15 +$plugin_name = $config[:global][:test_plugin_name]
16 +$pastel = Pastel.new
17 +
18 +class TestRunner
19 + attr_reader :stats
20 + def initialize
21 + @stats = {
22 + :suites => 0,
23 + :tests => 0,
24 + :assertions => 0
25 + }
26 + @test = nil
27 + end
28 + def add_assertion()
29 + @stats[:assertions] += 1
30 + end
31 + def FAIL(msg, exception = nil, loc = nil)
32 + puts $pastel.red.bold(" ✕ FAIL")
33 + STDERR.print " "
34 + if loc
35 + STDERR.print $pastel.yellow("@#{loc.path}:#{loc.lineno}: ")
36 + else
37 + STDERR.print $pastel.yellow("@#{caller_locations(1, 1).first.path}:#{caller_locations(1, 1).first.lineno}: ")
38 + end
39 + STDERR.puts msg
40 + STDERR.puts exception.full_message(:highlight => true) if exception
41 + STDERR.puts $pastel.yellow(" Backtrace:")
42 + caller.each do |line|
43 + STDERR.puts " #{line}"
44 + end
45 + exit 1
46 + end
47 + def PASS()
48 + STDERR.puts $pastel.green.bold(" ✓ PASS")
49 + @stats[:tests] += 1
50 + @test = nil
51 + end
52 + def TEST_SUITE(name)
53 + puts $pastel.bold("• TEST SUITE: \"#{name}\"")
54 + @stats[:suites] += 1
55 + end
56 + def assert_no_test_running()
57 + unless @test.nil?
58 + STDERR.puts $pastel.red("\nFATAL: Test \"#{@test}\" did not call PASS() or FAIL()!")
59 + exit 1
60 + end
61 + end
62 + def TEST(name, description = nil)
63 + assert_no_test_running()
64 + @test = name
65 + col = 0
66 + txt = " ├─ T: #{name} "
67 + col += txt.length
68 + print $pastel.bold(txt)
69 +
70 + tab = 50
71 + rem = tab - (col % tab)
72 + rem.times do putc ' ' end
73 + col += rem
74 +
75 + if (description)
76 + txt = " - #{description} "
77 + col += txt.length
78 + print txt
79 +
80 + tab = 180
81 + rem = tab - (col % tab)
82 + rem.times do putc '.' end
83 + end
84 + end
85 + def FINALIZE()
86 + assert_no_test_running()
87 + end
88 +end
89 +
90 +$test_runner = TestRunner.new
91 +def FAIL(msg, exception = nil, loc = nil)
92 + $test_runner.FAIL(msg, exception, loc)
93 +end
94 +def PASS()
95 + $test_runner.PASS()
96 +end
97 +def TEST_SUITE(name)
98 + $test_runner.TEST_SUITE(name)
99 +end
100 +def TEST(name, description = nil)
101 + $test_runner.TEST(name, description)
102 +end
103 +
104 +def assert_eq(got, expected, msg = nil)
105 + unless got == expected
106 + FAIL("Expected #{expected}, got #{got} #{msg ? "(#{msg})" : ""}", nil, caller_locations(1, 1).first)
107 + end
108 + $test_runner.add_assertion()
109 +end
110 +def assert_eq_http_code(got, expected, msg = nil)
111 + unless got.code == expected
112 + FAIL("Expected #{expected}, got #{got}. Server \"#{got.parsed_response}\" #{msg ? "(#{msg})" : ""}", nil, caller_locations(1, 1).first)
113 + end
114 + $test_runner.add_assertion()
115 +end
116 +def assert_eq_str(got, expected, msg = nil)
117 + unless got == expected
118 + FAIL("Strings do not match #{msg ? "(#{msg})" : ""}", nil, caller_locations(1, 1).first)
119 + end
120 + $test_runner.add_assertion()
121 +end
122 +def assert_not_eq_str(got, expected, msg = nil)
123 + unless got != expected
124 + FAIL("Strings shoud not match #{msg ? "(#{msg})" : ""}", nil, caller_locations(1, 1).first)
125 + end
126 + $test_runner.add_assertion()
127 +end
128 +def assert_nothing_raised()
129 + begin
130 + yield
131 + rescue Exception => e
132 + FAIL("Unexpected exception of type #{e.class} raised. Msg: \"#{e.message}\"", e, caller_locations(1, 1).first)
133 + end
134 + $test_runner.add_assertion()
135 +end
136 +def assert_has_key?(hash, key)
137 + unless hash.has_key?(key)
138 + FAIL("Expected key \"#{key}\" in hash", nil, caller_locations(1, 1).first)
139 + end
140 + $test_runner.add_assertion()
141 +end
142 +def assert_array_include?(array, value)
143 + unless array.include?(value)
144 + FAIL("Expected array to include \"#{value}\"", nil, caller_locations(1, 1).first)
145 + end
146 + $test_runner.add_assertion()
147 +end
148 +def assert_array_not_include?(array, value)
149 + if array.include?(value)
150 + FAIL("Expected array to not include \"#{value}\"", nil, caller_locations(1, 1).first)
151 + end
152 + $test_runner.add_assertion()
153 +end
154 +def assert_is_one_of(value, *values)
155 + unless values.include?(value)
156 + FAIL("Expected value to be one of #{values.join(", ")}", nil, caller_locations(1, 1).first)
157 + end
158 + $test_runner.add_assertion()
159 +end
160 +def assert_not_nil(value)
161 + if value.nil?
162 + FAIL("Expected value to not be nil", nil, caller_locations(1, 1).first)
163 + end
164 + $test_runner.add_assertion()
165 +end
166 +def assert_nil(value)
167 + unless value.nil?
168 + FAIL("Expected value to be nil", nil, caller_locations(1, 1).first)
169 + end
170 + $test_runner.add_assertion()
171 +end
172 +
173 +
174 +class DynCfgHttpClient
175 + def self.protocol(cfg)
176 + return cfg[:ssl] ? 'https://' : 'http://'
177 + end
178 + def self.url_base(host)
179 + return "#{protocol(host)}#{host[:host]}:#{host[:port]}"
180 + end
181 + def self.get_url_cfg_base(host, child = nil)
182 + url = url_base(host)
183 + url += "/host/#{child[:mguid]}" if child
184 + url += "/api/v2/config"
185 + return url
186 + end
187 + def self.get_url_cfg_plugin(host, plugin, child = nil)
188 + return get_url_cfg_base(host, child) + '/' + plugin
189 + end
190 + def self.get_url_cfg_module(host, plugin, mod, child = nil)
191 + return get_url_cfg_plugin(host, plugin, child) + '/' + mod
192 + end
193 + def self.get_url_cfg_job(host, plugin, mod, job_id, child = nil)
194 + return get_url_cfg_module(host, plugin, mod, child) + "/#{job_id}"
195 + end
196 + def self.get_plugin_list(host, child = nil)
197 + begin
198 + return HTTParty.get(get_url_cfg_base(host, child), verify: false, format: :plain)
199 + rescue => e
200 + FAIL(e.message, e)
201 + end
202 + end
203 + def self.get_plugin_config(host, plugin, child = nil)
204 + begin
205 + return HTTParty.get(get_url_cfg_plugin(host, plugin, child), verify: false)
206 + rescue => e
207 + FAIL(e.message, e)
208 + end
209 + end
210 + def self.set_plugin_config(host, plugin, cfg, child = nil)
211 + begin
212 + return HTTParty.put(get_url_cfg_plugin(host, plugin, child), verify: false, body: cfg)
213 + rescue => e
214 + FAIL(e.message, e)
215 + end
216 + end
217 + def self.get_plugin_module_list(host, plugin, child = nil)
218 + begin
219 + return HTTParty.get(get_url_cfg_plugin(host, plugin, child) + "/modules", verify: false, format: :plain)
220 + rescue => e
221 + FAIL(e.message, e)
222 + end
223 + end
224 + def self.get_job_list(host, plugin, mod, child = nil)
225 + begin
226 + return HTTParty.get(get_url_cfg_module(host, plugin, mod, child) + "/jobs", verify: false, format: :plain)
227 + rescue => e
228 + FAIL(e.message, e)
229 + end
230 + end
231 + def self.create_job(host, plugin, mod, job_id, job_cfg, child = nil)
232 + begin
233 + return HTTParty.post(get_url_cfg_job(host, plugin, mod, job_id, child), verify: false, body: job_cfg)
234 + rescue => e
235 + FAIL(e.message, e)
236 + end
237 + end
238 + def self.delete_job(host, plugin, mod, job_id, child = nil)
239 + begin
240 + return HTTParty.delete(get_url_cfg_job(host, plugin, mod, job_id, child), verify: false)
241 + rescue => e
242 + FAIL(e.message, e)
243 + end
244 + end
245 + def self.get_job_config(host, plugin, mod, job_id, child = nil)
246 + begin
247 + return HTTParty.get(get_url_cfg_job(host, plugin, mod, job_id, child), verify: false, format: :plain)
248 + rescue => e
249 + FAIL(e.message, e)
250 + end
251 + end
252 + def self.set_job_config(host, plugin, mod, job_id, job_cfg, child = nil)
253 + begin
254 + return HTTParty.put(get_url_cfg_job(host, plugin, mod, job_id, child), verify: false, body: job_cfg)
255 + rescue => e
256 + FAIL(e.message, e)
257 + end
258 + end
259 +end
260 +
261 +require_relative 'sub_tests/test_parent_child.rb'
262 +
263 +$test_runner.FINALIZE()
264 +puts $pastel.green.bold("All tests passed!")
265 +puts ("Total #{$test_runner.stats[:assertions]} assertions, #{$test_runner.stats[:tests]} tests in #{$test_runner.stats[:suites]} suites")
266 +exit 0
libnetdata/dyn_conf/tests/test_plugin/test.plugin new
+250
@@ -0,0 +1,250 @@
1 +#!/usr/bin/env ruby
2 +
3 +# bogus chart that we create just so there is at least one chart
4 +CHART_TYPE = 'lines'
5 +UPDATE_EVERY = 1
6 +PRIORITY = 100000
7 +CHART_NAME = 'number_of_processes'
8 +DIMENSION_NAME = 'running'
9 +
10 +$plugin_name = "external_plugin"
11 +$plugin_version = "0.0.1"
12 +$plugin_config = <<-HEREDOC
13 +test_plugin_config
14 +hableba hableba hableba
15 +HEREDOC
16 +
17 +$array_module_name = 'module_of_the_future'
18 +$fixed_job_name = 'fixed_job'
19 +
20 +$modules = {
21 + $array_module_name => {
22 + :type => :job_array,
23 + :jobs => {
24 + $fixed_job_name => {
25 + :type => :fixed,
26 + :config => <<-HEREDOC
27 +fixed_job_config
28 +HEREDOC
29 + },
30 + },
31 + :config => <<-HEREDOC
32 +module_of_the_future_config
33 +HEREDOC
34 + },
35 + "module_of_the_future_single_type" => {
36 + :type => :single,
37 + :jobs => {},
38 + :config => <<-HEREDOC
39 +module_of_the_future_single_type_config
40 +HEREDOC
41 + }
42 +}
43 +
44 +def out(str)
45 + $log.puts "2 NETDATA> #{str}"
46 + $stdout.puts str
47 + $stdout.flush
48 + $log.flush
49 +end
50 +
51 +def log(str)
52 + $log.puts "LOG > #{str}"
53 + $log.flush
54 +end
55 +
56 +#TODO this is AI code, verify
57 +def split_with_quotes(str)
58 + result = []
59 + current_word = ""
60 + in_quotes = false
61 + escaped = false
62 +
63 + str.each_char do |char|
64 + if char == '\\' && !escaped
65 + escaped = true
66 + next
67 + end
68 +
69 + if char == '"' && !escaped
70 + in_quotes = !in_quotes
71 + current_word << char
72 + elsif char == ' ' && !in_quotes
73 + result << current_word unless current_word.empty?
74 + current_word = ""
75 + else
76 + current_word << char
77 + end
78 +
79 + escaped = false
80 + end
81 +
82 + result << current_word unless current_word.empty?
83 +
84 + result
85 +end
86 +
87 +
88 +def print_startup_messages
89 + out "DYNCFG_ENABLE #{$plugin_name}"
90 + $modules.each do |name, module_config|
91 + out "DYNCFG_REGISTER_MODULE #{name} #{module_config[:type]}"
92 + end
93 + out "CHART system.#{CHART_NAME} '' 'Number of running processes' 'processes' processes processes.#{CHART_NAME} #{CHART_TYPE} #{PRIORITY} #{UPDATE_EVERY}"
94 + out "DIMENSION #{DIMENSION_NAME} '' absolute 1 1"
95 +
96 + $modules.each do |mod_name, mod|
97 + next unless mod[:type] == :job_array
98 + mod[:jobs].each do |job_name, job|
99 + next unless job[:type] == :fixed
100 + out "DYNCFG_REGISTER_JOB #{mod_name} #{job_name} stock 0"
101 + out "REPORT_JOB_STATUS #{$array_module_name} #{$fixed_job_name} running 0"
102 + end
103 + end
104 +end
105 +
106 +def function_result(txid, msg, result)
107 + out "FUNCTION_RESULT_BEGIN #{txid} #{result} text/plain 5"
108 + out msg
109 + out "FUNCTION_RESULT_END"
110 +end
111 +
112 +def process_payload_function(params)
113 + log "payload function #{params[:fncname]}, #{params[:fncparams]}"
114 + fnc_name, mod_name, job_name = params[:fncparams]
115 + case fnc_name
116 + when 'set_plugin_config'
117 + $plugin_config = params[:payload]
118 + function_result(params[:txid], "plugin config set", 1)
119 + when 'set_module_config'
120 + mod = $modules[mod_name]
121 + return function_result(params[:txid], "no such module", 0) if mod.nil?
122 + mod[:config] = params[:payload]
123 + function_result(params[:txid], "module config set", 1)
124 + when 'set_job_config'
125 + mod = $modules[mod_name]
126 + return function_result(params[:txid], "no such module", 0) if mod.nil?
127 + job = mod[:jobs][job_name]
128 + if job.nil?
129 + job = Hash.new if job.nil?
130 + job[:type] = :dynamic
131 + mod[:jobs][job_name] = job
132 + end
133 + job[:config] = params[:payload]
134 + function_result(params[:txid], "job config set", 1)
135 + end
136 +end
137 +
138 +def process_function(params)
139 + log "normal function #{params[:fncname]}, #{params[:fncparams]}"
140 + fnc_name, mod_name, job_name = params[:fncparams]
141 + case fnc_name
142 + when 'get_plugin_config'
143 + function_result(params[:txid], $plugin_config, 1)
144 + when 'get_module_config'
145 + return function_result(params[:txid], "no such module", 0) unless $modules.has_key?(mod_name)
146 + function_result(params[:txid], $modules[mod_name][:config], 1)
147 + when 'get_job_config'
148 + mod = $modules[mod_name]
149 + return function_result(params[:txid], "no such module", 0) if mod.nil?
150 + job = mod[:jobs][job_name]
151 + return function_result(params[:txid], "no such job", 0) if job.nil?
152 + function_result(params[:txid], job[:config], 1)
153 + when 'delete_job'
154 + mod = $modules[mod_name]
155 + return function_result(params[:txid], "no such module", 0) if mod.nil?
156 + job = mod[:jobs][job_name]
157 + return function_result(params[:txid], "no such job", 0) if job.nil?
158 + if job[:type] == :fixed
159 + return function_result(params[:txid], "this job can't be deleted", 0)
160 + else
161 + mod[:jobs].delete(job_name)
162 + function_result(params[:txid], "job deleted", 1)
163 + end
164 + end
165 +end
166 +
167 +$inflight_incoming = nil
168 +def process_input(input)
169 + words = split_with_quotes(input)
170 +
171 + unless $inflight_incoming.nil?
172 + if input == "FUNCTION_PAYLOAD_END"
173 + log $inflight_incoming[:payload]
174 + process_payload_function($inflight_incoming)
175 + $inflight_incoming = nil
176 + else
177 + $inflight_incoming[:payload] << input
178 + $inflight_incoming[:payload] << "\n"
179 + end
180 + return
181 + end
182 +
183 + case words[0]
184 + when "FUNCTION", "FUNCTION_PAYLOAD"
185 + params = {}
186 + params[:command] = words[0]
187 + params[:txid] = words[1]
188 + params[:timeout] = words[2].to_i
189 + params[:fncname] = words[3]
190 + params[:fncname] = params[:fncname][1..-2] if params[:fncname].start_with?('"') && params[:fncname].end_with?('"')
191 + if params[:command] == "FUNCTION_PAYLOAD"
192 + $inflight_incoming = Hash.new
193 + params[:fncparams] = split_with_quotes(params[:fncname])
194 + params[:fncname] = params[:fncparams][0]
195 + $inflight_incoming[:txid] = params[:txid]
196 + $inflight_incoming[:fncname] = params[:fncname]
197 + $inflight_incoming[:params] = params
198 + $inflight_incoming[:fncparams] = params[:fncparams]
199 + $inflight_incoming[:payload] = ""
200 + else
201 + params[:fncparams] = split_with_quotes(params[:fncname])
202 + params[:fncname] = params[:fncparams][0]
203 + process_function(params)
204 + end
205 + end
206 +end
207 +
208 +def read_and_output_metric
209 + processes = `ps -e | wc -l`.to_i - 1 # -1 to exclude the header line
210 + timestamp = Time.now.to_i
211 +
212 + puts "BEGIN system.#{CHART_NAME}"
213 + puts "SET #{DIMENSION_NAME} = #{processes}"
214 + puts "END"
215 +end
216 +
217 +def the_main
218 + $stderr.reopen("/tmp/test_plugin_err.log", "w")
219 + $log = File.open("/tmp/test_plugin.log", "w")
220 + $log.puts "Starting plugin"
221 + print_startup_messages
222 + $log.puts "init done"
223 + $log.flush
224 +
225 + last_metric_time = Time.now
226 +
227 + loop do
228 + time_since_last_metric = Time.now - last_metric_time
229 +
230 + # If it's been more than 1 second since we collected metrics, collect them now
231 + if time_since_last_metric >= 1
232 + read_and_output_metric
233 + last_metric_time = Time.now
234 + end
235 +
236 + # Use select to wait for input, but only wait up to the time remaining until we need to collect metrics again
237 + remaining_time = [1 - time_since_last_metric, 0].max
238 + if select([$stdin], nil, nil, remaining_time)
239 + input = $stdin.gets
240 + next if input.class != String
241 + input.chomp!
242 + $log.puts "RAW INPUT< #{input}"
243 + $log.flush
244 + process_input(input)
245 + end
246 + end
247 +end
248 +
249 +
250 +the_main if __FILE__ == $PROGRAM_NAME
streaming/rrdpush.c
+117
@@ -96,6 +96,9 @@ STREAM_CAPABILITIES stream_our_capabilities(RRDHOST *host, bool sender) {
96 STREAM_CAP_BINARY |
97 STREAM_CAP_INTERPOLATED |
98 STREAM_HAS_COMPRESSION |
99 +#ifdef NETDATA_TEST_DYNCFG
100 + STREAM_CAP_DYNCFG |
101 +#endif
102 (ieee754_doubles ? STREAM_CAP_IEEE754 : 0) |
103 (ml_capability ? STREAM_CAP_DATA_WITH_ML : 0) |
104 0;
@@ -465,6 +468,47 @@ void rrdset_push_metrics_finished(RRDSET_STREAM_BUFFER *rsb, RRDSET *st) {
468 *rsb = (RRDSET_STREAM_BUFFER){ .wb = NULL, };
469 }
470
471 +// TODO enable this macro before release
472 +#define bail_if_no_cap(cap) \
473 + if(unlikely(!stream_has_capability(host->sender, cap))) { \
474 + netdata_log_error("STREAM %s [send]: cannot send job status update - parent does not support it.", rrdhost_hostname(host)); \
475 + return; \
476 + }
477 +
478 +#define dyncfg_check_can_push(host) \
479 + if(unlikely(!rrdhost_can_send_definitions_to_parent(host))) \
480 + return; \
481 + bail_if_no_cap(STREAM_CAP_DYNCFG)
482 +
483 +// assumes job is locked and acquired!!!
484 +void rrdpush_send_job_status_update(RRDHOST *host, const char *plugin_name, const char *module_name, struct job *job) {
485 + dyncfg_check_can_push(host);
486 +
487 + BUFFER *wb = sender_start(host->sender);
488 +
489 + buffer_sprintf(wb, PLUGINSD_KEYWORD_REPORT_JOB_STATUS " %s %s %s %s %d\n", plugin_name, module_name, job->name, job_status2str(job->status), job->state);
490 + if (job->reason)
491 + buffer_sprintf(wb, " \"%s\"", job->reason);
492 +
493 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
494 +
495 + sender_thread_buffer_free();
496 +
497 + job->dirty = 0;
498 +}
499 +
500 +void rrdpush_send_job_deleted(RRDHOST *host, const char *plugin_name, const char *module_name, const char *job_name) {
501 + dyncfg_check_can_push(host);
502 +
503 + BUFFER *wb = sender_start(host->sender);
504 +
505 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DELETE_JOB " %s %s %s\n", plugin_name, module_name, job_name);
506 +
507 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
508 +
509 + sender_thread_buffer_free();
510 +}
511 +
512 RRDSET_STREAM_BUFFER rrdset_push_metric_initialize(RRDSET *st, time_t wall_clock_time) {
513 RRDHOST *host = st->rrdhost;
514
@@ -559,6 +603,78 @@ void rrdpush_send_global_functions(RRDHOST *host) {
603 sender_thread_buffer_free();
604 }
605
606 +void rrdpush_send_dyncfg(RRDHOST *host) {
607 + dyncfg_check_can_push(host);
608 +
609 + BUFFER *wb = sender_start(host->sender);
610 +
611 + DICTIONARY *plugins_dict = host->configurable_plugins;
612 +
613 + struct configurable_plugin *plug;
614 + dfe_start_read(plugins_dict, plug) {
615 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_ENABLE " %s\n", plug->name);
616 + struct module *mod;
617 + dfe_start_read(plug->modules, mod) {
618 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE " %s %s %s\n", plug->name, mod->name, module_type2str(mod->type));
619 + struct job *job;
620 + dfe_start_read(mod->jobs, job) {
621 + pthread_mutex_lock(&job->lock);
622 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB " %s %s %s %s %"PRIu32"\n", plug->name, mod->name, job->name, job_type2str(job->type), job->flags);
623 + buffer_sprintf(wb, PLUGINSD_KEYWORD_REPORT_JOB_STATUS " %s %s %s %s %d", plug->name, mod->name, job->name, job_status2str(job->status), job->state);
624 + if (job->reason)
625 + buffer_sprintf(wb, " \"%s\"", job->reason);
626 + buffer_sprintf(wb, "\n");
627 + job->dirty = 0;
628 + pthread_mutex_unlock(&job->lock);
629 + } dfe_done(job);
630 + } dfe_done(mod);
631 + }
632 + dfe_done(plug);
633 +
634 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
635 +
636 + sender_thread_buffer_free();
637 +}
638 +
639 +void rrdpush_send_dyncfg_enable(RRDHOST *host, const char *plugin_name)
640 +{
641 + dyncfg_check_can_push(host);
642 +
643 + BUFFER *wb = sender_start(host->sender);
644 +
645 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_ENABLE " %s\n", plugin_name);
646 +
647 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
648 +
649 + sender_thread_buffer_free();
650 +}
651 +
652 +void rrdpush_send_dyncfg_reg_module(RRDHOST *host, const char *plugin_name, const char *module_name, enum module_type type)
653 +{
654 + dyncfg_check_can_push(host);
655 +
656 + BUFFER *wb = sender_start(host->sender);
657 +
658 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE " %s %s %s\n", plugin_name, module_name, module_type2str(type));
659 +
660 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
661 +
662 + sender_thread_buffer_free();
663 +}
664 +
665 +void rrdpush_send_dyncfg_reg_job(RRDHOST *host, const char *plugin_name, const char *module_name, const char *job_name, enum job_type type, uint32_t flags)
666 +{
667 + dyncfg_check_can_push(host);
668 +
669 + BUFFER *wb = sender_start(host->sender);
670 +
671 + buffer_sprintf(wb, PLUGINSD_KEYWORD_DYNCFG_REGISTER_JOB " %s %s %s %s %"PRIu32"\n", plugin_name, module_name, job_name, job_type2str(type), flags);
672 +
673 + sender_commit(host->sender, wb, STREAM_TRAFFIC_TYPE_METADATA);
674 +
675 + sender_thread_buffer_free();
676 +}
677 +
678 void rrdpush_send_claimed_id(RRDHOST *host) {
679 if(!stream_has_capability(host->sender, STREAM_CAP_CLAIM))
680 return;
@@ -1288,6 +1404,7 @@ static struct {
1404 { STREAM_CAP_INTERPOLATED, "INTERPOLATED" },
1405 { STREAM_CAP_IEEE754, "IEEE754" },
1406 { STREAM_CAP_DATA_WITH_ML, "ML" },
1407 + { STREAM_CAP_DYNCFG, "DYN_CFG" },
1408 { 0 , NULL },
1409 };
1410
streaming/rrdpush.h
+19
@@ -45,6 +45,7 @@ typedef enum {
45 STREAM_CAP_INTERPOLATED = (1 << 14), // streaming supports interpolated streaming of values
46 STREAM_CAP_IEEE754 = (1 << 15), // streaming supports binary/hex transfer of double values
47 STREAM_CAP_DATA_WITH_ML = (1 << 16), // streaming supports transferring anomaly bit
48 + STREAM_CAP_DYNCFG = (1 << 17), // dynamic configuration of plugins trough streaming
49
50 STREAM_CAP_INVALID = (1 << 30), // used as an invalid value for capabilities when this is set
51 // this must be signed int, so don't use the last bit
@@ -232,6 +233,13 @@ typedef enum __attribute__((packed)) {
233 SENDER_FLAG_COMPRESSION = (1 << 1), // The stream needs to have and has compression
234 } SENDER_FLAGS;
235
236 +struct function_payload_state {
237 + BUFFER *payload;
238 + char *txid;
239 + char *fn_name;
240 + char *timeout;
241 +};
242 +
243 struct sender_state {
244 RRDHOST *host;
245 pid_t tid; // the thread id of the sender, from gettid()
@@ -261,6 +269,9 @@ struct sender_state {
269 int rrdpush_sender_pipe[2]; // collector to sender thread signaling
270 int rrdpush_sender_socket;
271
272 + int receiving_function_payload;
273 + struct function_payload_state function_payload; // state when receiving function with payload
274 +
275 uint16_t hops;
276
277 #ifdef ENABLE_RRDPUSH_COMPRESSION
@@ -482,6 +493,7 @@ void *rrdpush_sender_thread(void *ptr);
493 void rrdpush_send_host_labels(RRDHOST *host);
494 void rrdpush_send_claimed_id(RRDHOST *host);
495 void rrdpush_send_global_functions(RRDHOST *host);
496 +void rrdpush_send_dyncfg(RRDHOST *host);
497
498 #define THREAD_TAG_STREAM_RECEIVER "RCVR" // "[host]" is appended
499 #define THREAD_TAG_STREAM_SENDER "SNDR" // "[host]" is appended
@@ -764,4 +776,11 @@ typedef struct rrdhost_status {
776 void rrdhost_status(RRDHOST *host, time_t now, RRDHOST_STATUS *s);
777 bool rrdhost_state_cloud_emulation(RRDHOST *host);
778
779 +void rrdpush_send_job_status_update(RRDHOST *host, const char *plugin_name, const char *module_name, struct job *job);
780 +void rrdpush_send_job_deleted(RRDHOST *host, const char *plugin_name, const char *module_name, const char *job_name);
781 +
782 +void rrdpush_send_dyncfg_enable(RRDHOST *host, const char *plugin_name);
783 +void rrdpush_send_dyncfg_reg_module(RRDHOST *host, const char *plugin_name, const char *module_name, enum module_type type);
784 +void rrdpush_send_dyncfg_reg_job(RRDHOST *host, const char *plugin_name, const char *module_name, const char *job_name, enum job_type type, uint32_t flags);//x
785 +
786 #endif //NETDATA_RRDPUSH_H
streaming/sender.c
+54 -5
@@ -955,6 +955,12 @@ void execute_commands(struct sender_state *s) {
955 while( start < end && (newline = strchr(start, '\n')) ) {
956 *newline = '\0';
957
958 + if (s->receiving_function_payload && unlikely(strcmp(start, PLUGINSD_KEYWORD_FUNCTION_PAYLOAD_END) != 0)) {
959 + buffer_strcat(s->function_payload.payload, start);
960 + start = newline + 1;
961 + continue;
962 + }
963 +
964 netdata_log_access("STREAM: %d from '%s' for host '%s': %s",
965 gettid(), s->connected_to, rrdhost_hostname(s->host), start);
966
@@ -965,12 +971,12 @@ void execute_commands(struct sender_state *s) {
971
972 const char *keyword = get_word(words, num_words, 0);
973
968 - if(keyword && strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION) == 0) {
974 + if(keyword && (strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION) == 0 || strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION_PAYLOAD_END) == 0)) {
975 worker_is_busy(WORKER_SENDER_JOB_FUNCTION_REQUEST);
976
971 - char *transaction = get_word(words, num_words, 1);
972 - char *timeout_s = get_word(words, num_words, 2);
973 - char *function = get_word(words, num_words, 3);
977 + char *transaction = s->receiving_function_payload ? s->function_payload.txid : get_word(words, num_words, 1);
978 + char *timeout_s = s->receiving_function_payload ? s->function_payload.timeout : get_word(words, num_words, 2);
979 + char *function = s->receiving_function_payload ? s->function_payload.fn_name : get_word(words, num_words, 3);
980
981 if(!transaction || !*transaction || !timeout_s || !*timeout_s || !function || !*function) {
982 netdata_log_error("STREAM %s [send to %s] %s execution command is incomplete (transaction = '%s', timeout = '%s', function = '%s'). Ignoring it.",
@@ -990,8 +996,9 @@ void execute_commands(struct sender_state *s) {
996 tmp->transaction = string_strdupz(transaction);
997 BUFFER *wb = buffer_create(PLUGINSD_LINE_MAX + 1, &netdata_buffers_statistics.buffers_functions);
998
999 + char *payload = s->receiving_function_payload ? (char *)buffer_tostring(s->function_payload.payload) : NULL;
1000 int code = rrd_function_run(s->host, wb, timeout, function, false, transaction,
994 - stream_execute_function_callback, tmp, NULL, NULL);
1001 + stream_execute_function_callback, tmp, NULL, NULL, payload);
1002
1003 if(code != HTTP_RESP_OK) {
1004 if (!buffer_strlen(wb))
@@ -1000,6 +1007,47 @@ void execute_commands(struct sender_state *s) {
1007 stream_execute_function_callback(wb, code, tmp);
1008 }
1009 }
1010 +
1011 + if (s->receiving_function_payload) {
1012 + s->receiving_function_payload = false;
1013 +
1014 + buffer_free(s->function_payload.payload);
1015 + freez(s->function_payload.txid);
1016 + freez(s->function_payload.timeout);
1017 + freez(s->function_payload.fn_name);
1018 +
1019 + memset(&s->function_payload, 0, sizeof(struct function_payload_state));
1020 + }
1021 + }
1022 + else if (keyword && strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION_PAYLOAD) == 0) {
1023 + if (s->receiving_function_payload) {
1024 + netdata_log_error("STREAM %s [send to %s] received %s command while already receiving function payload", rrdhost_hostname(s->host), s->connected_to, keyword);
1025 + s->receiving_function_payload = false;
1026 + buffer_free(s->function_payload.payload);
1027 + s->function_payload.payload = NULL;
1028 +
1029 + // TODO send error response
1030 + }
1031 +
1032 + char *transaction = get_word(words, num_words, 1);
1033 + char *timeout_s = get_word(words, num_words, 2);
1034 + char *function = get_word(words, num_words, 3);
1035 +
1036 + if(!transaction || !*transaction || !timeout_s || !*timeout_s || !function || !*function) {
1037 + netdata_log_error("STREAM %s [send to %s] %s execution command is incomplete (transaction = '%s', timeout = '%s', function = '%s'). Ignoring it.",
1038 + rrdhost_hostname(s->host), s->connected_to,
1039 + keyword,
1040 + transaction?transaction:"(unset)",
1041 + timeout_s?timeout_s:"(unset)",
1042 + function?function:"(unset)");
1043 + }
1044 +
1045 + s->receiving_function_payload = true;
1046 + s->function_payload.payload = buffer_create(4096, &netdata_buffers_statistics.buffers_functions);
1047 +
1048 + s->function_payload.txid = strdupz(get_word(words, num_words, 1));
1049 + s->function_payload.timeout = strdupz(get_word(words, num_words, 2));
1050 + s->function_payload.fn_name = strdupz(get_word(words, num_words, 3));
1051 }
1052 else if(keyword && strcmp(keyword, PLUGINSD_KEYWORD_FUNCTION_CANCEL) == 0) {
1053 worker_is_busy(WORKER_SENDER_JOB_FUNCTION_REQUEST);
@@ -1343,6 +1391,7 @@ void *rrdpush_sender_thread(void *ptr) {
1391 rrdpush_send_claimed_id(s->host);
1392 rrdpush_send_host_labels(s->host);
1393 rrdpush_send_global_functions(s->host);
1394 + rrdpush_send_dyncfg(s->host);
1395 s->replication.oldest_request_after_t = 0;
1396
1397 rrdhost_flag_set(s->host, RRDHOST_FLAG_RRDPUSH_SENDER_READY_4_METRICS);
web/api/web_api_v1.c
+1 -1
@@ -1414,7 +1414,7 @@ int web_client_api_request_v1_function(RRDHOST *host, struct web_client *w, char
1414
1415 return rrd_function_run(host, wb, timeout, function, true, NULL,
1416 NULL, NULL,
1417 - web_client_interrupt_callback, w);
1417 + web_client_interrupt_callback, w, NULL);
1418 }
1419
1420 int web_client_api_request_v1_functions(RRDHOST *host, struct web_client *w, char *url __maybe_unused) {
web/api/web_api_v2.c
+114 -2
@@ -669,6 +669,18 @@ static int web_client_api_request_v2_config(RRDHOST *host __maybe_unused, struct
669 char *url_full = url;
670
671 buffer_flush(w->response.data);
672 +
673 + if (strncmp(url, "/host/", strlen("/host/")) == 0) {
674 + url += strlen("/host/");
675 + char *host_id_end = strchr(url, '/');
676 + if (host_id_end == NULL) {
677 + buffer_sprintf(w->response.data, "Invalid URL");
678 + freez(url_full);
679 + return HTTP_RESP_BAD_REQUEST;
680 + }
681 + url += host_id_end - url;
682 + }
683 +
684 if (strncmp(url, CONFIG_API_V2_URL, strlen(CONFIG_API_V2_URL)) != 0) {
685 buffer_sprintf(w->response.data, "Invalid URL");
686 freez(url_full);
@@ -709,7 +721,7 @@ static int web_client_api_request_v2_config(RRDHOST *host __maybe_unused, struct
721 return HTTP_RESP_BAD_REQUEST;
722 }
723
712 - struct uni_http_response resp = dyn_conf_process_http_request(http_method, plugin, module, job_id, w->post_payload, w->post_payload_size);
724 + struct uni_http_response resp = dyn_conf_process_http_request(host->configurable_plugins, http_method, plugin, module, job_id, w->post_payload, w->post_payload_size);
725 if (resp.content[resp.content_length - 1] != '\0') {
726 char *con = mallocz(resp.content_length + 1);
727 memcpy(con, resp.content, resp.content_length);
@@ -727,6 +739,105 @@ static int web_client_api_request_v2_config(RRDHOST *host __maybe_unused, struct
739 return resp.status;
740 }
741
742 +static json_object *job_statuses_grouped() {
743 + json_object *top_obj = json_object_new_object();
744 + json_object *host_vec = json_object_new_array();
745 +
746 +
747 + RRDHOST *host;
748 +
749 + dfe_start_reentrant(rrdhost_root_index, host) {
750 + json_object *host_obj = json_object_new_object();
751 + json_object *host_sub_obj = json_object_new_string(host->machine_guid);
752 + json_object_object_add(host_obj, "host_guid", host_sub_obj);
753 + host_sub_obj = json_object_new_array();
754 +
755 + DICTIONARY *plugins_dict = host->configurable_plugins;
756 +
757 + struct configurable_plugin *plugin;
758 + dfe_start_read(plugins_dict, plugin) {
759 + json_object *plugin_obj = json_object_new_object();
760 + json_object *plugin_sub_obj = json_object_new_string(plugin->name);
761 + json_object_object_add(plugin_obj, "name", plugin_sub_obj);
762 + plugin_sub_obj = json_object_new_array();
763 +
764 + struct module *module;
765 + dfe_start_read(plugin->modules, module) {
766 + json_object *module_obj = json_object_new_object();
767 + json_object *module_sub_obj = json_object_new_string(module->name);
768 + json_object_object_add(module_obj, "name", module_sub_obj);
769 + module_sub_obj = json_object_new_array();
770 +
771 + struct job *job;
772 + dfe_start_read(module->jobs, job) {
773 + json_object *job_obj = json_object_new_object();
774 + json_object *job_sub_obj = json_object_new_string(job->name);
775 + json_object_object_add(job_obj, "name", job_sub_obj);
776 + job_sub_obj = job2json(job);
777 + json_object_object_add(job_obj, "job", job_sub_obj);
778 + json_object_array_add(module_sub_obj, job_obj);
779 + } dfe_done(job);
780 + json_object_object_add(module_obj, "jobs", module_sub_obj);
781 + json_object_array_add(plugin_sub_obj, module_obj);
782 + } dfe_done(module);
783 + json_object_object_add(plugin_obj, "modules", plugin_sub_obj);
784 + json_object_array_add(host_sub_obj, plugin_obj);
785 + } dfe_done(plugin);
786 + json_object_object_add(host_obj, "plugins", host_sub_obj);
787 + json_object_array_add(host_vec, host_obj);
788 + }
789 + dfe_done(host);
790 +
791 + json_object_object_add(top_obj, "hosts", host_vec);
792 + return top_obj;
793 +}
794 +
795 +static json_object *job_statuses_flat() {
796 + RRDHOST *host;
797 +
798 + json_object *ret = json_object_new_array();
799 +
800 + dfe_start_reentrant(rrdhost_root_index, host) {
801 + DICTIONARY *plugins_dict = host->configurable_plugins;
802 +
803 + struct configurable_plugin *plugin;
804 + dfe_start_read(plugins_dict, plugin) {
805 + struct module *module;
806 + dfe_start_read(plugin->modules, module) {
807 + struct job *job;
808 + dfe_start_read(module->jobs, job) {
809 + json_object *job_rich = json_object_new_object();
810 + json_object *obj = json_object_new_string(host->machine_guid);
811 + json_object_object_add(job_rich, "host_guid", obj);
812 + obj = json_object_new_string(plugin->name);
813 + json_object_object_add(job_rich, "plugin_name", obj);
814 + obj = json_object_new_string(module->name);
815 + json_object_object_add(job_rich, "module_name", obj);
816 + obj = job2json(job);
817 + json_object_object_add(job_rich, "job", obj);
818 + json_object_array_add(ret, job_rich);
819 + } dfe_done(job);
820 + } dfe_done(module);
821 + } dfe_done(plugin);
822 + }
823 + dfe_done(host);
824 +
825 + return ret;
826 +}
827 +
828 +static int web_client_api_request_v2_job_statuses(RRDHOST *host, struct web_client *w, char *query) {
829 + json_object *json;
830 + if (strstr(query, "grouped") != NULL)
831 + json = job_statuses_grouped();
832 + else
833 + json = job_statuses_flat();
834 +
835 + buffer_flush(w->response.data);
836 + buffer_strcat(w->response.data, json_object_to_json_string_ext(json, JSON_C_TO_STRING_PRETTY));
837 + w->response.data->content_type = CT_APPLICATION_JSON;
838 + return HTTP_RESP_OK;
839 +}
840 +
841 static struct web_api_command api_commands_v2[] = {
842 {"info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_info, 0},
843
@@ -750,7 +861,8 @@ static struct web_api_command api_commands_v2[] = {
861 {"bearer_protection", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_protection, 0},
862 {"bearer_get_token", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_token, 0},
863
753 - {"config", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_config, 1},
864 + {"config", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_config, 1},
865 + {"job_statuses", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_job_statuses, 0},
866
867 { "ilove.svg", 0, WEB_CLIENT_ACL_NOCHECK, web_client_api_request_v2_ilove, 0 },
868