mirror of
https://github.com/netdata/netdata.git
synced 2025-04-10 16:17:36 +00:00
Dynamic Config MVP0 (#15486)
* work on dynamic configuration interface which should allow adding/modifying monitoring endpoints during runtime
This commit is contained in:
parent
4739bbc9a4
commit
256f22ff09
19 changed files with 1642 additions and 168 deletions
|
@ -200,6 +200,8 @@ LIBNETDATA_FILES = \
|
|||
libnetdata/worker_utilization/worker_utilization.c \
|
||||
libnetdata/worker_utilization/worker_utilization.h \
|
||||
libnetdata/http/http_defs.h \
|
||||
libnetdata/dyn_conf/dyn_conf.c \
|
||||
libnetdata/dyn_conf/dyn_conf.h \
|
||||
$(NULL)
|
||||
|
||||
if ENABLE_PLUGIN_EBPF
|
||||
|
|
|
@ -12,41 +12,44 @@ PARSER_KEYWORD;
|
|||
#
|
||||
# Plugins Only Keywords
|
||||
#
|
||||
FLUSH, 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1
|
||||
DISABLE, 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2
|
||||
EXIT, 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3
|
||||
HOST, 71, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 4
|
||||
HOST_DEFINE, 72, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 5
|
||||
HOST_DEFINE_END, 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6
|
||||
HOST_LABEL, 74, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 7
|
||||
FLUSH, 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1
|
||||
DISABLE, 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2
|
||||
EXIT, 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3
|
||||
HOST, 71, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 4
|
||||
HOST_DEFINE, 72, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 5
|
||||
HOST_DEFINE_END, 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6
|
||||
HOST_LABEL, 74, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 7
|
||||
#
|
||||
# Common keywords
|
||||
#
|
||||
BEGIN, 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8
|
||||
CHART, 32, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 9
|
||||
CLABEL, 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10
|
||||
CLABEL_COMMIT, 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11
|
||||
DIMENSION, 31, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 12
|
||||
END, 13, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 13
|
||||
FUNCTION, 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14
|
||||
FUNCTION_RESULT_BEGIN, 42, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 15
|
||||
LABEL, 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16
|
||||
OVERWRITE, 52, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 17
|
||||
SET, 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 18
|
||||
VARIABLE, 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19
|
||||
BEGIN, 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8
|
||||
CHART, 32, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 9
|
||||
CLABEL, 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10
|
||||
CLABEL_COMMIT, 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11
|
||||
DIMENSION, 31, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 12
|
||||
END, 13, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 13
|
||||
FUNCTION, 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14
|
||||
FUNCTION_RESULT_BEGIN, 42, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 15
|
||||
LABEL, 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16
|
||||
OVERWRITE, 52, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 17
|
||||
SET, 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 18
|
||||
VARIABLE, 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19
|
||||
DYNCFG_ENABLE, 101, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20
|
||||
DYNCFG_REGISTER_MODULE, 102, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21
|
||||
REPORT_JOB_STATUS, 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22
|
||||
#
|
||||
# Streaming only keywords
|
||||
#
|
||||
CLAIMED_ID, 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20
|
||||
BEGIN2, 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21
|
||||
SET2, 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22
|
||||
END2, 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23
|
||||
CLAIMED_ID, 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23
|
||||
BEGIN2, 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24
|
||||
SET2, 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25
|
||||
END2, 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26
|
||||
#
|
||||
# Streaming Replication keywords
|
||||
#
|
||||
CHART_DEFINITION_END, 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24
|
||||
RBEGIN, 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25
|
||||
RDSTATE, 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26
|
||||
REND, 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27
|
||||
RSET, 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28
|
||||
RSSTATE, 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29
|
||||
CHART_DEFINITION_END, 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27
|
||||
RBEGIN, 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28
|
||||
RDSTATE, 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29
|
||||
REND, 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30
|
||||
RSET, 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31
|
||||
RSSTATE, 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32
|
||||
|
|
|
@ -30,12 +30,12 @@
|
|||
#endif
|
||||
|
||||
|
||||
#define GPERF_PARSER_TOTAL_KEYWORDS 29
|
||||
#define GPERF_PARSER_TOTAL_KEYWORDS 32
|
||||
#define GPERF_PARSER_MIN_WORD_LENGTH 3
|
||||
#define GPERF_PARSER_MAX_WORD_LENGTH 21
|
||||
#define GPERF_PARSER_MIN_HASH_VALUE 4
|
||||
#define GPERF_PARSER_MAX_HASH_VALUE 36
|
||||
/* maximum key range = 33, duplicates = 0 */
|
||||
#define GPERF_PARSER_MAX_WORD_LENGTH 22
|
||||
#define GPERF_PARSER_MIN_HASH_VALUE 3
|
||||
#define GPERF_PARSER_MAX_HASH_VALUE 41
|
||||
/* maximum key range = 39, duplicates = 0 */
|
||||
|
||||
#ifdef __GNUC__
|
||||
__inline
|
||||
|
@ -49,99 +49,105 @@ gperf_keyword_hash_function (register const char *str, register size_t len)
|
|||
{
|
||||
static unsigned char asso_values[] =
|
||||
{
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 15, 10, 1, 1, 9,
|
||||
4, 37, 0, 20, 37, 37, 9, 37, 14, 0,
|
||||
37, 37, 1, 0, 37, 7, 13, 37, 18, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37, 37, 37, 37, 37,
|
||||
37, 37, 37, 37, 37, 37
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 16, 7, 2, 11, 0,
|
||||
8, 42, 3, 9, 42, 42, 9, 42, 0, 2,
|
||||
42, 42, 1, 3, 42, 7, 17, 42, 27, 2,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42, 42, 42, 42, 42,
|
||||
42, 42, 42, 42, 42, 42
|
||||
};
|
||||
return len + asso_values[(unsigned char)str[1]] + asso_values[(unsigned char)str[0]];
|
||||
}
|
||||
|
||||
static PARSER_KEYWORD gperf_keywords[] =
|
||||
{
|
||||
{(char*)0}, {(char*)0}, {(char*)0}, {(char*)0},
|
||||
#line 18 "gperf-config.txt"
|
||||
{"HOST", 71, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 4},
|
||||
#line 51 "gperf-config.txt"
|
||||
{"RSET", 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28},
|
||||
#line 26 "gperf-config.txt"
|
||||
{"CHART", 32, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 9},
|
||||
{(char*)0},
|
||||
#line 52 "gperf-config.txt"
|
||||
{"RSSTATE", 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29},
|
||||
#line 49 "gperf-config.txt"
|
||||
{"RDSTATE", 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26},
|
||||
#line 21 "gperf-config.txt"
|
||||
{"HOST_LABEL", 74, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 7},
|
||||
#line 19 "gperf-config.txt"
|
||||
{"HOST_DEFINE", 72, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 5},
|
||||
#line 35 "gperf-config.txt"
|
||||
{"SET", 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 18},
|
||||
#line 42 "gperf-config.txt"
|
||||
{"SET2", 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22},
|
||||
#line 50 "gperf-config.txt"
|
||||
{"REND", 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27},
|
||||
#line 20 "gperf-config.txt"
|
||||
{"HOST_DEFINE_END", 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6},
|
||||
#line 27 "gperf-config.txt"
|
||||
{"CLABEL", 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10},
|
||||
#line 48 "gperf-config.txt"
|
||||
{"RBEGIN", 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25},
|
||||
#line 15 "gperf-config.txt"
|
||||
{"FLUSH", 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1},
|
||||
#line 31 "gperf-config.txt"
|
||||
{"FUNCTION", 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14},
|
||||
#line 40 "gperf-config.txt"
|
||||
{"CLAIMED_ID", 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20},
|
||||
#line 47 "gperf-config.txt"
|
||||
{"CHART_DEFINITION_END", 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24},
|
||||
#line 34 "gperf-config.txt"
|
||||
{"OVERWRITE", 52, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 17},
|
||||
#line 28 "gperf-config.txt"
|
||||
{"CLABEL_COMMIT", 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11},
|
||||
#line 25 "gperf-config.txt"
|
||||
{"BEGIN", 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8},
|
||||
#line 41 "gperf-config.txt"
|
||||
{"BEGIN2", 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21},
|
||||
#line 30 "gperf-config.txt"
|
||||
{"END", 13, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 13},
|
||||
#line 43 "gperf-config.txt"
|
||||
{"END2", 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23},
|
||||
#line 16 "gperf-config.txt"
|
||||
{"DISABLE", 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2},
|
||||
#line 33 "gperf-config.txt"
|
||||
{"LABEL", 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16},
|
||||
#line 29 "gperf-config.txt"
|
||||
{"DIMENSION", 31, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 12},
|
||||
#line 17 "gperf-config.txt"
|
||||
{"EXIT", 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3},
|
||||
#line 32 "gperf-config.txt"
|
||||
{"FUNCTION_RESULT_BEGIN", 42, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 15},
|
||||
{(char*)0}, {(char*)0}, {(char*)0},
|
||||
#line 30 "gperf-config.txt"
|
||||
{"END", 13, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 13},
|
||||
#line 46 "gperf-config.txt"
|
||||
{"END2", 3, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 26},
|
||||
#line 53 "gperf-config.txt"
|
||||
{"REND", 25, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 30},
|
||||
#line 35 "gperf-config.txt"
|
||||
{"SET", 11, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 18},
|
||||
#line 45 "gperf-config.txt"
|
||||
{"SET2", 1, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 25},
|
||||
#line 54 "gperf-config.txt"
|
||||
{"RSET", 21, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 31},
|
||||
#line 18 "gperf-config.txt"
|
||||
{"HOST", 71, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 4},
|
||||
#line 26 "gperf-config.txt"
|
||||
{"CHART", 32, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 9},
|
||||
#line 55 "gperf-config.txt"
|
||||
{"RSSTATE", 24, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 32},
|
||||
#line 25 "gperf-config.txt"
|
||||
{"BEGIN", 12, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 8},
|
||||
#line 44 "gperf-config.txt"
|
||||
{"BEGIN2", 2, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 24},
|
||||
#line 51 "gperf-config.txt"
|
||||
{"RBEGIN", 22, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 28},
|
||||
#line 21 "gperf-config.txt"
|
||||
{"HOST_LABEL", 74, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 7},
|
||||
#line 19 "gperf-config.txt"
|
||||
{"HOST_DEFINE", 72, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 5},
|
||||
#line 27 "gperf-config.txt"
|
||||
{"CLABEL", 34, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 10},
|
||||
#line 39 "gperf-config.txt"
|
||||
{"REPORT_JOB_STATUS", 110, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 22},
|
||||
#line 52 "gperf-config.txt"
|
||||
{"RDSTATE", 23, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 29},
|
||||
#line 20 "gperf-config.txt"
|
||||
{"HOST_DEFINE_END", 73, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 6},
|
||||
#line 43 "gperf-config.txt"
|
||||
{"CLAIMED_ID", 61, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 23},
|
||||
#line 15 "gperf-config.txt"
|
||||
{"FLUSH", 97, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 1},
|
||||
#line 31 "gperf-config.txt"
|
||||
{"FUNCTION", 41, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 14},
|
||||
#line 28 "gperf-config.txt"
|
||||
{"CLABEL_COMMIT", 35, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 11},
|
||||
#line 50 "gperf-config.txt"
|
||||
{"CHART_DEFINITION_END", 33, PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 27},
|
||||
#line 37 "gperf-config.txt"
|
||||
{"DYNCFG_ENABLE", 101, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 20},
|
||||
#line 16 "gperf-config.txt"
|
||||
{"DISABLE", 98, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 2},
|
||||
#line 34 "gperf-config.txt"
|
||||
{"OVERWRITE", 52, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 17},
|
||||
#line 29 "gperf-config.txt"
|
||||
{"DIMENSION", 31, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 12},
|
||||
#line 33 "gperf-config.txt"
|
||||
{"LABEL", 51, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 16},
|
||||
#line 17 "gperf-config.txt"
|
||||
{"EXIT", 99, PARSER_INIT_PLUGINSD, WORKER_PARSER_FIRST_JOB + 3},
|
||||
{(char*)0}, {(char*)0}, {(char*)0},
|
||||
#line 38 "gperf-config.txt"
|
||||
{"DYNCFG_REGISTER_MODULE", 102, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 21},
|
||||
#line 32 "gperf-config.txt"
|
||||
{"FUNCTION_RESULT_BEGIN", 42, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 15},
|
||||
{(char*)0}, {(char*)0}, {(char*)0}, {(char*)0},
|
||||
#line 36 "gperf-config.txt"
|
||||
{"VARIABLE", 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19}
|
||||
{"VARIABLE", 53, PARSER_INIT_PLUGINSD|PARSER_INIT_STREAMING, WORKER_PARSER_FIRST_JOB + 19}
|
||||
};
|
||||
|
||||
PARSER_KEYWORD *
|
||||
|
|
|
@ -43,6 +43,11 @@
|
|||
#define PLUGINSD_KEYWORD_HOST_LABEL "HOST_LABEL"
|
||||
#define PLUGINSD_KEYWORD_HOST "HOST"
|
||||
|
||||
#define PLUGINSD_KEYWORD_DYNCFG_ENABLE "DYNCFG_ENABLE"
|
||||
#define PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE "DYNCFG_REGISTER_MODULE"
|
||||
|
||||
#define PLUGINSD_KEYWORD_REPORT_JOB_STATUS "REPORT_JOB_STATUS"
|
||||
|
||||
#define PLUGINSD_KEYWORD_EXIT "EXIT"
|
||||
|
||||
#define PLUGINS_FUNCTIONS_TIMEOUT_DEFAULT 10 // seconds
|
||||
|
@ -80,6 +85,9 @@ struct plugind {
|
|||
|
||||
time_t started_t;
|
||||
|
||||
const DICTIONARY_ITEM *cfg_dict_item;
|
||||
struct configurable_plugin *configuration;
|
||||
|
||||
struct plugind *prev;
|
||||
struct plugind *next;
|
||||
};
|
||||
|
|
|
@ -726,6 +726,7 @@ struct inflight_function {
|
|||
usec_t timeout_ut;
|
||||
usec_t started_ut;
|
||||
usec_t sent_ut;
|
||||
const char *payload;
|
||||
};
|
||||
|
||||
static void inflight_functions_insert_callback(const DICTIONARY_ITEM *item, void *func, void *parser_ptr) {
|
||||
|
@ -737,7 +738,8 @@ static void inflight_functions_insert_callback(const DICTIONARY_ITEM *item, void
|
|||
pf->code = HTTP_RESP_GATEWAY_TIMEOUT;
|
||||
|
||||
char buffer[2048 + 1];
|
||||
snprintfz(buffer, 2048, "FUNCTION %s %d \"%s\"\n",
|
||||
snprintfz(buffer, 2048, "%s %s %d \"%s\"\n",
|
||||
pf->payload ? "FUNCTION_PAYLOAD" : "FUNCTION",
|
||||
dictionary_acquired_item_name(item),
|
||||
pf->timeout,
|
||||
string2str(pf->function));
|
||||
|
@ -757,6 +759,25 @@ static void inflight_functions_insert_callback(const DICTIONARY_ITEM *item, void
|
|||
string2str(pf->function), dictionary_acquired_item_name(item), ret,
|
||||
pf->sent_ut - pf->started_ut);
|
||||
}
|
||||
|
||||
if (!pf->payload)
|
||||
return;
|
||||
|
||||
// send the payload to the plugin
|
||||
ret = send_to_plugin(pf->payload, parser);
|
||||
|
||||
if(ret < 0) {
|
||||
netdata_log_error("FUNCTION_PAYLOAD: failed to send function to plugin, error %d", ret);
|
||||
rrd_call_function_error(pf->destination_wb, "Failed to communicate with collector", HTTP_RESP_BACKEND_FETCH_FAILED);
|
||||
}
|
||||
else {
|
||||
internal_error(LOG_FUNCTIONS,
|
||||
"FUNCTION_PAYLOAD '%s' with transaction '%s' sent to collector (%d bytes, in %llu usec)",
|
||||
string2str(pf->function), dictionary_acquired_item_name(item), ret,
|
||||
pf->sent_ut - pf->started_ut);
|
||||
}
|
||||
|
||||
send_to_plugin("\nFUNCTION_PAYLOAD_END\n", parser);
|
||||
}
|
||||
|
||||
static bool inflight_functions_conflict_callback(const DICTIONARY_ITEM *item __maybe_unused, void *func __maybe_unused, void *new_func, void *parser_ptr __maybe_unused) {
|
||||
|
@ -827,6 +848,7 @@ static int pluginsd_execute_function_callback(BUFFER *destination_wb, int timeou
|
|||
.function = string_strdupz(function),
|
||||
.callback = callback,
|
||||
.callback_data = callback_data,
|
||||
.payload = NULL
|
||||
};
|
||||
|
||||
uuid_t uuid;
|
||||
|
@ -1834,6 +1856,264 @@ static inline PARSER_RC pluginsd_exit(char **words __maybe_unused, size_t num_wo
|
|||
return PARSER_RC_STOP;
|
||||
}
|
||||
|
||||
struct mutex_cond {
|
||||
pthread_mutex_t lock;
|
||||
pthread_cond_t cond;
|
||||
int rc;
|
||||
};
|
||||
|
||||
static void virt_fnc_got_data_cb(BUFFER *wb, int code, void *callback_data)
|
||||
{
|
||||
struct mutex_cond *ctx = callback_data;
|
||||
pthread_mutex_lock(&ctx->lock);
|
||||
ctx->rc = code;
|
||||
pthread_cond_broadcast(&ctx->cond);
|
||||
pthread_mutex_unlock(&ctx->lock);
|
||||
}
|
||||
|
||||
#define VIRT_FNC_TIMEOUT 1
|
||||
dyncfg_config_t call_virtual_function_blocking(PARSER *parser, const char *name, int *rc, const char *payload) {
|
||||
usec_t now = now_realtime_usec();
|
||||
BUFFER *wb = buffer_create(4096, NULL);
|
||||
|
||||
struct mutex_cond cond = {
|
||||
.lock = PTHREAD_MUTEX_INITIALIZER,
|
||||
.cond = PTHREAD_COND_INITIALIZER
|
||||
};
|
||||
|
||||
struct inflight_function tmp = {
|
||||
.started_ut = now,
|
||||
.timeout_ut = now + VIRT_FNC_TIMEOUT + USEC_PER_SEC,
|
||||
.destination_wb = wb,
|
||||
.timeout = VIRT_FNC_TIMEOUT,
|
||||
.function = string_strdupz(name),
|
||||
.callback = virt_fnc_got_data_cb,
|
||||
.callback_data = &cond,
|
||||
.payload = payload,
|
||||
};
|
||||
|
||||
uuid_t uuid;
|
||||
uuid_generate_time(uuid);
|
||||
|
||||
char key[UUID_STR_LEN];
|
||||
uuid_unparse_lower(uuid, key);
|
||||
|
||||
dictionary_write_lock(parser->inflight.functions);
|
||||
|
||||
// if there is any error, our dictionary callbacks will call the caller callback to notify
|
||||
// the caller about the error - no need for error handling here.
|
||||
dictionary_set(parser->inflight.functions, key, &tmp, sizeof(struct inflight_function));
|
||||
|
||||
if(!parser->inflight.smaller_timeout || tmp.timeout_ut < parser->inflight.smaller_timeout)
|
||||
parser->inflight.smaller_timeout = tmp.timeout_ut;
|
||||
|
||||
// garbage collect stale inflight functions
|
||||
if(parser->inflight.smaller_timeout < now)
|
||||
inflight_functions_garbage_collect(parser, now);
|
||||
|
||||
dictionary_write_unlock(parser->inflight.functions);
|
||||
|
||||
struct timespec tp;
|
||||
clock_gettime(CLOCK_REALTIME, &tp);
|
||||
tp.tv_sec += (time_t)VIRT_FNC_TIMEOUT;
|
||||
|
||||
pthread_mutex_lock(&cond.lock);
|
||||
|
||||
int ret = pthread_cond_timedwait(&cond.cond, &cond.lock, &tp);
|
||||
if (ret == ETIMEDOUT)
|
||||
netdata_log_error("PLUGINSD: DYNCFG virtual function %s timed out", name);
|
||||
|
||||
pthread_mutex_unlock(&cond.lock);
|
||||
|
||||
dyncfg_config_t cfg;
|
||||
cfg.data = strdupz(buffer_tostring(wb));
|
||||
cfg.data_size = buffer_strlen(wb);
|
||||
|
||||
if (rc != NULL)
|
||||
*rc = cond.rc;
|
||||
|
||||
buffer_free(wb);
|
||||
return cfg;
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_plugin_config_cb(void *usr_ctx)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
return call_virtual_function_blocking(parser, "get_plugin_config", NULL, NULL);
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_plugin_config_schema_cb(void *usr_ctx)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
return call_virtual_function_blocking(parser, "get_plugin_config_schema", NULL, NULL);
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_module_config_cb(void *usr_ctx, const char *module_name)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "get_module_config %s", module_name);
|
||||
return call_virtual_function_blocking(parser, buf, NULL, NULL);
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_module_config_schema_cb(void *usr_ctx, const char *module_name)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "get_module_config_schema %s", module_name);
|
||||
return call_virtual_function_blocking(parser, buf, NULL, NULL);
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_job_config_schema_cb(void *usr_ctx, const char *module_name)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "get_job_config_schema %s", module_name);
|
||||
return call_virtual_function_blocking(parser, buf, NULL, NULL);
|
||||
}
|
||||
|
||||
static dyncfg_config_t get_job_config_cb(void *usr_ctx, const char *module_name, const char* job_name)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "get_job_config %s %s", module_name, job_name);
|
||||
return call_virtual_function_blocking(parser, buf, NULL, NULL);
|
||||
}
|
||||
|
||||
enum set_config_result set_plugin_config_cb(void *usr_ctx, dyncfg_config_t *cfg)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
int rc;
|
||||
call_virtual_function_blocking(parser, "set_plugin_config", &rc, cfg->data);
|
||||
if(rc != 1)
|
||||
return SET_CONFIG_REJECTED;
|
||||
return SET_CONFIG_ACCEPTED;
|
||||
}
|
||||
|
||||
enum set_config_result set_module_config_cb(void *usr_ctx, const char *module_name, dyncfg_config_t *cfg)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
int rc;
|
||||
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "set_module_config %s", module_name);
|
||||
call_virtual_function_blocking(parser, buf, &rc, cfg->data);
|
||||
|
||||
if(rc != 1)
|
||||
return SET_CONFIG_REJECTED;
|
||||
return SET_CONFIG_ACCEPTED;
|
||||
}
|
||||
|
||||
enum set_config_result set_job_config_cb(void *usr_ctx, const char *module_name, const char *job_name, dyncfg_config_t *cfg)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
int rc;
|
||||
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "set_job_config %s %s", module_name, job_name);
|
||||
call_virtual_function_blocking(parser, buf, &rc, cfg->data);
|
||||
|
||||
if(rc != 1)
|
||||
return SET_CONFIG_REJECTED;
|
||||
return SET_CONFIG_ACCEPTED;
|
||||
}
|
||||
|
||||
enum set_config_result delete_job_cb(void *usr_ctx, const char *module_name, const char *job_name)
|
||||
{
|
||||
PARSER *parser = usr_ctx;
|
||||
int rc;
|
||||
|
||||
char buf[1024];
|
||||
snprintfz(buf, sizeof(buf), "delete_job %s %s", module_name, job_name);
|
||||
call_virtual_function_blocking(parser, buf, &rc, NULL);
|
||||
|
||||
if(rc != 1)
|
||||
return SET_CONFIG_REJECTED;
|
||||
return SET_CONFIG_ACCEPTED;
|
||||
}
|
||||
|
||||
|
||||
static inline PARSER_RC pluginsd_register_plugin(char **words __maybe_unused, size_t num_words __maybe_unused, PARSER *parser __maybe_unused) {
|
||||
netdata_log_info("PLUGINSD: DYNCFG_ENABLE");
|
||||
|
||||
if (unlikely (num_words != 2))
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_ENABLE, "missing name parameter");
|
||||
|
||||
struct configurable_plugin *cfg = callocz(1, sizeof(struct configurable_plugin));
|
||||
|
||||
cfg->name = strdupz(words[1]);
|
||||
cfg->set_config_cb = set_plugin_config_cb;
|
||||
cfg->get_config_cb = get_plugin_config_cb;
|
||||
cfg->get_config_schema_cb = get_plugin_config_schema_cb;
|
||||
cfg->cb_usr_ctx = parser;
|
||||
|
||||
parser->user.cd->cfg_dict_item = register_plugin(cfg);
|
||||
|
||||
if (unlikely(parser->user.cd->cfg_dict_item == NULL)) {
|
||||
freez(cfg->name);
|
||||
freez(cfg);
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_ENABLE, "error registering plugin");
|
||||
}
|
||||
|
||||
parser->user.cd->configuration = cfg;
|
||||
return PARSER_RC_OK;
|
||||
}
|
||||
|
||||
static inline PARSER_RC pluginsd_register_module(char **words __maybe_unused, size_t num_words __maybe_unused, PARSER *parser __maybe_unused) {
|
||||
netdata_log_info("PLUGINSD: DYNCFG_REG_MODULE");
|
||||
|
||||
struct configurable_plugin *plug_cfg = parser->user.cd->configuration;
|
||||
if (unlikely(plug_cfg == NULL))
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "you have to enable dynamic configuration first using " PLUGINSD_KEYWORD_DYNCFG_ENABLE);
|
||||
|
||||
if (unlikely(num_words != 3))
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "expected 2 parameters module_name followed by module_type");
|
||||
|
||||
struct module *mod = callocz(1, sizeof(struct module));
|
||||
|
||||
mod->type = str2_module_type(words[2]);
|
||||
if (unlikely(mod->type == MOD_TYPE_UNKNOWN)) {
|
||||
freez(mod);
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_DYNCFG_REGISTER_MODULE, "unknown module type (allowed: job_array, single)");
|
||||
}
|
||||
|
||||
mod->name = strdupz(words[1]);
|
||||
|
||||
mod->set_config_cb = set_module_config_cb;
|
||||
mod->get_config_cb = get_module_config_cb;
|
||||
mod->get_config_schema_cb = get_module_config_schema_cb;
|
||||
mod->config_cb_usr_ctx = parser;
|
||||
|
||||
mod->get_job_config_cb = get_job_config_cb;
|
||||
mod->get_job_config_schema_cb = get_job_config_schema_cb;
|
||||
mod->set_job_config_cb = set_job_config_cb;
|
||||
mod->delete_job_cb = delete_job_cb;
|
||||
mod->job_config_cb_usr_ctx = parser;
|
||||
|
||||
register_module(plug_cfg, mod);
|
||||
return PARSER_RC_OK;
|
||||
}
|
||||
|
||||
// job_status <module_name> <job_name> <status_code> <state> <message>
|
||||
static inline PARSER_RC pluginsd_job_status(char **words, size_t num_words, PARSER *parser)
|
||||
{
|
||||
if (unlikely(num_words != 6 && num_words != 5))
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "expected 4 or 5 parameters: module_name, job_name, status_code, state, [optional: message]");
|
||||
|
||||
int state = atoi(words[4]);
|
||||
|
||||
enum job_status job_status = str2job_state(words[3]);
|
||||
if (unlikely(job_status == JOB_STATUS_UNKNOWN))
|
||||
return PLUGINSD_DISABLE_PLUGIN(parser, PLUGINSD_KEYWORD_REPORT_JOB_STATUS, "unknown job state");
|
||||
|
||||
char *message = NULL;
|
||||
if (num_words == 6)
|
||||
message = strdupz(words[5]);
|
||||
|
||||
report_job_status(parser->user.cd->configuration, words[1], words[2], job_status, state, message);
|
||||
return PARSER_RC_OK;
|
||||
}
|
||||
|
||||
static inline PARSER_RC streaming_claimed_id(char **words, size_t num_words, PARSER *parser)
|
||||
{
|
||||
const char *host_uuid_str = get_word(words, num_words, 1);
|
||||
|
@ -2138,6 +2418,12 @@ PARSER_RC parser_execute(PARSER *parser, PARSER_KEYWORD *keyword, char **words,
|
|||
case 99:
|
||||
return pluginsd_exit(words, num_words, parser);
|
||||
|
||||
case 101:
|
||||
return pluginsd_register_plugin(words, num_words, parser);
|
||||
|
||||
case 102:
|
||||
return pluginsd_register_module(words, num_words, parser);
|
||||
|
||||
default:
|
||||
fatal("Unknown keyword '%s' with id %zu", keyword->keyword, keyword->id);
|
||||
}
|
||||
|
@ -2158,6 +2444,11 @@ void parser_destroy(PARSER *parser) {
|
|||
if (unlikely(!parser))
|
||||
return;
|
||||
|
||||
if (parser->user.cd != NULL && parser->user.cd->configuration != NULL) {
|
||||
unregister_plugin(parser->user.cd->cfg_dict_item);
|
||||
parser->user.cd->configuration = NULL;
|
||||
}
|
||||
|
||||
dictionary_destroy(parser->inflight.functions);
|
||||
freez(parser);
|
||||
}
|
||||
|
|
|
@ -2073,6 +2073,8 @@ int main(int argc, char **argv) {
|
|||
signals_block();
|
||||
signals_init(); // setup the signals we want to use
|
||||
|
||||
dyn_conf_init();
|
||||
|
||||
// --------------------------------------------------------------------
|
||||
// check which threads are enabled and initialize them
|
||||
|
||||
|
|
|
@ -195,6 +195,15 @@ const struct netdata_static_thread static_threads_common[] = {
|
|||
.init_routine = NULL,
|
||||
.start_routine = profile_main
|
||||
},
|
||||
{
|
||||
.name = "DYNCFG",
|
||||
.config_section = NULL,
|
||||
.config_name = NULL,
|
||||
.enabled = 1,
|
||||
.thread = NULL,
|
||||
.init_routine = NULL,
|
||||
.start_routine = dyncfg_main
|
||||
},
|
||||
|
||||
// terminator
|
||||
{
|
||||
|
|
906
libnetdata/dyn_conf/dyn_conf.c
Normal file
906
libnetdata/dyn_conf/dyn_conf.c
Normal file
|
@ -0,0 +1,906 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "dyn_conf.h"
|
||||
|
||||
#define DYN_CONF_PATH_MAX (4096)
|
||||
#define DYN_CONF_DIR VARLIB_DIR "/etc"
|
||||
|
||||
#define DYN_CONF_JOB_SCHEMA "job_schema"
|
||||
#define DYN_CONF_SCHEMA "schema"
|
||||
#define DYN_CONF_MODULE_LIST "modules"
|
||||
#define DYN_CONF_JOB_LIST "jobs"
|
||||
#define DYN_CONF_CFG_EXT ".cfg"
|
||||
|
||||
DICTIONARY *plugins_dict = NULL;
|
||||
|
||||
struct deferred_cfg_send {
|
||||
char *plugin_name;
|
||||
char *module_name;
|
||||
char *job_name;
|
||||
struct deferred_cfg_send *next;
|
||||
};
|
||||
|
||||
struct deferred_cfg_send *deferred_configs = NULL;
|
||||
pthread_mutex_t deferred_configs_lock = PTHREAD_MUTEX_INITIALIZER;
|
||||
pthread_cond_t deferred_configs_cond = PTHREAD_COND_INITIALIZER;
|
||||
|
||||
static void deferred_config_push_back(const char *plugin_name, const char *module_name, const char *job_name)
|
||||
{
|
||||
struct deferred_cfg_send *deferred = callocz(1, sizeof(struct deferred_cfg_send));
|
||||
deferred->plugin_name = strdupz(plugin_name);
|
||||
if (module_name != NULL) {
|
||||
deferred->module_name = strdupz(module_name);
|
||||
if (job_name != NULL)
|
||||
deferred->job_name = strdupz(job_name);
|
||||
}
|
||||
pthread_mutex_lock(&deferred_configs_lock);
|
||||
struct deferred_cfg_send *last = deferred_configs;
|
||||
if (last == NULL)
|
||||
deferred_configs = deferred;
|
||||
else {
|
||||
while (last->next != NULL)
|
||||
last = last->next;
|
||||
last->next = deferred;
|
||||
}
|
||||
pthread_cond_signal(&deferred_configs_cond);
|
||||
pthread_mutex_unlock(&deferred_configs_lock);
|
||||
}
|
||||
|
||||
static struct deferred_cfg_send *deferred_config_pop()
|
||||
{
|
||||
pthread_mutex_lock(&deferred_configs_lock);
|
||||
while (deferred_configs == NULL)
|
||||
pthread_cond_wait(&deferred_configs_cond, &deferred_configs_lock);
|
||||
struct deferred_cfg_send *deferred = deferred_configs;
|
||||
deferred_configs = deferred_configs->next;
|
||||
pthread_mutex_unlock(&deferred_configs_lock);
|
||||
return deferred;
|
||||
}
|
||||
|
||||
static void deferred_config_free(struct deferred_cfg_send *dcs)
|
||||
{
|
||||
freez(dcs->plugin_name);
|
||||
freez(dcs->module_name);
|
||||
freez(dcs->job_name);
|
||||
freez(dcs);
|
||||
}
|
||||
|
||||
static int _get_list_of_plugins_json_cb(const DICTIONARY_ITEM *item, void *entry, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
json_object *obj = (json_object *)data;
|
||||
struct configurable_plugin *plugin = (struct configurable_plugin *)entry;
|
||||
|
||||
json_object *plugin_name = json_object_new_string(plugin->name);
|
||||
json_object_array_add(obj, plugin_name);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
json_object *get_list_of_plugins_json()
|
||||
{
|
||||
json_object *obj = json_object_new_array();
|
||||
|
||||
dictionary_walkthrough_read(plugins_dict, _get_list_of_plugins_json_cb, obj);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
static int _get_list_of_modules_json_cb(const DICTIONARY_ITEM *item, void *entry, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
json_object *obj = (json_object *)data;
|
||||
struct module *module = (struct module *)entry;
|
||||
|
||||
json_object *json_module = json_object_new_object();
|
||||
|
||||
json_object *json_item = json_object_new_string(module->name);
|
||||
json_object_object_add(json_module, "name", json_item);
|
||||
const char *module_type;
|
||||
switch (module->type) {
|
||||
case MOD_TYPE_SINGLE:
|
||||
module_type = "single";
|
||||
break;
|
||||
case MOD_TYPE_ARRAY:
|
||||
module_type = "job_array";
|
||||
break;
|
||||
default:
|
||||
module_type = "unknown";
|
||||
break;
|
||||
}
|
||||
json_item = json_object_new_string(module_type);
|
||||
json_object_object_add(json_module, "type", json_item);
|
||||
|
||||
json_object_array_add(obj, json_module);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
json_object *get_list_of_modules_json(struct configurable_plugin *plugin)
|
||||
{
|
||||
json_object *obj = json_object_new_array();
|
||||
|
||||
pthread_mutex_lock(&plugin->lock);
|
||||
|
||||
dictionary_walkthrough_read(plugin->modules, _get_list_of_modules_json_cb, obj);
|
||||
|
||||
pthread_mutex_unlock(&plugin->lock);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
const char *job_status2str(enum job_status status)
|
||||
{
|
||||
switch (status) {
|
||||
case JOB_STATUS_UNKNOWN:
|
||||
return "unknown";
|
||||
case JOB_STATUS_STOPPED:
|
||||
return "stopped";
|
||||
case JOB_STATUS_RUNNING:
|
||||
return "running";
|
||||
case JOB_STATUS_ERROR:
|
||||
return "error";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
static int _get_list_of_jobs_json_cb(const DICTIONARY_ITEM *item, void *entry, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
json_object *obj = (json_object *)data;
|
||||
struct job *job = (struct job *)entry;
|
||||
|
||||
json_object *json_job = json_object_new_object();
|
||||
json_object *json_item = json_object_new_string(job->name);
|
||||
json_object_object_add(json_job, "name", json_item);
|
||||
json_item = json_object_new_string(job_status2str(job->status));
|
||||
json_object_object_add(json_job, "state", json_item);
|
||||
int64_t last_state_update_s = job->last_state_update / USEC_PER_SEC;
|
||||
int64_t last_state_update_us = job->last_state_update % USEC_PER_SEC;
|
||||
|
||||
json_item = json_object_new_int64(last_state_update_s);
|
||||
json_object_object_add(json_job, "last_state_update_s", json_item);
|
||||
|
||||
json_item = json_object_new_int64(last_state_update_us);
|
||||
json_object_object_add(json_job, "last_state_update_us", json_item);
|
||||
|
||||
json_object_array_add(obj, json_job);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
json_object *get_list_of_jobs_json(struct module *module)
|
||||
{
|
||||
json_object *obj = json_object_new_array();
|
||||
|
||||
pthread_mutex_lock(&module->lock);
|
||||
|
||||
dictionary_walkthrough_read(module->jobs, _get_list_of_jobs_json_cb, obj);
|
||||
|
||||
pthread_mutex_unlock(&module->lock);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
struct job *get_job_by_name(struct module *module, const char *job_name)
|
||||
{
|
||||
return dictionary_get(module->jobs, job_name);
|
||||
}
|
||||
|
||||
int remove_job(struct module *module, struct job *job)
|
||||
{
|
||||
// as we are going to do unlink here we better make sure we have all to build proper path
|
||||
if (unlikely(job->name == NULL || module == NULL || module->name == NULL || module->plugin == NULL || module->plugin->name == NULL))
|
||||
return 0;
|
||||
|
||||
enum set_config_result rc = module->delete_job_cb(module->job_config_cb_usr_ctx, module->name, job->name);
|
||||
|
||||
if (rc != SET_CONFIG_ACCEPTED) {
|
||||
error_report("DYNCFG module \"%s\" rejected delete job for \"%s\"", module->name, job->name);
|
||||
return 0;
|
||||
}
|
||||
|
||||
BUFFER *buffer = buffer_create(DYN_CONF_PATH_MAX, NULL);
|
||||
buffer_sprintf(buffer, DYN_CONF_DIR "/%s/%s/%s" DYN_CONF_CFG_EXT, module->plugin->name, module->name, job->name);
|
||||
unlink(buffer_tostring(buffer));
|
||||
buffer_free(buffer);
|
||||
return dictionary_del(module->jobs, job->name);
|
||||
}
|
||||
|
||||
struct module *get_module_by_name(struct configurable_plugin *plugin, const char *module_name)
|
||||
{
|
||||
return dictionary_get(plugin->modules, module_name);
|
||||
}
|
||||
|
||||
inline struct configurable_plugin *get_plugin_by_name(const char *name)
|
||||
{
|
||||
return dictionary_get(plugins_dict, name);
|
||||
}
|
||||
|
||||
static int store_config(const char *module_name, const char *submodule_name, const char *cfg_idx, dyncfg_config_t cfg)
|
||||
{
|
||||
BUFFER *filename = buffer_create(DYN_CONF_PATH_MAX, NULL);
|
||||
buffer_sprintf(filename, DYN_CONF_DIR "/%s", module_name);
|
||||
if (mkdir(buffer_tostring(filename), 0755) == -1) {
|
||||
if (errno != EEXIST) {
|
||||
netdata_log_error("DYNCFG store_config: failed to create module directory %s", buffer_tostring(filename));
|
||||
buffer_free(filename);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (submodule_name != NULL) {
|
||||
buffer_sprintf(filename, "/%s", submodule_name);
|
||||
if (mkdir(buffer_tostring(filename), 0755) == -1) {
|
||||
if (errno != EEXIST) {
|
||||
netdata_log_error("DYNCFG store_config: failed to create submodule directory %s", buffer_tostring(filename));
|
||||
buffer_free(filename);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cfg_idx != NULL)
|
||||
buffer_sprintf(filename, "/%s", cfg_idx);
|
||||
|
||||
buffer_strcat(filename, DYN_CONF_CFG_EXT);
|
||||
|
||||
|
||||
error_report("DYNCFG store_config: %s", buffer_tostring(filename));
|
||||
|
||||
//write to file
|
||||
FILE *f = fopen(buffer_tostring(filename), "w");
|
||||
if (f == NULL) {
|
||||
error_report("DYNCFG store_config: failed to open %s for writing", buffer_tostring(filename));
|
||||
buffer_free(filename);
|
||||
return 1;
|
||||
}
|
||||
|
||||
fwrite(cfg.data, cfg.data_size, 1, f);
|
||||
fclose(f);
|
||||
|
||||
buffer_free(filename);
|
||||
return 0;
|
||||
}
|
||||
|
||||
dyncfg_config_t load_config(const char *plugin_name, const char *module_name, const char *job_id)
|
||||
{
|
||||
BUFFER *filename = buffer_create(DYN_CONF_PATH_MAX, NULL);
|
||||
buffer_sprintf(filename, DYN_CONF_DIR "/%s", plugin_name);
|
||||
if (module_name != NULL)
|
||||
buffer_sprintf(filename, "/%s", module_name);
|
||||
|
||||
if (job_id != NULL)
|
||||
buffer_sprintf(filename, "/%s", job_id);
|
||||
|
||||
buffer_strcat(filename, DYN_CONF_CFG_EXT);
|
||||
|
||||
dyncfg_config_t config;
|
||||
long bytes;
|
||||
config.data = read_by_filename(buffer_tostring(filename), &bytes);
|
||||
|
||||
if (config.data == NULL)
|
||||
error_report("DYNCFG load_config: failed to load config from %s", buffer_tostring(filename));
|
||||
|
||||
config.data_size = bytes;
|
||||
|
||||
buffer_free(filename);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
char *set_plugin_config(struct configurable_plugin *plugin, dyncfg_config_t cfg)
|
||||
{
|
||||
enum set_config_result rc = plugin->set_config_cb(plugin->cb_usr_ctx, &cfg);
|
||||
if (rc != SET_CONFIG_ACCEPTED) {
|
||||
error_report("DYNCFG plugin \"%s\" rejected config", plugin->name);
|
||||
return "plugin rejected config";
|
||||
}
|
||||
|
||||
if (store_config(plugin->name, NULL, NULL, cfg)) {
|
||||
error_report("DYNCFG could not store config for module \"%s\"", plugin->name);
|
||||
return "could not store config on disk";
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static char *set_module_config(struct module *mod, dyncfg_config_t cfg)
|
||||
{
|
||||
struct configurable_plugin *plugin = mod->plugin;
|
||||
|
||||
enum set_config_result rc = mod->set_config_cb(mod->config_cb_usr_ctx, mod->name, &cfg);
|
||||
if (rc != SET_CONFIG_ACCEPTED) {
|
||||
error_report("DYNCFG module \"%s\" rejected config", plugin->name);
|
||||
return "module rejected config";
|
||||
}
|
||||
|
||||
if (store_config(plugin->name, mod->name, NULL, cfg)) {
|
||||
error_report("DYNCFG could not store config for module \"%s\"", mod->name);
|
||||
return "could not store config on disk";
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
struct job *job_new()
|
||||
{
|
||||
struct job *job = callocz(1, sizeof(struct job));
|
||||
job->state = JOB_STATUS_UNKNOWN;
|
||||
job->last_state_update = now_realtime_usec();
|
||||
return job;
|
||||
}
|
||||
|
||||
static int set_job_config(struct job *job, dyncfg_config_t cfg)
|
||||
{
|
||||
struct module *mod = job->module;
|
||||
enum set_config_result rt = mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, job->name, &cfg);
|
||||
|
||||
if (rt != SET_CONFIG_ACCEPTED) {
|
||||
error_report("DYNCFG module \"%s\" rejected config for job \"%s\"", mod->name, job->name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (store_config(mod->plugin->name, mod->name, job->name, cfg)) {
|
||||
error_report("DYNCFG could not store config for module \"%s\"", mod->name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct job *add_job(struct module *mod, const char *job_id, dyncfg_config_t cfg)
|
||||
{
|
||||
struct job *job = job_new();
|
||||
job->name = strdupz(job_id);
|
||||
job->module = mod;
|
||||
|
||||
if (set_job_config(job, cfg)) {
|
||||
freez(job->name);
|
||||
freez(job);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
dictionary_set(mod->jobs, job->name, job, sizeof(job));
|
||||
|
||||
return job;
|
||||
|
||||
}
|
||||
|
||||
void module_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
UNUSED(data);
|
||||
struct module *mod = (struct module *)value;
|
||||
dictionary_destroy(mod->jobs);
|
||||
freez(mod->name);
|
||||
freez(mod);
|
||||
}
|
||||
|
||||
|
||||
const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin)
|
||||
{
|
||||
if (get_plugin_by_name(plugin->name) != NULL) {
|
||||
error_report("DYNCFG plugin \"%s\" already registered", plugin->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (plugin->set_config_cb == NULL) {
|
||||
error_report("DYNCFG plugin \"%s\" has no set_config_cb", plugin->name);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
pthread_mutex_init(&plugin->lock, NULL);
|
||||
|
||||
plugin->modules = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
|
||||
dictionary_register_delete_callback(plugin->modules, module_del_cb, NULL);
|
||||
|
||||
deferred_config_push_back(plugin->name, NULL, NULL);
|
||||
|
||||
dictionary_set(plugins_dict, plugin->name, plugin, sizeof(plugin));
|
||||
|
||||
// the plugin keeps the pointer to the dictionary item, so we need to acquire it
|
||||
return dictionary_get_and_acquire_item(plugins_dict, plugin->name);
|
||||
}
|
||||
|
||||
void unregister_plugin(const DICTIONARY_ITEM *plugin)
|
||||
{
|
||||
struct configurable_plugin *plug = dictionary_acquired_item_value(plugin);
|
||||
dictionary_acquired_item_release(plugins_dict, plugin);
|
||||
dictionary_del(plugins_dict, plug->name);
|
||||
}
|
||||
|
||||
void job_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
UNUSED(data);
|
||||
struct job *job = (struct job *)value;
|
||||
freez(job->reason);
|
||||
freez(job->name);
|
||||
freez(job);
|
||||
}
|
||||
|
||||
int register_module(struct configurable_plugin *plugin, struct module *module)
|
||||
{
|
||||
if (get_module_by_name(plugin, module->name) != NULL) {
|
||||
error_report("DYNCFG module \"%s\" already registered", module->name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
pthread_mutex_init(&module->lock, NULL);
|
||||
|
||||
deferred_config_push_back(plugin->name, module->name, NULL);
|
||||
|
||||
module->plugin = plugin;
|
||||
|
||||
if (module->type == MOD_TYPE_ARRAY) {
|
||||
module->jobs = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
|
||||
dictionary_register_delete_callback(module->jobs, job_del_cb, NULL);
|
||||
|
||||
// load all jobs from disk
|
||||
BUFFER *path = buffer_create(DYN_CONF_PATH_MAX, NULL);
|
||||
buffer_sprintf(path, "%s/%s/%s", DYN_CONF_DIR, plugin->name, module->name);
|
||||
DIR *dir = opendir(buffer_tostring(path));
|
||||
if (dir != NULL) {
|
||||
struct dirent *ent;
|
||||
while ((ent = readdir(dir)) != NULL) {
|
||||
if (ent->d_name[0] == '.')
|
||||
continue;
|
||||
if (ent->d_type != DT_REG)
|
||||
continue;
|
||||
size_t len = strnlen(ent->d_name, NAME_MAX);
|
||||
if (len <= strlen(DYN_CONF_CFG_EXT))
|
||||
continue;
|
||||
if (strcmp(ent->d_name + len - strlen(DYN_CONF_CFG_EXT), DYN_CONF_CFG_EXT) != 0)
|
||||
continue;
|
||||
ent->d_name[len - strlen(DYN_CONF_CFG_EXT)] = '\0';
|
||||
|
||||
struct job *job = job_new();
|
||||
job->name = strdupz(ent->d_name);
|
||||
job->module = module;
|
||||
dictionary_set(module->jobs, job->name, job, sizeof(job));
|
||||
|
||||
deferred_config_push_back(plugin->name, module->name, job->name);
|
||||
}
|
||||
closedir(dir);
|
||||
}
|
||||
buffer_free(path);
|
||||
}
|
||||
|
||||
dictionary_set(plugin->modules, module->name, module, sizeof(module));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
void handle_dyncfg_root(struct uni_http_response *resp, int method)
|
||||
{
|
||||
if (method != HTTP_METHOD_GET) {
|
||||
resp->content = "method not allowed";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
return;
|
||||
}
|
||||
json_object *obj = get_list_of_plugins_json();
|
||||
json_object *wrapper = json_object_new_object();
|
||||
json_object_object_add(wrapper, "configurable_plugins", obj);
|
||||
resp->content = strdupz(json_object_to_json_string_ext(wrapper, JSON_C_TO_STRING_PRETTY));
|
||||
json_object_put(wrapper);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_type = CT_APPLICATION_JSON;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = strlen(resp->content);
|
||||
}
|
||||
|
||||
void handle_plugin_root(struct uni_http_response *resp, int method, struct configurable_plugin *plugin, void *post_payload, size_t post_payload_size)
|
||||
{
|
||||
switch(method) {
|
||||
case HTTP_METHOD_GET:
|
||||
{
|
||||
dyncfg_config_t cfg = plugin->get_config_cb(plugin->cb_usr_ctx);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = free;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
}
|
||||
case HTTP_METHOD_PUT:
|
||||
{
|
||||
char *response;
|
||||
if (post_payload == NULL) {
|
||||
resp->content = "no payload";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
dyncfg_config_t cont = {
|
||||
.data = post_payload,
|
||||
.data_size = post_payload_size
|
||||
};
|
||||
response = set_plugin_config(plugin, cont);
|
||||
if (response == NULL) {
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content = "OK";
|
||||
resp->content_length = strlen(resp->content);
|
||||
} else {
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
resp->content = response;
|
||||
resp->content_length = strlen(resp->content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
default:
|
||||
resp->content = "method not allowed";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (strncmp(module, DYN_CONF_SCHEMA, strlen(DYN_CONF_SCHEMA)) == 0) {
|
||||
dyncfg_config_t cfg = plugin->get_config_schema_cb(plugin->cb_usr_ctx);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
}
|
||||
if (strncmp(module, DYN_CONF_MODULE_LIST, strlen(DYN_CONF_MODULE_LIST)) == 0) {
|
||||
if (method != HTTP_METHOD_GET) {
|
||||
resp->content = "method not allowed (only GET)";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
return;
|
||||
}
|
||||
json_object *obj = get_list_of_modules_json(plugin);
|
||||
json_object *wrapper = json_object_new_object();
|
||||
json_object_object_add(wrapper, "modules", obj);
|
||||
resp->content = strdupz(json_object_to_json_string_ext(wrapper, JSON_C_TO_STRING_PRETTY));
|
||||
json_object_put(wrapper);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_type = CT_APPLICATION_JSON;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
struct module *mod = get_module_by_name(plugin, module);
|
||||
if (mod == NULL) {
|
||||
resp->content = "module not found";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
if (method == HTTP_METHOD_GET) {
|
||||
dyncfg_config_t cfg = mod->get_config_cb(mod->config_cb_usr_ctx, mod->name);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = free;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
} else if (method == HTTP_METHOD_PUT) {
|
||||
char *response;
|
||||
if (post_payload == NULL) {
|
||||
resp->content = "no payload";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
dyncfg_config_t cont = {
|
||||
.data = post_payload,
|
||||
.data_size = post_payload_size
|
||||
};
|
||||
response = set_module_config(mod, cont);
|
||||
if (response == NULL) {
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content = "OK";
|
||||
resp->content_length = strlen(resp->content);
|
||||
} else {
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
resp->content = response;
|
||||
resp->content_length = strlen(resp->content);
|
||||
}
|
||||
return;
|
||||
}
|
||||
resp->content = "method not allowed";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
}
|
||||
|
||||
static inline 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, struct job *job)
|
||||
{
|
||||
if (method == HTTP_METHOD_POST) {
|
||||
if (job != NULL) {
|
||||
resp->content = "can't POST, job already exists (use PUT to update?)";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
if (post_payload == NULL) {
|
||||
resp->content = "no payload";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
dyncfg_config_t cont = {
|
||||
.data = post_payload,
|
||||
.data_size = post_payload_size
|
||||
};
|
||||
job = add_job(mod, job_id, cont);
|
||||
if (job == NULL) {
|
||||
resp->content = "failed to add job";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_INTERNAL_SERVER_ERROR;
|
||||
return;
|
||||
}
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content = "OK";
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
if (job == NULL) {
|
||||
resp->content = "job not found";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
switch (method) {
|
||||
case HTTP_METHOD_GET:
|
||||
{
|
||||
dyncfg_config_t cfg = mod->get_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, job->name);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
}
|
||||
case HTTP_METHOD_PUT:
|
||||
{
|
||||
if (post_payload == NULL) {
|
||||
resp->content = "missing payload";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
return;
|
||||
}
|
||||
dyncfg_config_t cont = {
|
||||
.data = post_payload,
|
||||
.data_size = post_payload_size
|
||||
};
|
||||
if(set_job_config(job, cont)) {
|
||||
resp->status = HTTP_RESP_BAD_REQUEST;
|
||||
resp->content = "failed to set job config";
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content = "OK";
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
case HTTP_METHOD_DELETE:
|
||||
{
|
||||
if (!remove_job(mod, job)) {
|
||||
resp->content = "failed to remove job";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_INTERNAL_SERVER_ERROR;
|
||||
return;
|
||||
}
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content = "OK";
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
default:
|
||||
resp->content = "method not allowed (only GET, PUT, DELETE)";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
if (strncmp(job_id, DYN_CONF_SCHEMA, strlen(DYN_CONF_SCHEMA)) == 0) {
|
||||
dyncfg_config_t cfg = mod->get_config_schema_cb(mod->config_cb_usr_ctx, mod->name);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
}
|
||||
if (strncmp(job_id, DYN_CONF_JOB_SCHEMA, strlen(DYN_CONF_JOB_SCHEMA)) == 0) {
|
||||
dyncfg_config_t cfg = mod->get_job_config_schema_cb(mod->job_config_cb_usr_ctx, mod->name);
|
||||
resp->content = mallocz(cfg.data_size);
|
||||
memcpy(resp->content, cfg.data, cfg.data_size);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = cfg.data_size;
|
||||
return;
|
||||
}
|
||||
if (strncmp(job_id, DYN_CONF_JOB_LIST, strlen(DYN_CONF_JOB_LIST)) == 0) {
|
||||
if (mod->type != MOD_TYPE_ARRAY) {
|
||||
resp->content = "module type is not job_array (can't get the list of jobs)";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_NOT_FOUND;
|
||||
return;
|
||||
}
|
||||
if (method != HTTP_METHOD_GET) {
|
||||
resp->content = "method not allowed (only GET)";
|
||||
resp->content_length = strlen(resp->content);
|
||||
resp->status = HTTP_RESP_METHOD_NOT_ALLOWED;
|
||||
return;
|
||||
}
|
||||
json_object *obj = get_list_of_jobs_json(mod);
|
||||
json_object *wrapper = json_object_new_object();
|
||||
json_object_object_add(wrapper, "jobs", obj);
|
||||
resp->content = strdupz(json_object_to_json_string_ext(wrapper, JSON_C_TO_STRING_PRETTY));
|
||||
json_object_put(wrapper);
|
||||
resp->status = HTTP_RESP_OK;
|
||||
resp->content_type = CT_APPLICATION_JSON;
|
||||
resp->content_free = freez;
|
||||
resp->content_length = strlen(resp->content);
|
||||
return;
|
||||
}
|
||||
const DICTIONARY_ITEM *job_item = dictionary_get_and_acquire_item(mod->jobs, job_id);
|
||||
struct job *job = dictionary_acquired_item_value(job_item);
|
||||
|
||||
_handle_job_root(resp, method, mod, job_id, post_payload, post_payload_size, job);
|
||||
|
||||
dictionary_acquired_item_release(mod->jobs, job_item);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
struct uni_http_response resp = {
|
||||
.status = HTTP_RESP_INTERNAL_SERVER_ERROR,
|
||||
.content_type = CT_TEXT_PLAIN,
|
||||
.content = HTTP_RESP_INTERNAL_SERVER_ERROR_STR,
|
||||
.content_free = NULL,
|
||||
.content_length = 0
|
||||
};
|
||||
if (plugin == NULL) {
|
||||
handle_dyncfg_root(&resp, method);
|
||||
return resp;
|
||||
}
|
||||
const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, plugin);
|
||||
if (plugin_item == NULL) {
|
||||
resp.content = "plugin not found";
|
||||
resp.content_length = strlen(resp.content);
|
||||
resp.status = HTTP_RESP_NOT_FOUND;
|
||||
return resp;
|
||||
}
|
||||
struct configurable_plugin *plug = dictionary_acquired_item_value(plugin_item);
|
||||
if (module == NULL) {
|
||||
handle_plugin_root(&resp, method, plug, post_payload, post_payload_size);
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
if (job_id == NULL) {
|
||||
handle_module_root(&resp, method, plug, module, post_payload, post_payload_size);
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
// for modules we do not do get_and_acquire as modules are never removed (only together with the plugin)
|
||||
struct module *mod = get_module_by_name(plug, module);
|
||||
if (mod == NULL) {
|
||||
resp.content = "module not found";
|
||||
resp.content_length = strlen(resp.content);
|
||||
resp.status = HTTP_RESP_NOT_FOUND;
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
if (mod->type != MOD_TYPE_ARRAY) {
|
||||
resp.content = "module is not array";
|
||||
resp.content_length = strlen(resp.content);
|
||||
resp.status = HTTP_RESP_NOT_FOUND;
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
handle_job_root(&resp, method, mod, job_id, post_payload, post_payload_size);
|
||||
|
||||
EXIT_PLUGIN:
|
||||
dictionary_acquired_item_release(plugins_dict, plugin_item);
|
||||
return resp;
|
||||
}
|
||||
|
||||
void plugin_del_cb(const DICTIONARY_ITEM *item, void *value, void *data)
|
||||
{
|
||||
UNUSED(item);
|
||||
UNUSED(data);
|
||||
struct configurable_plugin *plugin = (struct configurable_plugin *)value;
|
||||
dictionary_destroy(plugin->modules);
|
||||
freez(plugin->name);
|
||||
freez(plugin);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(plugins_dict, plugin->name);
|
||||
if (item == NULL) {
|
||||
netdata_log_error("plugin %s not found", plugin->name);
|
||||
return;
|
||||
}
|
||||
struct configurable_plugin *plug = dictionary_acquired_item_value(item);
|
||||
struct module *mod = get_module_by_name(plug, module_name);
|
||||
if (mod == NULL) {
|
||||
netdata_log_error("module %s not found", module_name);
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
if (mod->type != MOD_TYPE_ARRAY) {
|
||||
netdata_log_error("module %s is not array", module_name);
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
const DICTIONARY_ITEM *job_item = dictionary_get_and_acquire_item(mod->jobs, job_name);
|
||||
if (job_item == NULL) {
|
||||
netdata_log_error("job %s not found", job_name);
|
||||
goto EXIT_PLUGIN;
|
||||
}
|
||||
struct job *job = dictionary_acquired_item_value(job_item);
|
||||
job->status = status;
|
||||
job->state = status_code;
|
||||
if (job->reason != NULL) {
|
||||
freez(job->reason);
|
||||
}
|
||||
job->reason = reason;
|
||||
job->last_state_update = now_realtime_usec();
|
||||
|
||||
dictionary_acquired_item_release(mod->jobs, job_item);
|
||||
|
||||
EXIT_PLUGIN:
|
||||
dictionary_acquired_item_release(plugins_dict, item);
|
||||
}
|
||||
|
||||
int dyn_conf_init(void)
|
||||
{
|
||||
if (mkdir(DYN_CONF_DIR, 0755) == -1) {
|
||||
if (errno != EEXIST) {
|
||||
netdata_log_error("failed to create directory for dynamic configuration");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
plugins_dict = dictionary_create(DICT_OPTION_VALUE_LINK_DONT_CLONE);
|
||||
dictionary_register_delete_callback(plugins_dict, plugin_del_cb, NULL);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void *dyncfg_main(void *in)
|
||||
{
|
||||
while (!netdata_exit) {
|
||||
struct deferred_cfg_send *dcs = deferred_config_pop();
|
||||
const DICTIONARY_ITEM *plugin_item = dictionary_get_and_acquire_item(plugins_dict, dcs->plugin_name);
|
||||
if (plugin_item == NULL) {
|
||||
error_report("DYNCFG, plugin %s not found", dcs->plugin_name);
|
||||
deferred_config_free(dcs);
|
||||
continue;
|
||||
}
|
||||
struct configurable_plugin *plugin = dictionary_acquired_item_value(plugin_item);
|
||||
if (dcs->module_name == NULL) {
|
||||
dyncfg_config_t cfg = load_config(dcs->plugin_name, NULL, NULL);
|
||||
if (cfg.data != NULL) {
|
||||
plugin->set_config_cb(plugin->cb_usr_ctx, &cfg);
|
||||
freez(cfg.data);
|
||||
}
|
||||
} else if (dcs->job_name == NULL) {
|
||||
dyncfg_config_t cfg = load_config(dcs->plugin_name, dcs->module_name, NULL);
|
||||
if (cfg.data != NULL) {
|
||||
struct module *mod = get_module_by_name(plugin, dcs->module_name);
|
||||
mod->set_config_cb(mod->config_cb_usr_ctx, mod->name, &cfg);
|
||||
freez(cfg.data);
|
||||
}
|
||||
} else {
|
||||
dyncfg_config_t cfg = load_config(dcs->plugin_name, dcs->module_name, dcs->job_name);
|
||||
if (cfg.data != NULL) {
|
||||
struct module *mod = get_module_by_name(plugin, dcs->module_name);
|
||||
mod->set_job_config_cb(mod->job_config_cb_usr_ctx, mod->name, dcs->job_name, &cfg);
|
||||
freez(cfg.data);
|
||||
}
|
||||
}
|
||||
deferred_config_free(dcs);
|
||||
dictionary_acquired_item_release(plugins_dict, plugin_item);
|
||||
}
|
||||
return NULL;
|
||||
}
|
136
libnetdata/dyn_conf/dyn_conf.h
Normal file
136
libnetdata/dyn_conf/dyn_conf.h
Normal file
|
@ -0,0 +1,136 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#ifndef DYN_CONF_H
|
||||
#define DYN_CONF_H
|
||||
|
||||
#include "../libnetdata.h"
|
||||
|
||||
enum module_type {
|
||||
MOD_TYPE_UNKNOWN = 0,
|
||||
MOD_TYPE_ARRAY,
|
||||
MOD_TYPE_SINGLE
|
||||
};
|
||||
|
||||
static inline enum module_type str2_module_type(const char *type_name)
|
||||
{
|
||||
if (strcmp(type_name, "job_array") == 0)
|
||||
return MOD_TYPE_ARRAY;
|
||||
else if (strcmp(type_name, "single") == 0)
|
||||
return MOD_TYPE_SINGLE;
|
||||
return MOD_TYPE_UNKNOWN;
|
||||
}
|
||||
|
||||
struct dyncfg_config {
|
||||
void *data;
|
||||
size_t data_size;
|
||||
};
|
||||
|
||||
typedef struct dyncfg_config dyncfg_config_t;
|
||||
|
||||
struct configurable_plugin;
|
||||
struct module;
|
||||
|
||||
enum job_status {
|
||||
JOB_STATUS_UNKNOWN = 0, // State used until plugin reports first status
|
||||
JOB_STATUS_STOPPED,
|
||||
JOB_STATUS_RUNNING,
|
||||
JOB_STATUS_ERROR
|
||||
};
|
||||
|
||||
inline enum job_status str2job_state(const char *state_name) {
|
||||
if (strcmp(state_name, "stopped") == 0)
|
||||
return JOB_STATUS_STOPPED;
|
||||
else if (strcmp(state_name, "running") == 0)
|
||||
return JOB_STATUS_RUNNING;
|
||||
else if (strcmp(state_name, "error") == 0)
|
||||
return JOB_STATUS_ERROR;
|
||||
return JOB_STATUS_UNKNOWN;
|
||||
}
|
||||
|
||||
enum set_config_result {
|
||||
SET_CONFIG_ACCEPTED = 0,
|
||||
SET_CONFIG_REJECTED,
|
||||
SET_CONFIG_DEFFER
|
||||
};
|
||||
|
||||
struct job
|
||||
{
|
||||
char *name;
|
||||
|
||||
//state reported by config
|
||||
enum job_status status; // reported by plugin, enum as this has to be interpreted by UI
|
||||
int state; // code reported by plugin which can mean anything plugin wants
|
||||
char *reason; // reported by plugin, can be NULL (optional)
|
||||
|
||||
usec_t last_state_update;
|
||||
|
||||
struct module *module;
|
||||
};
|
||||
|
||||
struct module
|
||||
{
|
||||
pthread_mutex_t lock;
|
||||
char *name;
|
||||
enum module_type type;
|
||||
|
||||
struct configurable_plugin *plugin;
|
||||
|
||||
// module config
|
||||
enum set_config_result (*set_config_cb)(void *usr_ctx, const char *module_name, dyncfg_config_t *cfg);
|
||||
dyncfg_config_t (*get_config_cb)(void *usr_ctx, const char *name);
|
||||
dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx, const char *name);
|
||||
void *config_cb_usr_ctx;
|
||||
|
||||
DICTIONARY *jobs;
|
||||
|
||||
// jobs config
|
||||
dyncfg_config_t (*get_job_config_cb)(void *usr_ctx, const char *module_name, const char *job_name);
|
||||
dyncfg_config_t (*get_job_config_schema_cb)(void *usr_ctx, const char *module_name);
|
||||
enum set_config_result (*set_job_config_cb)(void *usr_ctx, const char *module_name, const char *job_name, dyncfg_config_t *cfg);
|
||||
enum set_config_result (*delete_job_cb)(void *usr_ctx, const char *module_name, const char *job_name);
|
||||
void *job_config_cb_usr_ctx;
|
||||
};
|
||||
|
||||
struct configurable_plugin {
|
||||
pthread_mutex_t lock;
|
||||
char *name;
|
||||
DICTIONARY *modules;
|
||||
const char *schema;
|
||||
|
||||
dyncfg_config_t (*get_config_cb)(void *usr_ctx);
|
||||
dyncfg_config_t (*get_config_schema_cb)(void *usr_ctx);
|
||||
enum set_config_result (*set_config_cb)(void *usr_ctx, dyncfg_config_t *cfg);
|
||||
void *cb_usr_ctx; // context for all callbacks (split if needed in future)
|
||||
};
|
||||
|
||||
// API to be used by plugins
|
||||
const DICTIONARY_ITEM *register_plugin(struct configurable_plugin *plugin);
|
||||
void unregister_plugin(const DICTIONARY_ITEM *plugin);
|
||||
int register_module(struct configurable_plugin *plugin, struct module *module);
|
||||
|
||||
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);
|
||||
|
||||
// API to be used by the web server(s)
|
||||
json_object *get_list_of_plugins_json();
|
||||
struct configurable_plugin *get_plugin_by_name(const char *name);
|
||||
|
||||
json_object *get_list_of_modules_json(struct configurable_plugin *plugin);
|
||||
struct module *get_module_by_name(struct configurable_plugin *plugin, const char *module_name);
|
||||
|
||||
// helper struct to make interface between internal webserver and h2o same
|
||||
struct uni_http_response {
|
||||
int status;
|
||||
char *content;
|
||||
size_t content_length;
|
||||
HTTP_CONTENT_TYPE content_type;
|
||||
void (*content_free)(void *);
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
// API to be used by main netdata process, initialization and destruction etc.
|
||||
int dyn_conf_init(void);
|
||||
|
||||
void *dyncfg_main(void *in);
|
||||
|
||||
#endif //DYN_CONF_H
|
|
@ -17,15 +17,23 @@
|
|||
#define HTTP_RESP_UNAUTHORIZED 401
|
||||
#define HTTP_RESP_FORBIDDEN 403
|
||||
#define HTTP_RESP_NOT_FOUND 404
|
||||
#define HTTP_RESP_METHOD_NOT_ALLOWED 405
|
||||
#define HTTP_RESP_METHOD_NOT_ALLOWED_STR "Method Not Allowed"
|
||||
#define HTTP_RESP_CONFLICT 409
|
||||
#define HTTP_RESP_PRECOND_FAIL 412
|
||||
#define HTTP_RESP_CONTENT_TOO_LONG 413
|
||||
|
||||
// HTTP_CODES 5XX Server Errors
|
||||
#define HTTP_RESP_INTERNAL_SERVER_ERROR 500
|
||||
#define HTTP_RESP_INTERNAL_SERVER_ERROR_STR "Internal Server Error"
|
||||
#define HTTP_RESP_BACKEND_FETCH_FAILED 503 // 503 is right
|
||||
#define HTTP_RESP_SERVICE_UNAVAILABLE 503 // 503 is right
|
||||
#define HTTP_RESP_GATEWAY_TIMEOUT 504
|
||||
#define HTTP_RESP_BACKEND_RESPONSE_INVALID 591
|
||||
|
||||
#define HTTP_METHOD_GET (1)
|
||||
#define HTTP_METHOD_POST (2)
|
||||
#define HTTP_METHOD_PUT (3)
|
||||
#define HTTP_METHOD_DELETE (4)
|
||||
|
||||
#endif /* NETDATA_HTTP_DEFS_H */
|
||||
|
|
|
@ -1628,7 +1628,7 @@ void recursive_config_double_dir_load(const char *user_path, const char *stock_p
|
|||
|
||||
// Returns the number of bytes read from the file if file_size is not NULL.
|
||||
// The actual buffer has an extra byte set to zero (not included in the count).
|
||||
char *read_by_filename(char *filename, long *file_size)
|
||||
char *read_by_filename(const char *filename, long *file_size)
|
||||
{
|
||||
FILE *f = fopen(filename, "r");
|
||||
if (!f)
|
||||
|
|
|
@ -579,7 +579,7 @@ void recursive_config_double_dir_load(
|
|||
, void *data
|
||||
, size_t depth
|
||||
);
|
||||
char *read_by_filename(char *filename, long *file_size);
|
||||
char *read_by_filename(const char *filename, long *file_size);
|
||||
char *find_and_replace(const char *src, const char *find, const char *replace, const char *where);
|
||||
|
||||
/* fix for alpine linux */
|
||||
|
@ -837,6 +837,7 @@ extern char *netdata_configured_host_prefix;
|
|||
#include "yaml.h"
|
||||
#include "http/http_defs.h"
|
||||
#include "gorilla/gorilla.h"
|
||||
#include "dyn_conf/dyn_conf.h"
|
||||
|
||||
// BEWARE: this exists in alarm-notify.sh
|
||||
#define DEFAULT_CLOUD_BASE_URL "https://app.netdata.cloud"
|
||||
|
|
|
@ -243,7 +243,7 @@ inline bool url_is_request_complete(char *begin, char *end, size_t length, char
|
|||
if(likely(strncmp(begin, "GET ", 4)) == 0) {
|
||||
return strstr(end - 4, "\r\n\r\n");
|
||||
}
|
||||
else if(unlikely(strncmp(begin, "POST ", 5) == 0)) {
|
||||
else if(unlikely(strncmp(begin, "POST ", 5) == 0 || strncmp(begin, "PUT ", 4) == 0)) {
|
||||
char *cl = strstr(begin, "Content-Length: ");
|
||||
if(!cl) return false;
|
||||
cl = &cl[16];
|
||||
|
|
|
@ -40,12 +40,28 @@ int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_pat
|
|||
return HTTP_RESP_BAD_REQUEST;
|
||||
}
|
||||
|
||||
uint32_t hash = simple_hash(url_path_endpoint);
|
||||
char *api_command = strchr(url_path_endpoint, '/');
|
||||
if (likely(api_command == NULL)) // only config command supports subpaths for now
|
||||
api_command = url_path_endpoint;
|
||||
else {
|
||||
size_t api_command_len = api_command - url_path_endpoint;
|
||||
api_command = callocz(1, api_command_len + 1);
|
||||
memcpy(api_command, url_path_endpoint, api_command_len);
|
||||
}
|
||||
|
||||
uint32_t hash = simple_hash(api_command);
|
||||
|
||||
for(int i = 0; api_commands[i].command ; i++) {
|
||||
if(unlikely(hash == api_commands[i].hash && !strcmp(url_path_endpoint, api_commands[i].command))) {
|
||||
if(unlikely(!web_client_check_acl_and_bearer(w, api_commands[i].acl)))
|
||||
return web_client_bearer_required(w);
|
||||
if(unlikely(hash == api_commands[i].hash && !strcmp(api_command, api_commands[i].command))) {
|
||||
if(unlikely(!api_commands[i].allow_subpaths && api_command != url_path_endpoint)) {
|
||||
buffer_flush(w->response.data);
|
||||
buffer_sprintf(w->response.data, "API command '%s' does not support subpaths.", api_command);
|
||||
freez(api_command);
|
||||
return HTTP_RESP_BAD_REQUEST;
|
||||
}
|
||||
|
||||
if(unlikely(api_commands[i].acl != WEB_CLIENT_ACL_NOCHECK) && !(w->acl & api_commands[i].acl))
|
||||
return web_client_permission_denied(w);
|
||||
|
||||
char *query_string = (char *)buffer_tostring(w->url_query_string_decoded);
|
||||
|
||||
|
@ -56,6 +72,9 @@ int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_pat
|
|||
}
|
||||
}
|
||||
|
||||
if (api_command != url_path_endpoint)
|
||||
freez(api_command);
|
||||
|
||||
buffer_flush(w->response.data);
|
||||
buffer_strcat(w->response.data, "Unsupported API command: ");
|
||||
buffer_strcat_htmlescape(w->response.data, url_path_endpoint);
|
||||
|
|
|
@ -20,6 +20,7 @@ struct web_api_command {
|
|||
uint32_t hash;
|
||||
WEB_CLIENT_ACL acl;
|
||||
int (*callback)(RRDHOST *host, struct web_client *w, char *url);
|
||||
unsigned int allow_subpaths;
|
||||
};
|
||||
|
||||
struct web_client;
|
||||
|
|
|
@ -1463,43 +1463,43 @@ int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struc
|
|||
#endif
|
||||
|
||||
static struct web_api_command api_commands_v1[] = {
|
||||
{ "info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_info },
|
||||
{ "data", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_data },
|
||||
{ "chart", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_chart },
|
||||
{ "charts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_charts },
|
||||
{ "context", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_context },
|
||||
{ "contexts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_contexts },
|
||||
{ "info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_info, 0 },
|
||||
{ "data", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_data, 0 },
|
||||
{ "chart", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_chart, 0 },
|
||||
{ "charts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_charts, 0 },
|
||||
{ "context", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_context, 0 },
|
||||
{ "contexts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_contexts, 0 },
|
||||
|
||||
// registry checks the ACL by itself, so we allow everything
|
||||
{ "registry", 0, WEB_CLIENT_ACL_NOCHECK, web_client_api_request_v1_registry },
|
||||
{ "registry", 0, WEB_CLIENT_ACL_NOCHECK, web_client_api_request_v1_registry, 0 },
|
||||
|
||||
// badges can be fetched with both dashboard and badge permissions
|
||||
{ "badge.svg", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC | WEB_CLIENT_ACL_BADGE, web_client_api_request_v1_badge },
|
||||
{ "badge.svg", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC | WEB_CLIENT_ACL_BADGE, web_client_api_request_v1_badge, 0 },
|
||||
|
||||
{ "alarms", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarms },
|
||||
{ "alarms_values", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarms_values },
|
||||
{ "alarm_log", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_log },
|
||||
{ "alarm_variables", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_variables },
|
||||
{ "alarm_count", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_count },
|
||||
{ "allmetrics", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_allmetrics },
|
||||
{ "alarms", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarms, 0 },
|
||||
{ "alarms_values", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarms_values, 0 },
|
||||
{ "alarm_log", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_log, 0 },
|
||||
{ "alarm_variables", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_variables, 0 },
|
||||
{ "alarm_count", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_alarm_count, 0 },
|
||||
{ "allmetrics", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_allmetrics, 0 },
|
||||
|
||||
#if defined(ENABLE_ML)
|
||||
{ "ml_info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_ml_info },
|
||||
{ "ml_info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_ml_info, 0 },
|
||||
// { "ml_models", 0, WEB_CLIENT_ACL_DASHBOARD, web_client_api_request_v1_ml_models },
|
||||
#endif
|
||||
|
||||
{"manage/health", 0, WEB_CLIENT_ACL_MGMT | WEB_CLIENT_ACL_ACLK, web_client_api_request_v1_mgmt_health },
|
||||
{ "aclk", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_aclk_state },
|
||||
{ "metric_correlations", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_metric_correlations },
|
||||
{ "weights", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_weights },
|
||||
{"manage/health", 0, WEB_CLIENT_ACL_MGMT | WEB_CLIENT_ACL_ACLK, web_client_api_request_v1_mgmt_health, 0 },
|
||||
{ "aclk", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_aclk_state, 0 },
|
||||
{ "metric_correlations", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_metric_correlations, 0 },
|
||||
{ "weights", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_weights, 0 },
|
||||
|
||||
{"function", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v1_function },
|
||||
{"functions", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v1_functions },
|
||||
{"function", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v1_function, 0 },
|
||||
{"functions", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v1_functions, 0 },
|
||||
|
||||
{ "dbengine_stats", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_dbengine_stats },
|
||||
{ "dbengine_stats", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v1_dbengine_stats, 0 },
|
||||
|
||||
// terminator
|
||||
{ NULL, 0, WEB_CLIENT_ACL_NONE, NULL },
|
||||
{ NULL, 0, WEB_CLIENT_ACL_NONE, NULL, 0 },
|
||||
};
|
||||
|
||||
inline int web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url_path_endpoint) {
|
||||
|
|
|
@ -644,31 +644,97 @@ static int web_client_api_request_v2_webrtc(RRDHOST *host __maybe_unused, struct
|
|||
return webrtc_new_connection(w->post_payload, w->response.data);
|
||||
}
|
||||
|
||||
#define CONFIG_API_V2_URL "/api/v2/config"
|
||||
static int web_client_api_request_v2_config(RRDHOST *host __maybe_unused, struct web_client *w, char *query) {
|
||||
|
||||
char *url = strdupz(buffer_tostring(w->url_as_received));
|
||||
char *url_full = url;
|
||||
|
||||
if (strncmp(url, CONFIG_API_V2_URL, strlen(CONFIG_API_V2_URL)) != 0) {
|
||||
buffer_sprintf(w->response.data, "Invalid URL");
|
||||
return HTTP_RESP_BAD_REQUEST;
|
||||
}
|
||||
url += strlen(CONFIG_API_V2_URL);
|
||||
|
||||
char *save_ptr = NULL;
|
||||
char *plugin = strtok_r(url, "/", &save_ptr);
|
||||
char *module = strtok_r(NULL, "/", &save_ptr);
|
||||
char *job_id = strtok_r(NULL, "/", &save_ptr);
|
||||
char *extra = strtok_r(NULL, "/", &save_ptr);
|
||||
|
||||
buffer_flush(w->response.data);
|
||||
if (extra != NULL) {
|
||||
buffer_sprintf(w->response.data, "Invalid URL");
|
||||
freez(url_full);
|
||||
return HTTP_RESP_BAD_REQUEST;
|
||||
}
|
||||
|
||||
int http_method;
|
||||
switch (w->mode)
|
||||
{
|
||||
case WEB_CLIENT_MODE_GET:
|
||||
http_method = HTTP_METHOD_GET;
|
||||
break;
|
||||
case WEB_CLIENT_MODE_POST:
|
||||
http_method = HTTP_METHOD_POST;
|
||||
break;
|
||||
case WEB_CLIENT_MODE_PUT:
|
||||
http_method = HTTP_METHOD_PUT;
|
||||
break;
|
||||
case WEB_CLIENT_MODE_DELETE:
|
||||
http_method = HTTP_METHOD_DELETE;
|
||||
break;
|
||||
default:
|
||||
buffer_sprintf(w->response.data, "Invalid HTTP method");
|
||||
freez(url_full);
|
||||
return HTTP_RESP_BAD_REQUEST;
|
||||
}
|
||||
|
||||
struct uni_http_response resp = dyn_conf_process_http_request(http_method, plugin, module, job_id, w->post_payload, w->post_payload_size);
|
||||
if (resp.content[resp.content_length - 1] != '\0') {
|
||||
char *con = mallocz(resp.content_length + 1);
|
||||
memcpy(con, resp.content, resp.content_length);
|
||||
con[resp.content_length] = '\0';
|
||||
if (resp.content_free)
|
||||
resp.content_free(resp.content);
|
||||
resp.content = con;
|
||||
resp.content_free = freez;
|
||||
}
|
||||
buffer_strcat(w->response.data, resp.content);
|
||||
if (resp.content_free)
|
||||
resp.content_free(resp.content);
|
||||
w->response.data->content_type = resp.content_type;
|
||||
freez(url_full);
|
||||
return resp.status;
|
||||
}
|
||||
|
||||
static struct web_api_command api_commands_v2[] = {
|
||||
{"info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_info},
|
||||
{"info", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_info, 0},
|
||||
|
||||
{"data", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_data},
|
||||
{"weights", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_weights},
|
||||
{"data", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_data, 0},
|
||||
{"weights", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_weights, 0},
|
||||
|
||||
{"contexts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_contexts},
|
||||
{"nodes", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_nodes},
|
||||
{"node_instances", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_node_instances},
|
||||
{"versions", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_versions},
|
||||
{"functions", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v2_functions},
|
||||
{"q", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_q},
|
||||
{"alerts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alerts},
|
||||
{"contexts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_contexts, 0},
|
||||
{"nodes", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_nodes, 0},
|
||||
{"node_instances", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_node_instances, 0},
|
||||
{"versions", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_versions, 0},
|
||||
{"functions", 0, WEB_CLIENT_ACL_ACLK_WEBRTC_DASHBOARD_WITH_BEARER | ACL_DEV_OPEN_ACCESS, web_client_api_request_v2_functions, 0},
|
||||
{"q", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_q, 0},
|
||||
{"alerts", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alerts, 0},
|
||||
|
||||
{"alert_transitions", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alert_transitions},
|
||||
{"alert_config", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alert_config},
|
||||
{"alert_transitions", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alert_transitions, 0},
|
||||
{"alert_config", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_alert_config, 0},
|
||||
|
||||
{"claim", 0, WEB_CLIENT_ACL_NOCHECK, web_client_api_request_v2_claim},
|
||||
{"claim", 0, WEB_CLIENT_ACL_NOCHECK, web_client_api_request_v2_claim, 0},
|
||||
|
||||
{"rtc_offer", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, web_client_api_request_v2_webrtc},
|
||||
{"bearer_protection", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_protection},
|
||||
{"bearer_get_token", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_token},
|
||||
{"rtc_offer", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, web_client_api_request_v2_webrtc, 0},
|
||||
{"bearer_protection", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_protection, 0},
|
||||
{"bearer_get_token", 0, WEB_CLIENT_ACL_ACLK | ACL_DEV_OPEN_ACCESS, api_v2_bearer_token, 0},
|
||||
|
||||
{"config", 0, WEB_CLIENT_ACL_DASHBOARD_ACLK_WEBRTC, web_client_api_request_v2_config, 1},
|
||||
|
||||
// terminator
|
||||
{NULL, 0, WEB_CLIENT_ACL_NONE, NULL},
|
||||
{NULL, 0, WEB_CLIENT_ACL_NONE, NULL, 0},
|
||||
};
|
||||
|
||||
inline int web_client_api_request_v2(RRDHOST *host, struct web_client *w, char *url_path_endpoint) {
|
||||
|
|
|
@ -204,7 +204,9 @@ void web_client_request_done(struct web_client *w) {
|
|||
break;
|
||||
|
||||
case WEB_CLIENT_MODE_POST:
|
||||
case WEB_CLIENT_MODE_PUT:
|
||||
case WEB_CLIENT_MODE_GET:
|
||||
case WEB_CLIENT_MODE_DELETE:
|
||||
mode = "DATA";
|
||||
break;
|
||||
|
||||
|
@ -1075,6 +1077,14 @@ static inline char *web_client_valid_method(struct web_client *w, char *s) {
|
|||
s = &s[5];
|
||||
w->mode = WEB_CLIENT_MODE_POST;
|
||||
}
|
||||
else if(!strncmp(s, "PUT ", 4)) {
|
||||
s = &s[4];
|
||||
w->mode = WEB_CLIENT_MODE_PUT;
|
||||
}
|
||||
else if(!strncmp(s, "DELETE ", 7)) {
|
||||
s = &s[7];
|
||||
w->mode = WEB_CLIENT_MODE_DELETE;
|
||||
}
|
||||
else if(!strncmp(s, "STREAM ", 7)) {
|
||||
s = &s[7];
|
||||
|
||||
|
@ -1747,6 +1757,8 @@ void web_client_process_request(struct web_client *w) {
|
|||
case WEB_CLIENT_MODE_FILECOPY:
|
||||
case WEB_CLIENT_MODE_POST:
|
||||
case WEB_CLIENT_MODE_GET:
|
||||
case WEB_CLIENT_MODE_PUT:
|
||||
case WEB_CLIENT_MODE_DELETE:
|
||||
if(unlikely(
|
||||
!web_client_can_access_dashboard(w) &&
|
||||
!web_client_can_access_registry(w) &&
|
||||
|
@ -1879,6 +1891,8 @@ void web_client_process_request(struct web_client *w) {
|
|||
|
||||
case WEB_CLIENT_MODE_POST:
|
||||
case WEB_CLIENT_MODE_GET:
|
||||
case WEB_CLIENT_MODE_PUT:
|
||||
case WEB_CLIENT_MODE_DELETE:
|
||||
netdata_log_debug(D_WEB_CLIENT, "%llu: Done preparing the response. Sending data (%zu bytes) to client.", w->id, w->response.data->len);
|
||||
break;
|
||||
|
||||
|
@ -2042,7 +2056,7 @@ ssize_t web_client_send_deflate(struct web_client *w)
|
|||
|
||||
// ask for FINISH if we have all the input
|
||||
int flush = Z_SYNC_FLUSH;
|
||||
if((w->mode == WEB_CLIENT_MODE_GET || w->mode == WEB_CLIENT_MODE_POST)
|
||||
if((w->mode == WEB_CLIENT_MODE_GET || w->mode == WEB_CLIENT_MODE_POST || w->mode == WEB_CLIENT_MODE_PUT || w->mode == WEB_CLIENT_MODE_DELETE)
|
||||
|| (w->mode == WEB_CLIENT_MODE_FILECOPY && !web_client_has_wait_receive(w) && w->response.data->len == w->response.rlen)) {
|
||||
flush = Z_FINISH;
|
||||
netdata_log_debug(D_DEFLATE, "%llu: Requesting Z_FINISH, if possible.", w->id);
|
||||
|
|
|
@ -18,6 +18,8 @@ typedef enum web_client_mode {
|
|||
WEB_CLIENT_MODE_FILECOPY = 2,
|
||||
WEB_CLIENT_MODE_OPTIONS = 3,
|
||||
WEB_CLIENT_MODE_STREAM = 4,
|
||||
WEB_CLIENT_MODE_PUT = 5,
|
||||
WEB_CLIENT_MODE_DELETE = 6,
|
||||
} WEB_CLIENT_MODE;
|
||||
|
||||
typedef enum {
|
||||
|
|
Loading…
Add table
Reference in a new issue