diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-11-09 08:36:11 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-11-09 08:37:11 +0000 |
commit | 910c794ec6d0a364b4aabccf22b715cb45780e83 (patch) | |
tree | 561a9ef6b6a4668102674e1a52b3e7563c57ac61 /src/web/api | |
parent | Releasing debian version 1.47.5-1. (diff) | |
download | netdata-debian.tar.xz netdata-debian.zip |
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/web/api')
100 files changed, 5083 insertions, 3191 deletions
diff --git a/src/web/api/badges/web_buffer_svg.h b/src/web/api/badges/web_buffer_svg.h deleted file mode 100644 index 71857811..00000000 --- a/src/web/api/badges/web_buffer_svg.h +++ /dev/null @@ -1,18 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef NETDATA_WEB_BUFFER_SVG_H -#define NETDATA_WEB_BUFFER_SVG_H 1 - -#include "libnetdata/libnetdata.h" -#include "web/server/web_client.h" - -void buffer_svg(BUFFER *wb, const char *label, - NETDATA_DOUBLE value, const char *units, const char *label_color, const char *value_color, int precision, int scale, uint32_t options, int fixed_width_lbl, int fixed_width_val, const char* text_color_lbl, const char* text_color_val); -char *format_value_and_unit(char *value_string, size_t value_string_len, - NETDATA_DOUBLE value, const char *units, int precision); - -int web_client_api_request_v1_badge(struct rrdhost *host, struct web_client *w, char *url); - -#include "web/api/web_api_v1.h" - -#endif /* NETDATA_WEB_BUFFER_SVG_H */ diff --git a/src/web/api/exporters/README.md b/src/web/api/exporters/README.md index 20693796..47b44348 100644 --- a/src/web/api/exporters/README.md +++ b/src/web/api/exporters/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Exporters" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/README.md -sidebar_label: "Exporters" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api" ---> - # Exporters TBD diff --git a/src/web/api/exporters/allmetrics.c b/src/web/api/exporters/allmetrics.c deleted file mode 100644 index 55179c0a..00000000 --- a/src/web/api/exporters/allmetrics.c +++ /dev/null @@ -1,132 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#include "allmetrics.h" - -struct prometheus_output_options { - char *name; - PROMETHEUS_OUTPUT_OPTIONS flag; -} prometheus_output_flags_root[] = { - { "names", PROMETHEUS_OUTPUT_NAMES }, - { "timestamps", PROMETHEUS_OUTPUT_TIMESTAMPS }, - { "variables", PROMETHEUS_OUTPUT_VARIABLES }, - { "oldunits", PROMETHEUS_OUTPUT_OLDUNITS }, - { "hideunits", PROMETHEUS_OUTPUT_HIDEUNITS }, - // terminator - { NULL, PROMETHEUS_OUTPUT_NONE }, -}; - -inline int web_client_api_request_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url) { - int format = ALLMETRICS_SHELL; - const char *filter = NULL; - const char *prometheus_server = w->client_ip; - - uint32_t prometheus_exporting_options; - if (prometheus_exporter_instance) - prometheus_exporting_options = prometheus_exporter_instance->config.options; - else - prometheus_exporting_options = global_exporting_options; - - PROMETHEUS_OUTPUT_OPTIONS prometheus_output_options = - PROMETHEUS_OUTPUT_TIMESTAMPS | - ((prometheus_exporting_options & EXPORTING_OPTION_SEND_NAMES) ? PROMETHEUS_OUTPUT_NAMES : 0); - - const char *prometheus_prefix; - if (prometheus_exporter_instance) - prometheus_prefix = prometheus_exporter_instance->config.prefix; - else - prometheus_prefix = global_exporting_prefix; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - if(!strcmp(name, "format")) { - if(!strcmp(value, ALLMETRICS_FORMAT_SHELL)) - format = ALLMETRICS_SHELL; - else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS)) - format = ALLMETRICS_PROMETHEUS; - else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS)) - format = ALLMETRICS_PROMETHEUS_ALL_HOSTS; - else if(!strcmp(value, ALLMETRICS_FORMAT_JSON)) - format = ALLMETRICS_JSON; - else - format = 0; - } - else if(!strcmp(name, "filter")) { - filter = value; - } - else if(!strcmp(name, "server")) { - prometheus_server = value; - } - else if(!strcmp(name, "prefix")) { - prometheus_prefix = value; - } - else if(!strcmp(name, "data") || !strcmp(name, "source") || !strcmp(name, "data source") || !strcmp(name, "data-source") || !strcmp(name, "data_source") || !strcmp(name, "datasource")) { - prometheus_exporting_options = exporting_parse_data_source(value, prometheus_exporting_options); - } - else { - int i; - for(i = 0; prometheus_output_flags_root[i].name ; i++) { - if(!strcmp(name, prometheus_output_flags_root[i].name)) { - if(!strcmp(value, "yes") || !strcmp(value, "1") || !strcmp(value, "true")) - prometheus_output_options |= prometheus_output_flags_root[i].flag; - else { - prometheus_output_options &= ~prometheus_output_flags_root[i].flag; - } - - break; - } - } - } - } - - buffer_flush(w->response.data); - buffer_no_cacheable(w->response.data); - - switch(format) { - case ALLMETRICS_JSON: - w->response.data->content_type = CT_APPLICATION_JSON; - rrd_stats_api_v1_charts_allmetrics_json(host, filter, w->response.data); - return HTTP_RESP_OK; - - case ALLMETRICS_SHELL: - w->response.data->content_type = CT_TEXT_PLAIN; - rrd_stats_api_v1_charts_allmetrics_shell(host, filter, w->response.data); - return HTTP_RESP_OK; - - case ALLMETRICS_PROMETHEUS: - w->response.data->content_type = CT_PROMETHEUS; - rrd_stats_api_v1_charts_allmetrics_prometheus_single_host( - host - , filter - , w->response.data - , prometheus_server - , prometheus_prefix - , prometheus_exporting_options - , prometheus_output_options - ); - return HTTP_RESP_OK; - - case ALLMETRICS_PROMETHEUS_ALL_HOSTS: - w->response.data->content_type = CT_PROMETHEUS; - rrd_stats_api_v1_charts_allmetrics_prometheus_all_hosts( - host - , filter - , w->response.data - , prometheus_server - , prometheus_prefix - , prometheus_exporting_options - , prometheus_output_options - ); - return HTTP_RESP_OK; - - default: - w->response.data->content_type = CT_TEXT_PLAIN; - buffer_strcat(w->response.data, "Which format? '" ALLMETRICS_FORMAT_SHELL "', '" ALLMETRICS_FORMAT_PROMETHEUS "', '" ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "' and '" ALLMETRICS_FORMAT_JSON "' are currently supported."); - return HTTP_RESP_BAD_REQUEST; - } -} diff --git a/src/web/api/exporters/allmetrics.h b/src/web/api/exporters/allmetrics.h deleted file mode 100644 index 3afc42e2..00000000 --- a/src/web/api/exporters/allmetrics.h +++ /dev/null @@ -1,12 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef NETDATA_API_ALLMETRICS_H -#define NETDATA_API_ALLMETRICS_H - -#include "web/api/formatters/rrd2json.h" -#include "shell/allmetrics_shell.h" -#include "web/server/web_client.h" - -int web_client_api_request_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url); - -#endif //NETDATA_API_ALLMETRICS_H diff --git a/src/web/api/exporters/prometheus/README.md b/src/web/api/exporters/prometheus/README.md index 6c6bad3a..f4b1602c 100644 --- a/src/web/api/exporters/prometheus/README.md +++ b/src/web/api/exporters/prometheus/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Prometheus exporter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/prometheus/README.md -sidebar_label: "Prometheus exporter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Exporters" ---> - # Prometheus exporter Read the Prometheus exporter documentation: [Using Netdata with Prometheus](/src/exporting/prometheus/README.md). diff --git a/src/web/api/exporters/shell/README.md b/src/web/api/exporters/shell/README.md index 86b774f1..14faf1fb 100644 --- a/src/web/api/exporters/shell/README.md +++ b/src/web/api/exporters/shell/README.md @@ -1,25 +1,16 @@ -<!-- -title: "Shell exporter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/exporters/shell/README.md -sidebar_label: "Shell exporter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Exporters" ---> - # Shell exporter Shell scripts can now query Netdata: ```sh -eval "$(curl -s 'http://localhost:19999/api/v1/allmetrics')" +eval "$(curl -s 'http://localhost:19999/api/v3/allmetrics')" ``` after this command, all the Netdata metrics are exposed to shell. Check: ```sh # source the metrics -eval "$(curl -s 'http://localhost:19999/api/v1/allmetrics')" +eval "$(curl -s 'http://localhost:19999/api/v3/allmetrics')" # let's see if there are variables exposed by Netdata for system.cpu set | grep "^NETDATA_SYSTEM_CPU" @@ -50,7 +41,7 @@ echo ${NETDATA_ALARM_SYSTEM_SWAP_USED_SWAP_STATUS} CLEAR # is it fast? -time curl -s 'http://localhost:19999/api/v1/allmetrics' >/dev/null +time curl -s 'http://localhost:19999/api/v3/allmetrics' >/dev/null real 0m0,070s user 0m0,000s diff --git a/src/web/api/exporters/shell/allmetrics_shell.h b/src/web/api/exporters/shell/allmetrics_shell.h deleted file mode 100644 index d6598e08..00000000 --- a/src/web/api/exporters/shell/allmetrics_shell.h +++ /dev/null @@ -1,21 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef NETDATA_API_ALLMETRICS_SHELL_H -#define NETDATA_API_ALLMETRICS_SHELL_H - -#include "../allmetrics.h" - -#define ALLMETRICS_FORMAT_SHELL "shell" -#define ALLMETRICS_FORMAT_PROMETHEUS "prometheus" -#define ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "prometheus_all_hosts" -#define ALLMETRICS_FORMAT_JSON "json" - -#define ALLMETRICS_SHELL 1 -#define ALLMETRICS_PROMETHEUS 2 -#define ALLMETRICS_JSON 3 -#define ALLMETRICS_PROMETHEUS_ALL_HOSTS 4 - -void rrd_stats_api_v1_charts_allmetrics_json(RRDHOST *host, const char *filter_string, BUFFER *wb); -void rrd_stats_api_v1_charts_allmetrics_shell(RRDHOST *host, const char *filter_string, BUFFER *wb); - -#endif //NETDATA_API_ALLMETRICS_SHELL_H diff --git a/src/web/api/formatters/README.md b/src/web/api/formatters/README.md index 6347f5fb..27d80d40 100644 --- a/src/web/api/formatters/README.md +++ b/src/web/api/formatters/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Query formatting" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/README.md -sidebar_label: "Query formatting" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Formatters" ---> - # Query formatting API data queries need to be formatted before returned to the caller. diff --git a/src/web/api/formatters/charts2json.c b/src/web/api/formatters/charts2json.c index 0b45d77c..9407f224 100644 --- a/src/web/api/formatters/charts2json.c +++ b/src/web/api/formatters/charts2json.c @@ -37,7 +37,7 @@ const char* get_release_channel() { } void charts2json(RRDHOST *host, BUFFER *wb) { - static char *custom_dashboard_info_js_filename = NULL; + static const char *custom_dashboard_info_js_filename = NULL; size_t c = 0, dimensions = 0, memory = 0, alarms = 0; RRDSET *st; diff --git a/src/web/api/formatters/csv/README.md b/src/web/api/formatters/csv/README.md index e60aab57..435d2380 100644 --- a/src/web/api/formatters/csv/README.md +++ b/src/web/api/formatters/csv/README.md @@ -1,12 +1,3 @@ -<!-- -title: "CSV formatter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/csv/README.md -sidebar_label: "CSV formatter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Formatters" ---> - # CSV formatter The CSV formatter presents [results of database queries](/src/web/api/queries/README.md) in the following formats: diff --git a/src/web/api/formatters/json/README.md b/src/web/api/formatters/json/README.md index 4137b037..b1d02555 100644 --- a/src/web/api/formatters/json/README.md +++ b/src/web/api/formatters/json/README.md @@ -1,12 +1,3 @@ -<!-- -title: "JSON formatter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/json/README.md -sidebar_label: "JSON formatter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Formatters" ---> - # JSON formatter The CSV formatter presents [results of database queries](/src/web/api/queries/README.md) in the following formats: diff --git a/src/web/api/formatters/rrd2json.c b/src/web/api/formatters/rrd2json.c index 81c9ad5c..a8027548 100644 --- a/src/web/api/formatters/rrd2json.c +++ b/src/web/api/formatters/rrd2json.c @@ -10,46 +10,6 @@ void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb) buffer_json_finalize(wb); } -const char *rrdr_format_to_string(DATASOURCE_FORMAT format) { - switch(format) { - case DATASOURCE_JSON: - return DATASOURCE_FORMAT_JSON; - - case DATASOURCE_JSON2: - return DATASOURCE_FORMAT_JSON2; - - case DATASOURCE_DATATABLE_JSON: - return DATASOURCE_FORMAT_DATATABLE_JSON; - - case DATASOURCE_DATATABLE_JSONP: - return DATASOURCE_FORMAT_DATATABLE_JSONP; - - case DATASOURCE_JSONP: - return DATASOURCE_FORMAT_JSONP; - - case DATASOURCE_SSV: - return DATASOURCE_FORMAT_SSV; - - case DATASOURCE_CSV: - return DATASOURCE_FORMAT_CSV; - - case DATASOURCE_TSV: - return DATASOURCE_FORMAT_TSV; - - case DATASOURCE_HTML: - return DATASOURCE_FORMAT_HTML; - - case DATASOURCE_JS_ARRAY: - return DATASOURCE_FORMAT_JS_ARRAY; - - case DATASOURCE_SSV_COMMA: - return DATASOURCE_FORMAT_SSV_COMMA; - - default: - return "unknown"; - } -} - int rrdset2value_api_v1( RRDSET *st , BUFFER *wb diff --git a/src/web/api/formatters/rrd2json.h b/src/web/api/formatters/rrd2json.h index f0c0c39b..cf3492ff 100644 --- a/src/web/api/formatters/rrd2json.h +++ b/src/web/api/formatters/rrd2json.h @@ -3,26 +3,8 @@ #ifndef NETDATA_RRD2JSON_H #define NETDATA_RRD2JSON_H 1 -// type of JSON generations -typedef enum { - DATASOURCE_JSON = 0, - DATASOURCE_DATATABLE_JSON = 1, - DATASOURCE_DATATABLE_JSONP = 2, - DATASOURCE_SSV = 3, - DATASOURCE_CSV = 4, - DATASOURCE_JSONP = 5, - DATASOURCE_TSV = 6, - DATASOURCE_HTML = 7, - DATASOURCE_JS_ARRAY = 8, - DATASOURCE_SSV_COMMA = 9, - DATASOURCE_CSV_JSON_ARRAY = 10, - DATASOURCE_CSV_MARKDOWN = 11, - DATASOURCE_JSON2 = 12, -} DATASOURCE_FORMAT; +#include "web/api/web_api.h" -#include "web/api/web_api_v1.h" - -#include "web/api/exporters/allmetrics.h" #include "web/api/queries/rrdr.h" #include "web/api/formatters/csv/csv.h" @@ -38,22 +20,7 @@ typedef enum { #define HOSTNAME_MAX 1024 -#define DATASOURCE_FORMAT_JSON "json" -#define DATASOURCE_FORMAT_JSON2 "json2" -#define DATASOURCE_FORMAT_DATATABLE_JSON "datatable" -#define DATASOURCE_FORMAT_DATATABLE_JSONP "datasource" -#define DATASOURCE_FORMAT_JSONP "jsonp" -#define DATASOURCE_FORMAT_SSV "ssv" -#define DATASOURCE_FORMAT_CSV "csv" -#define DATASOURCE_FORMAT_TSV "tsv" -#define DATASOURCE_FORMAT_HTML "html" -#define DATASOURCE_FORMAT_JS_ARRAY "array" -#define DATASOURCE_FORMAT_SSV_COMMA "ssvcomma" -#define DATASOURCE_FORMAT_CSV_JSON_ARRAY "csvjsonarray" -#define DATASOURCE_FORMAT_CSV_MARKDOWN "markdown" - void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb); -const char *rrdr_format_to_string(DATASOURCE_FORMAT format); int data_query_execute(ONEWAYALLOC *owa, BUFFER *wb, struct query_target *qt, time_t *latest_timestamp); diff --git a/src/web/api/formatters/ssv/README.md b/src/web/api/formatters/ssv/README.md index b3249401..4d07fe5b 100644 --- a/src/web/api/formatters/ssv/README.md +++ b/src/web/api/formatters/ssv/README.md @@ -1,12 +1,3 @@ -<!-- -title: "SSV formatter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/ssv/README.md -sidebar_label: "SSV formatter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Formatters" ---> - # SSV formatter The SSV formatter sums all dimensions in [results of database queries](/src/web/api/queries/README.md) diff --git a/src/web/api/formatters/value/README.md b/src/web/api/formatters/value/README.md index 8a2df23c..1f0af813 100644 --- a/src/web/api/formatters/value/README.md +++ b/src/web/api/formatters/value/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Value formatter" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/formatters/value/README.md -sidebar_label: "Value formatter" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Formatters" ---> - # Value formatter The Value formatter presents [results of database queries](/src/web/api/queries/README.md) as a single value. diff --git a/src/web/api/functions/function-bearer_get_token.c b/src/web/api/functions/function-bearer_get_token.c new file mode 100644 index 00000000..8f14e68a --- /dev/null +++ b/src/web/api/functions/function-bearer_get_token.c @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "function-bearer_get_token.h" +#include "../v2/api_v2_calls.h" + +struct bearer_token_request { + nd_uuid_t claim_id; + nd_uuid_t machine_guid; + nd_uuid_t node_id; + HTTP_USER_ROLE user_role; + HTTP_ACCESS access; + nd_uuid_t cloud_account_id; + STRING *client_name; +}; + +static bool bearer_parse_json_payload(json_object *jobj, const char *path, void *data, BUFFER *error) { + struct bearer_token_request *rq = data; + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, path, "claim_id", rq->claim_id, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, path, "machine_guid", rq->machine_guid, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, path, "node_id", rq->node_id, error, true); + JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, path, "user_role", http_user_role2id, rq->user_role, error, true); + JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, path, "access", http_access2id_one, rq->access, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, path, "cloud_account_id", rq->cloud_account_id, error, true); + JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, path, "client_name", rq->client_name, error, true); + return true; +} + +int function_bearer_get_token(BUFFER *wb, const char *function __maybe_unused, BUFFER *payload, const char *source) { + if(!request_source_is_cloud(source)) + return rrd_call_function_error( + wb, "Bearer tokens can only be provided via NC.", HTTP_RESP_BAD_REQUEST); + + int code; + struct bearer_token_request rq = { 0 }; + CLEAN_JSON_OBJECT *jobj = json_parse_function_payload_or_error(wb, payload, &code, bearer_parse_json_payload, &rq); + if(!jobj || code != HTTP_RESP_OK) { + string_freez(rq.client_name); + return code; + } + + char claim_id[UUID_STR_LEN]; + uuid_unparse_lower(rq.claim_id, claim_id); + + char machine_guid[UUID_STR_LEN]; + uuid_unparse_lower(rq.machine_guid, machine_guid); + + char node_id[UUID_STR_LEN]; + uuid_unparse_lower(rq.node_id, node_id); + + int rc = bearer_get_token_json_response(wb, localhost, claim_id, machine_guid, node_id, + rq.user_role, rq.access, rq.cloud_account_id, + string2str(rq.client_name)); + + string_freez(rq.client_name); + return rc; +} + +int call_function_bearer_get_token(RRDHOST *host, struct web_client *w, const char *claim_id, const char *machine_guid, const char *node_id) { + CLEAN_BUFFER *payload = buffer_create(0, NULL); + buffer_json_initialize(payload, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + buffer_json_member_add_string(payload, "claim_id", claim_id); + buffer_json_member_add_string(payload, "machine_guid", machine_guid); + buffer_json_member_add_string(payload, "node_id", node_id); + buffer_json_member_add_string(payload, "user_role", http_id2user_role(w->user_role)); + http_access2buffer_json_array(payload, "access", w->access); + buffer_json_member_add_uuid(payload, "cloud_account_id", w->auth.cloud_account_id); + buffer_json_member_add_string(payload, "client_name", w->auth.client_name); + buffer_json_finalize(payload); + + CLEAN_BUFFER *source = buffer_create(0, NULL); + web_client_api_request_vX_source_to_buffer(w, source); + + char transaction_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->transaction, transaction_str); + return rrd_function_run(host, w->response.data, 10, + w->access, RRDFUNCTIONS_BEARER_GET_TOKEN, true, + transaction_str, NULL, NULL, + NULL, NULL, + NULL, NULL, + payload, buffer_tostring(source), true); +} diff --git a/src/web/api/functions/function-bearer_get_token.h b/src/web/api/functions/function-bearer_get_token.h new file mode 100644 index 00000000..03481ebb --- /dev/null +++ b/src/web/api/functions/function-bearer_get_token.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_FUNCTION_BEARER_GET_TOKEN_H +#define NETDATA_FUNCTION_BEARER_GET_TOKEN_H + +#include "daemon/common.h" + +int function_bearer_get_token(BUFFER *wb, const char *function, BUFFER *payload, const char *source); +int call_function_bearer_get_token(RRDHOST *host, struct web_client *w, const char *claim_id, const char *machine_guid, const char *node_id); + +#define RRDFUNCTIONS_BEARER_GET_TOKEN "bearer_get_token" +#define RRDFUNCTIONS_BEARER_GET_TOKEN_HELP "Get a bearer token for authenticated direct access to the agent" + +#endif //NETDATA_FUNCTION_BEARER_GET_TOKEN_H diff --git a/src/web/api/functions/function-progress.c b/src/web/api/functions/function-progress.c new file mode 100644 index 00000000..052a9020 --- /dev/null +++ b/src/web/api/functions/function-progress.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "function-progress.h" + +int function_progress(BUFFER *wb, const char *function __maybe_unused, BUFFER *payload __maybe_unused, const char *source __maybe_unused) { + return progress_function_result(wb, rrdhost_hostname(localhost)); +} + diff --git a/src/web/api/functions/function-progress.h b/src/web/api/functions/function-progress.h new file mode 100644 index 00000000..7d2d10b9 --- /dev/null +++ b/src/web/api/functions/function-progress.h @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_FUNCTION_PROGRESS_H +#define NETDATA_FUNCTION_PROGRESS_H + +#include "daemon/common.h" + +int function_progress(BUFFER *wb, const char *function, BUFFER *payload, const char *source); + +#endif //NETDATA_FUNCTION_PROGRESS_H diff --git a/src/web/api/functions/function-streaming.c b/src/web/api/functions/function-streaming.c new file mode 100644 index 00000000..11e97044 --- /dev/null +++ b/src/web/api/functions/function-streaming.c @@ -0,0 +1,627 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "function-streaming.h" + +int function_streaming(BUFFER *wb, const char *function __maybe_unused, BUFFER *payload __maybe_unused, const char *source __maybe_unused) { + + time_t now = now_realtime_sec(); + + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + + buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(localhost)); + buffer_json_member_add_uint64(wb, "status", HTTP_RESP_OK); + buffer_json_member_add_string(wb, "type", "table"); + buffer_json_member_add_time_t(wb, "update_every", 1); + buffer_json_member_add_boolean(wb, "has_history", false); + buffer_json_member_add_string(wb, "help", RRDFUNCTIONS_STREAMING_HELP); + buffer_json_member_add_array(wb, "data"); + + size_t max_sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_MAX] = { 0 }; + size_t max_db_metrics = 0, max_db_instances = 0, max_db_contexts = 0; + size_t max_collection_replication_instances = 0, max_streaming_replication_instances = 0; + size_t max_ml_anomalous = 0, max_ml_normal = 0, max_ml_trained = 0, max_ml_pending = 0, max_ml_silenced = 0; + { + RRDHOST *host; + dfe_start_read(rrdhost_root_index, host) { + RRDHOST_STATUS s; + rrdhost_status(host, now, &s); + buffer_json_add_array_item_array(wb); + + if(s.db.metrics > max_db_metrics) + max_db_metrics = s.db.metrics; + + if(s.db.instances > max_db_instances) + max_db_instances = s.db.instances; + + if(s.db.contexts > max_db_contexts) + max_db_contexts = s.db.contexts; + + if(s.ingest.replication.instances > max_collection_replication_instances) + max_collection_replication_instances = s.ingest.replication.instances; + + if(s.stream.replication.instances > max_streaming_replication_instances) + max_streaming_replication_instances = s.stream.replication.instances; + + for(int i = 0; i < STREAM_TRAFFIC_TYPE_MAX ;i++) { + if (s.stream.sent_bytes_on_this_connection_per_type[i] > + max_sent_bytes_on_this_connection_per_type[i]) + max_sent_bytes_on_this_connection_per_type[i] = + s.stream.sent_bytes_on_this_connection_per_type[i]; + } + + // retention + buffer_json_add_array_item_string(wb, rrdhost_hostname(s.host)); // Node + buffer_json_add_array_item_uint64(wb, s.db.first_time_s * MSEC_PER_SEC); // dbFrom + buffer_json_add_array_item_uint64(wb, s.db.last_time_s * MSEC_PER_SEC); // dbTo + + if(s.db.first_time_s && s.db.last_time_s && s.db.last_time_s > s.db.first_time_s) + buffer_json_add_array_item_uint64(wb, s.db.last_time_s - s.db.first_time_s); // dbDuration + else + buffer_json_add_array_item_string(wb, NULL); // dbDuration + + buffer_json_add_array_item_uint64(wb, s.db.metrics); // dbMetrics + buffer_json_add_array_item_uint64(wb, s.db.instances); // dbInstances + buffer_json_add_array_item_uint64(wb, s.db.contexts); // dbContexts + + // statuses + buffer_json_add_array_item_string(wb, rrdhost_ingest_status_to_string(s.ingest.status)); // InStatus + buffer_json_add_array_item_string(wb, rrdhost_streaming_status_to_string(s.stream.status)); // OutStatus + buffer_json_add_array_item_string(wb, rrdhost_ml_status_to_string(s.ml.status)); // MLStatus + + // collection + if(s.ingest.since) { + buffer_json_add_array_item_uint64(wb, s.ingest.since * MSEC_PER_SEC); // InSince + buffer_json_add_array_item_time_t(wb, s.now - s.ingest.since); // InAge + } + else { + buffer_json_add_array_item_string(wb, NULL); // InSince + buffer_json_add_array_item_string(wb, NULL); // InAge + } + buffer_json_add_array_item_string(wb, stream_handshake_error_to_string(s.ingest.reason)); // InReason + buffer_json_add_array_item_uint64(wb, s.ingest.hops); // InHops + buffer_json_add_array_item_double(wb, s.ingest.replication.completion); // InReplCompletion + buffer_json_add_array_item_uint64(wb, s.ingest.replication.instances); // InReplInstances + buffer_json_add_array_item_string(wb, s.ingest.peers.local.ip); // InLocalIP + buffer_json_add_array_item_uint64(wb, s.ingest.peers.local.port); // InLocalPort + buffer_json_add_array_item_string(wb, s.ingest.peers.peer.ip); // InRemoteIP + buffer_json_add_array_item_uint64(wb, s.ingest.peers.peer.port); // InRemotePort + buffer_json_add_array_item_string(wb, s.ingest.ssl ? "SSL" : "PLAIN"); // InSSL + stream_capabilities_to_json_array(wb, s.ingest.capabilities, NULL); // InCapabilities + + // streaming + if(s.stream.since) { + buffer_json_add_array_item_uint64(wb, s.stream.since * MSEC_PER_SEC); // OutSince + buffer_json_add_array_item_time_t(wb, s.now - s.stream.since); // OutAge + } + else { + buffer_json_add_array_item_string(wb, NULL); // OutSince + buffer_json_add_array_item_string(wb, NULL); // OutAge + } + buffer_json_add_array_item_string(wb, stream_handshake_error_to_string(s.stream.reason)); // OutReason + buffer_json_add_array_item_uint64(wb, s.stream.hops); // OutHops + buffer_json_add_array_item_double(wb, s.stream.replication.completion); // OutReplCompletion + buffer_json_add_array_item_uint64(wb, s.stream.replication.instances); // OutReplInstances + buffer_json_add_array_item_string(wb, s.stream.peers.local.ip); // OutLocalIP + buffer_json_add_array_item_uint64(wb, s.stream.peers.local.port); // OutLocalPort + buffer_json_add_array_item_string(wb, s.stream.peers.peer.ip); // OutRemoteIP + buffer_json_add_array_item_uint64(wb, s.stream.peers.peer.port); // OutRemotePort + buffer_json_add_array_item_string(wb, s.stream.ssl ? "SSL" : "PLAIN"); // OutSSL + buffer_json_add_array_item_string(wb, s.stream.compression ? "COMPRESSED" : "UNCOMPRESSED"); // OutCompression + stream_capabilities_to_json_array(wb, s.stream.capabilities, NULL); // OutCapabilities + buffer_json_add_array_item_uint64(wb, s.stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_DATA]); + buffer_json_add_array_item_uint64(wb, s.stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_METADATA]); + buffer_json_add_array_item_uint64(wb, s.stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_REPLICATION]); + buffer_json_add_array_item_uint64(wb, s.stream.sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_FUNCTIONS]); + + buffer_json_add_array_item_array(wb); // OutAttemptHandshake + time_t last_attempt = 0; + for(struct rrdpush_destinations *d = host->destinations; d ; d = d->next) { + if(d->since > last_attempt) + last_attempt = d->since; + + buffer_json_add_array_item_string(wb, stream_handshake_error_to_string(d->reason)); + } + buffer_json_array_close(wb); // // OutAttemptHandshake + + if(!last_attempt) { + buffer_json_add_array_item_string(wb, NULL); // OutAttemptSince + buffer_json_add_array_item_string(wb, NULL); // OutAttemptAge + } + else { + buffer_json_add_array_item_uint64(wb, last_attempt * 1000); // OutAttemptSince + buffer_json_add_array_item_time_t(wb, s.now - last_attempt); // OutAttemptAge + } + + // ML + if(s.ml.status == RRDHOST_ML_STATUS_RUNNING) { + buffer_json_add_array_item_uint64(wb, s.ml.metrics.anomalous); // MlAnomalous + buffer_json_add_array_item_uint64(wb, s.ml.metrics.normal); // MlNormal + buffer_json_add_array_item_uint64(wb, s.ml.metrics.trained); // MlTrained + buffer_json_add_array_item_uint64(wb, s.ml.metrics.pending); // MlPending + buffer_json_add_array_item_uint64(wb, s.ml.metrics.silenced); // MlSilenced + + if(s.ml.metrics.anomalous > max_ml_anomalous) + max_ml_anomalous = s.ml.metrics.anomalous; + + if(s.ml.metrics.normal > max_ml_normal) + max_ml_normal = s.ml.metrics.normal; + + if(s.ml.metrics.trained > max_ml_trained) + max_ml_trained = s.ml.metrics.trained; + + if(s.ml.metrics.pending > max_ml_pending) + max_ml_pending = s.ml.metrics.pending; + + if(s.ml.metrics.silenced > max_ml_silenced) + max_ml_silenced = s.ml.metrics.silenced; + + } + else { + buffer_json_add_array_item_string(wb, NULL); // MlAnomalous + buffer_json_add_array_item_string(wb, NULL); // MlNormal + buffer_json_add_array_item_string(wb, NULL); // MlTrained + buffer_json_add_array_item_string(wb, NULL); // MlPending + buffer_json_add_array_item_string(wb, NULL); // MlSilenced + } + + // close + buffer_json_array_close(wb); + } + dfe_done(host); + } + buffer_json_array_close(wb); // data + buffer_json_member_add_object(wb, "columns"); + { + size_t field_id = 0; + + // Node + buffer_rrdf_table_add_field(wb, field_id++, "Node", "Node's Hostname", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE | RRDF_FIELD_OPTS_UNIQUE_KEY | RRDF_FIELD_OPTS_STICKY, + NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbFrom", "DB Data Retention From", + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbTo", "DB Data Retention To", + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbDuration", "DB Data Retention Duration", + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbMetrics", "Time-series Metrics in the DB", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, (double)max_db_metrics, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbInstances", "Instances in the DB", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, (double)max_db_instances, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "dbContexts", "Contexts in the DB", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, (double)max_db_contexts, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + // --- statuses --- + + buffer_rrdf_table_add_field(wb, field_id++, "InStatus", "Data Collection Online Status", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + + buffer_rrdf_table_add_field(wb, field_id++, "OutStatus", "Streaming Online Status", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "MlStatus", "ML Status", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + // --- collection --- + + buffer_rrdf_table_add_field(wb, field_id++, "InSince", "Last Data Collection Status Change", + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, + 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InAge", "Last Data Collection Online Status Change Age", + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InReason", "Data Collection Online Status Reason", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InHops", "Data Collection Distance Hops from Origin Node", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InReplCompletion", "Inbound Replication Completion", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_BAR, RRDF_FIELD_TRANSFORM_NUMBER, + 1, "%", 100.0, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InReplInstances", "Inbound Replicating Instances", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "instances", (double)max_collection_replication_instances, RRDF_FIELD_SORT_DESCENDING, + NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InLocalIP", "Inbound Local IP", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InLocalPort", "Inbound Local Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InRemoteIP", "Inbound Remote IP", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InRemotePort", "Inbound Remote Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InSSL", "Inbound SSL Connection", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "InCapabilities", "Inbound Connection Capabilities", + RRDF_FIELD_TYPE_ARRAY, RRDF_FIELD_VISUAL_PILL, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + // --- streaming --- + + buffer_rrdf_table_add_field(wb, field_id++, "OutSince", "Last Streaming Status Change", + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, + 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutAge", "Last Streaming Status Change Age", + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutReason", "Streaming Status Reason", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutHops", "Streaming Distance Hops from Origin Node", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutReplCompletion", "Outbound Replication Completion", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_BAR, RRDF_FIELD_TRANSFORM_NUMBER, + 1, "%", 100.0, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutReplInstances", "Outbound Replicating Instances", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "instances", (double)max_streaming_replication_instances, RRDF_FIELD_SORT_DESCENDING, + NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutLocalIP", "Outbound Local IP", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutLocalPort", "Outbound Local Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutRemoteIP", "Outbound Remote IP", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutRemotePort", "Outbound Remote Port", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutSSL", "Outbound SSL Connection", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutCompression", "Outbound Compressed Connection", + RRDF_FIELD_TYPE_STRING, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutCapabilities", "Outbound Connection Capabilities", + RRDF_FIELD_TYPE_ARRAY, RRDF_FIELD_VISUAL_PILL, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutTrafficData", "Outbound Metric Data Traffic", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "bytes", (double)max_sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_DATA], + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutTrafficMetadata", "Outbound Metric Metadata Traffic", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "bytes", + (double)max_sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_METADATA], + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutTrafficReplication", "Outbound Metric Replication Traffic", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "bytes", + (double)max_sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_REPLICATION], + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutTrafficFunctions", "Outbound Metric Functions Traffic", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "bytes", + (double)max_sent_bytes_on_this_connection_per_type[STREAM_TRAFFIC_TYPE_FUNCTIONS], + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutAttemptHandshake", + "Outbound Connection Attempt Handshake Status", + RRDF_FIELD_TYPE_ARRAY, RRDF_FIELD_VISUAL_PILL, RRDF_FIELD_TRANSFORM_NONE, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_COUNT, RRDF_FIELD_FILTER_MULTISELECT, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutAttemptSince", + "Last Outbound Connection Attempt Status Change Time", + RRDF_FIELD_TYPE_TIMESTAMP, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DATETIME_MS, + 0, NULL, NAN, RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_MAX, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "OutAttemptAge", + "Last Outbound Connection Attempt Status Change Age", + RRDF_FIELD_TYPE_DURATION, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_DURATION_S, + 0, NULL, NAN, RRDF_FIELD_SORT_ASCENDING, NULL, + RRDF_FIELD_SUMMARY_MIN, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_VISIBLE, NULL); + + // --- ML --- + + buffer_rrdf_table_add_field(wb, field_id++, "MlAnomalous", "Number of Anomalous Metrics", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "metrics", + (double)max_ml_anomalous, + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "MlNormal", "Number of Not Anomalous Metrics", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "metrics", + (double)max_ml_normal, + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "MlTrained", "Number of Trained Metrics", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "metrics", + (double)max_ml_trained, + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "MlPending", "Number of Pending Metrics", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "metrics", + (double)max_ml_pending, + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + + buffer_rrdf_table_add_field(wb, field_id++, "MlSilenced", "Number of Silenced Metrics", + RRDF_FIELD_TYPE_INTEGER, RRDF_FIELD_VISUAL_VALUE, RRDF_FIELD_TRANSFORM_NUMBER, + 0, "metrics", + (double)max_ml_silenced, + RRDF_FIELD_SORT_DESCENDING, NULL, + RRDF_FIELD_SUMMARY_SUM, RRDF_FIELD_FILTER_RANGE, + RRDF_FIELD_OPTS_NONE, NULL); + } + buffer_json_object_close(wb); // columns + buffer_json_member_add_string(wb, "default_sort_column", "Node"); + buffer_json_member_add_object(wb, "charts"); + { + // Data Collection Age chart + buffer_json_member_add_object(wb, "InAge"); + { + buffer_json_member_add_string(wb, "name", "Data Collection Age"); + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "InAge"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + // Streaming Age chart + buffer_json_member_add_object(wb, "OutAge"); + { + buffer_json_member_add_string(wb, "name", "Streaming Age"); + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "OutAge"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + // DB Duration + buffer_json_member_add_object(wb, "dbDuration"); + { + buffer_json_member_add_string(wb, "name", "Retention Duration"); + buffer_json_member_add_string(wb, "type", "stacked-bar"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "dbDuration"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); // charts + + buffer_json_member_add_array(wb, "default_charts"); + { + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, "InAge"); + buffer_json_add_array_item_string(wb, "Node"); + buffer_json_array_close(wb); + + buffer_json_add_array_item_array(wb); + buffer_json_add_array_item_string(wb, "OutAge"); + buffer_json_add_array_item_string(wb, "Node"); + buffer_json_array_close(wb); + } + buffer_json_array_close(wb); + + buffer_json_member_add_object(wb, "group_by"); + { + buffer_json_member_add_object(wb, "Node"); + { + buffer_json_member_add_string(wb, "name", "Node"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "Node"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "InStatus"); + { + buffer_json_member_add_string(wb, "name", "Nodes by Collection Status"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "InStatus"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "OutStatus"); + { + buffer_json_member_add_string(wb, "name", "Nodes by Streaming Status"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "OutStatus"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "MlStatus"); + { + buffer_json_member_add_string(wb, "name", "Nodes by ML Status"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "MlStatus"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "InRemoteIP"); + { + buffer_json_member_add_string(wb, "name", "Nodes by Inbound IP"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "InRemoteIP"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + + buffer_json_member_add_object(wb, "OutRemoteIP"); + { + buffer_json_member_add_string(wb, "name", "Nodes by Outbound IP"); + buffer_json_member_add_array(wb, "columns"); + { + buffer_json_add_array_item_string(wb, "OutRemoteIP"); + } + buffer_json_array_close(wb); + } + buffer_json_object_close(wb); + } + buffer_json_object_close(wb); // group_by + + buffer_json_member_add_time_t(wb, "expires", now_realtime_sec() + 1); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} diff --git a/src/web/api/functions/function-streaming.h b/src/web/api/functions/function-streaming.h new file mode 100644 index 00000000..06da6af9 --- /dev/null +++ b/src/web/api/functions/function-streaming.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_FUNCTION_STREAMING_H +#define NETDATA_FUNCTION_STREAMING_H + +#include "daemon/common.h" + +#define RRDFUNCTIONS_STREAMING_HELP "Streaming status for parents and children." + +int function_streaming(BUFFER *wb, const char *function, BUFFER *payload, const char *source); + +#endif //NETDATA_FUNCTION_STREAMING_H diff --git a/src/web/api/functions/functions.c b/src/web/api/functions/functions.c new file mode 100644 index 00000000..c00e04ca --- /dev/null +++ b/src/web/api/functions/functions.c @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "functions.h" + +void global_functions_add(void) { + // we register this only on localhost + // for the other nodes, the origin server should register it + rrd_function_add_inline( + localhost, + NULL, + "streaming", + 10, + RRDFUNCTIONS_PRIORITY_DEFAULT + 1, + RRDFUNCTIONS_VERSION_DEFAULT, + RRDFUNCTIONS_STREAMING_HELP, + "top", + HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_SENSITIVE_DATA, + function_streaming); + + rrd_function_add_inline( + localhost, + NULL, + "netdata-api-calls", + 10, + RRDFUNCTIONS_PRIORITY_DEFAULT + 2, + RRDFUNCTIONS_VERSION_DEFAULT, + RRDFUNCTIONS_PROGRESS_HELP, + "top", + HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_SENSITIVE_DATA, + function_progress); + + rrd_function_add_inline( + localhost, + NULL, + RRDFUNCTIONS_BEARER_GET_TOKEN, + 10, + RRDFUNCTIONS_PRIORITY_DEFAULT + 3, + RRDFUNCTIONS_VERSION_DEFAULT, + RRDFUNCTIONS_BEARER_GET_TOKEN_HELP, + RRDFUNCTIONS_TAG_HIDDEN, + HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_SENSITIVE_DATA, + function_bearer_get_token); +} diff --git a/src/web/api/functions/functions.h b/src/web/api/functions/functions.h new file mode 100644 index 00000000..28c48354 --- /dev/null +++ b/src/web/api/functions/functions.h @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_FUNCTIONS_H +#define NETDATA_FUNCTIONS_H + +#include "daemon/common.h" + +#include "function-streaming.h" +#include "function-progress.h" +#include "function-bearer_get_token.h" + +void global_functions_add(void); + +#endif //NETDATA_FUNCTIONS_H diff --git a/src/web/api/health/README.md b/src/web/api/health/README.md index 725b4a36..a788e9b4 100644 --- a/src/web/api/health/README.md +++ b/src/web/api/health/README.md @@ -1,13 +1,3 @@ -<!-- -title: "Health API Calls" -date: 2020-04-27 -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/health/README.md -sidebar_label: "Health API Calls" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api" ---> - # Health API Calls ## Health Read API @@ -33,7 +23,7 @@ The size of the alert log is configured in `netdata.conf`. There are 2 settings: ``` [health] in memory max health log entries = 1000 - health log history = 432000 + health log retention = 5d ``` The API call retrieves all entries of the alert log: diff --git a/src/web/api/http_auth.c b/src/web/api/http_auth.c index ec052030..5c4fffca 100644 --- a/src/web/api/http_auth.c +++ b/src/web/api/http_auth.c @@ -2,83 +2,341 @@ #include "http_auth.h" -#define BEARER_TOKEN_EXPIRATION 86400 +#define BEARER_TOKEN_EXPIRATION (86400 * 1) -bool netdata_is_protected_by_bearer = false; // this is controlled by cloud, at the point the agent logs in - this should also be saved to /var/lib/netdata +bool netdata_is_protected_by_bearer = false; static DICTIONARY *netdata_authorized_bearers = NULL; struct bearer_token { nd_uuid_t cloud_account_id; - char cloud_user_name[CLOUD_USER_NAME_LENGTH]; + char client_name[CLOUD_CLIENT_NAME_LENGTH]; HTTP_ACCESS access; HTTP_USER_ROLE user_role; time_t created_s; time_t expires_s; }; -bool web_client_bearer_token_auth(struct web_client *w, const char *v) { - if(!uuid_parse_flexi(v, w->auth.bearer_token)) { - char uuid_str[UUID_COMPACT_STR_LEN]; - uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str); +static void bearer_tokens_path(char out[FILENAME_MAX]) { + filename_from_path_entry(out, netdata_configured_varlib_dir, "bearer_tokens", NULL); +} - struct bearer_token *z = dictionary_get(netdata_authorized_bearers, uuid_str); - if (z && z->expires_s > now_monotonic_sec()) { - strncpyz(w->auth.client_name, z->cloud_user_name, sizeof(w->auth.client_name) - 1); - uuid_copy(w->auth.cloud_account_id, z->cloud_account_id); - web_client_set_permissions(w, z->access, z->user_role, WEB_CLIENT_FLAG_AUTH_BEARER); - return true; - } - } - else - nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v); +static void bearer_token_filename(char out[FILENAME_MAX], nd_uuid_t uuid) { + char uuid_str[UUID_STR_LEN]; + uuid_unparse_lower(uuid, uuid_str); + + char path[FILENAME_MAX]; + bearer_tokens_path(path); + filename_from_path_entry(out, path, uuid_str, NULL); +} - return false; +static inline bool bearer_tokens_ensure_path_exists(void) { + char path[FILENAME_MAX]; + bearer_tokens_path(path); + return filename_is_dir(path, true); } -static void bearer_token_cleanup(void) { +static void bearer_token_delete_from_disk(nd_uuid_t *token) { + char filename[FILENAME_MAX]; + bearer_token_filename(filename, *token); + if(unlink(filename) != 0) + nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to unlink() file '%s'", filename); +} + +static void bearer_token_cleanup(bool force) { static time_t attempts = 0; - if(++attempts % 1000 != 0) + if(++attempts % 1000 != 0 && !force) return; - time_t now_s = now_monotonic_sec(); + time_t now_s = now_realtime_sec(); struct bearer_token *z; dfe_start_read(netdata_authorized_bearers, z) { - if(z->expires_s < now_s) + if(z->expires_s < now_s) { + nd_uuid_t uuid; + if(uuid_parse_flexi(z_dfe.name, uuid) == 0) + bearer_token_delete_from_disk(&uuid); + dictionary_del(netdata_authorized_bearers, z_dfe.name); + } } dfe_done(z); dictionary_garbage_collect(netdata_authorized_bearers); } -void bearer_tokens_init(void) { - netdata_authorized_bearers = dictionary_create_advanced( - DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, - NULL, sizeof(struct bearer_token)); +static uint64_t bearer_token_signature(nd_uuid_t token, struct bearer_token *bt) { + // we use a custom structure to make sure that changes in the other code will not affect the signature + + struct { + nd_uuid_t host_uuid; + nd_uuid_t token; + nd_uuid_t cloud_account_id; + char client_name[CLOUD_CLIENT_NAME_LENGTH]; + HTTP_ACCESS access; + HTTP_USER_ROLE user_role; + time_t created_s; + time_t expires_s; + } signature_payload = { + .access = bt->access, + .user_role = bt->user_role, + .created_s = bt->created_s, + .expires_s = bt->expires_s, + }; + uuid_copy(signature_payload.host_uuid, localhost->host_id.uuid); + uuid_copy(signature_payload.token, token); + uuid_copy(signature_payload.cloud_account_id, bt->cloud_account_id); + memset(signature_payload.client_name, 0, sizeof(signature_payload.client_name)); + strncpyz(signature_payload.client_name, bt->client_name, sizeof(signature_payload.client_name) - 1); + + return XXH3_64bits(&signature_payload, sizeof(signature_payload)); } -time_t bearer_create_token(nd_uuid_t *uuid, struct web_client *w) { +static bool bearer_token_save_to_file(nd_uuid_t token, struct bearer_token *bt) { + CLEAN_BUFFER *wb = buffer_create(0, NULL); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + buffer_json_member_add_uint64(wb, "version", 1); + buffer_json_member_add_uuid(wb, "host_uuid", localhost->host_id.uuid); + buffer_json_member_add_uuid(wb, "token", token); + buffer_json_member_add_uuid(wb, "cloud_account_id", bt->cloud_account_id); + buffer_json_member_add_string(wb, "client_name", bt->client_name); + http_access2buffer_json_array(wb, "access", bt->access); + buffer_json_member_add_string(wb, "user_role", http_id2user_role(bt->user_role)); + buffer_json_member_add_uint64(wb, "created_s", bt->created_s); + buffer_json_member_add_uint64(wb, "expires_s", bt->expires_s); + buffer_json_member_add_uint64(wb, "signature", bearer_token_signature(token, bt)); + buffer_json_finalize(wb); + + char filename[FILENAME_MAX]; + bearer_token_filename(filename, token); + + FILE *fp = fopen(filename, "w"); + if(!fp) { + nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot create file '%s'", filename); + return false; + } + + if(fwrite(buffer_tostring(wb), 1, buffer_strlen(wb), fp) != buffer_strlen(wb)) { + fclose(fp); + unlink(filename); + nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot save file '%s'", filename); + return false; + } + + fclose(fp); + return true; +} + +static time_t bearer_create_token_internal(nd_uuid_t token, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name, time_t created_s, time_t expires_s, bool save) { char uuid_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(token, uuid_str); + + struct bearer_token t = { 0 }, *bt; + const DICTIONARY_ITEM *item = dictionary_set_and_acquire_item(netdata_authorized_bearers, uuid_str, &t, sizeof(t)); + bt = dictionary_acquired_item_value(item); + + if(!bt->created_s) { + bt->created_s = created_s; + bt->expires_s = expires_s; + bt->user_role = user_role; + bt->access = access; + + uuid_copy(bt->cloud_account_id, cloud_account_id); + strncpyz(bt->client_name, client_name, sizeof(bt->cloud_account_id) - 1); + + if(save) + bearer_token_save_to_file(token, bt); + } + + time_t expiration = bt->expires_s; + + dictionary_acquired_item_release(netdata_authorized_bearers, item); + + return expiration; +} + +time_t bearer_create_token(nd_uuid_t *uuid, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name) { + time_t now_s = now_realtime_sec(); + time_t expires_s = 0; + + struct bearer_token *bt; + dfe_start_read(netdata_authorized_bearers, bt) { + if(bt->expires_s > now_s + 3600 * 2 && // expires in more than 2 hours + user_role == bt->user_role && // the user_role matches + access == bt->access && // the access matches + uuid_eq(cloud_account_id, bt->cloud_account_id) && // the cloud_account_id matches + strncmp(client_name, bt->client_name, sizeof(bt->client_name) - 1) == 0 && // the client_name matches + uuid_parse_flexi(bt_dfe.name, *uuid) == 0) // the token can be parsed + return expires_s; /* dfe will cleanup automatically */ + } + dfe_done(bt); uuid_generate_random(*uuid); - uuid_unparse_lower_compact(*uuid, uuid_str); - - struct bearer_token t = { 0 }, *z; - z = dictionary_set(netdata_authorized_bearers, uuid_str, &t, sizeof(t)); - if(!z->created_s) { - z->created_s = now_monotonic_sec(); - z->expires_s = z->created_s + BEARER_TOKEN_EXPIRATION; - z->user_role = w->user_role; - z->access = w->access; - uuid_copy(z->cloud_account_id, w->auth.cloud_account_id); - strncpyz(z->cloud_user_name, w->auth.client_name, sizeof(z->cloud_account_id) - 1); + expires_s = bearer_create_token_internal( + *uuid, user_role, access, cloud_account_id, client_name, + now_s, now_s + BEARER_TOKEN_EXPIRATION, true); + + bearer_token_cleanup(false); + + return expires_s; +} + +static bool bearer_token_parse_json(nd_uuid_t token, struct json_object *jobj, BUFFER *error) { + int64_t version; + nd_uuid_t token_in_file, cloud_account_id, host_uuid; + CLEAN_STRING *client_name = NULL; + HTTP_USER_ROLE user_role = HTTP_USER_ROLE_NONE; + HTTP_ACCESS access = HTTP_ACCESS_NONE; + time_t created_s = 0, expires_s = 0; + uint64_t signature = 0; + + JSONC_PARSE_INT64_OR_ERROR_AND_RETURN(jobj, ".", "version", version, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "host_uuid", host_uuid, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "token", token_in_file, error, true); + JSONC_PARSE_TXT2UUID_OR_ERROR_AND_RETURN(jobj, ".", "cloud_account_id", cloud_account_id, error, true); + JSONC_PARSE_TXT2STRING_OR_ERROR_AND_RETURN(jobj, ".", "client_name", client_name, error, true); + JSONC_PARSE_ARRAY_OF_TXT2BITMAP_OR_ERROR_AND_RETURN(jobj, ".", "access", http_access2id_one, access, error, true); + JSONC_PARSE_TXT2ENUM_OR_ERROR_AND_RETURN(jobj, ".", "user_role", http_user_role2id, user_role, error, true); + JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "created_s", created_s, error, true); + JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "expires_s", expires_s, error, true); + JSONC_PARSE_UINT64_OR_ERROR_AND_RETURN(jobj, ".", "signature", signature, error, true); + + if(uuid_compare(token, token_in_file) != 0) { + buffer_flush(error); + buffer_strcat(error, "token in JSON file does not match the filename"); + return false; + } + + if(uuid_compare(host_uuid, localhost->host_id.uuid) != 0) { + buffer_flush(error); + buffer_strcat(error, "Host UUID in JSON file does not match our host UUID"); + return false; + } + + if(!created_s || !expires_s || created_s >= expires_s) { + buffer_flush(error); + buffer_strcat(error, "bearer token has invalid dates"); + return false; + } + + struct bearer_token bt = { + .access = access, + .user_role = user_role, + .created_s = created_s, + .expires_s = expires_s, + }; + uuid_copy(bt.cloud_account_id, cloud_account_id); + strncpyz(bt.client_name, string2str(client_name), sizeof(bt.client_name) - 1); + + if(signature != bearer_token_signature(token_in_file, &bt)) { + buffer_flush(error); + buffer_strcat(error, "bearer token has invalid signature"); + return false; + } + + bearer_create_token_internal(token, user_role, access, + cloud_account_id, string2str(client_name), + created_s, expires_s, false); + + return true; +} + +static bool bearer_token_load_token(nd_uuid_t token) { + char filename[FILENAME_MAX]; + bearer_token_filename(filename, token); + + CLEAN_BUFFER *wb = buffer_create(0, NULL); + if(!read_txt_file_to_buffer(filename, wb, 1 * 1024 * 1024)) + return false; + + CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(wb)); + if (jobj == NULL) { + nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot parse bearer token file '%s'", filename); + return false; + } + + CLEAN_BUFFER *error = buffer_create(0, NULL); + bool rc = bearer_token_parse_json(token, jobj, error); + if(!rc) { + nd_log(NDLS_DAEMON, NDLP_ERR, "Failed to parse bearer token file '%s': %s", filename, buffer_tostring(error)); + unlink(filename); + return false; + } + + bearer_token_cleanup(true); + + return true; +} + +static void bearer_tokens_load_from_disk(void) { + bearer_tokens_ensure_path_exists(); + + char path[FILENAME_MAX]; + bearer_tokens_path(path); + + DIR *dir = opendir(path); + if(!dir) { + nd_log(NDLS_DAEMON, NDLP_ERR, "Cannot open directory '%s' to read saved bearer tokens", path); + return; } - bearer_token_cleanup(); + struct dirent *de; + while((de = readdir(dir))) { + if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0) + continue; + + ND_UUID uuid = UUID_ZERO; + if(uuid_parse_flexi(de->d_name, uuid.uuid) != 0 || UUIDiszero(uuid)) + continue; + + char filename[FILENAME_MAX]; + filename_from_path_entry(filename, path, de->d_name, NULL); + + if(de->d_type == DT_REG || (de->d_type == DT_LNK && filename_is_file(filename))) + bearer_token_load_token(uuid.uuid); + } + + closedir(dir); +} + +bool web_client_bearer_token_auth(struct web_client *w, const char *v) { + bool rc = false; + + if(!uuid_parse_flexi(v, w->auth.bearer_token)) { + char uuid_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->auth.bearer_token, uuid_str); + + const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str); + if(!item && bearer_token_load_token(w->auth.bearer_token)) + item = dictionary_get_and_acquire_item(netdata_authorized_bearers, uuid_str); + + if(item) { + struct bearer_token *bt = dictionary_acquired_item_value(item); + if (bt->expires_s > now_realtime_sec()) { + strncpyz(w->auth.client_name, bt->client_name, sizeof(w->auth.client_name) - 1); + uuid_copy(w->auth.cloud_account_id, bt->cloud_account_id); + web_client_set_permissions(w, bt->access, bt->user_role, WEB_CLIENT_FLAG_AUTH_BEARER); + rc = true; + } + + dictionary_acquired_item_release(netdata_authorized_bearers, item); + } + } + else + nd_log(NDLS_DAEMON, NDLP_NOTICE, "Invalid bearer token '%s' received.", v); + + return rc; +} + +void bearer_tokens_init(void) { + netdata_is_protected_by_bearer = + config_get_boolean(CONFIG_SECTION_WEB, "bearer token protection", netdata_is_protected_by_bearer); + + netdata_authorized_bearers = dictionary_create_advanced( + DICT_OPTION_DONT_OVERWRITE_VALUE | DICT_OPTION_FIXED_SIZE, + NULL, sizeof(struct bearer_token)); - return now_realtime_sec() + BEARER_TOKEN_EXPIRATION; + bearer_tokens_load_from_disk(); } bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len) { diff --git a/src/web/api/http_auth.h b/src/web/api/http_auth.h index f339a44c..0b01fdb1 100644 --- a/src/web/api/http_auth.h +++ b/src/web/api/http_auth.h @@ -11,7 +11,7 @@ extern bool netdata_is_protected_by_bearer; bool extract_bearer_token_from_request(struct web_client *w, char *dst, size_t dst_len); -time_t bearer_create_token(nd_uuid_t *uuid, struct web_client *w); +time_t bearer_create_token(nd_uuid_t *uuid, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name); bool web_client_bearer_token_auth(struct web_client *w, const char *v); static inline bool http_access_user_has_enough_access_level_for_endpoint(HTTP_ACCESS user, HTTP_ACCESS endpoint) { diff --git a/src/web/api/ilove/ilove.h b/src/web/api/ilove/ilove.h deleted file mode 100644 index 010c19c6..00000000 --- a/src/web/api/ilove/ilove.h +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-or-later - -#ifndef NETDATA_WEB_API_ILOVE_H -#define NETDATA_WEB_API_ILOVE_H 1 - -#include "libnetdata/libnetdata.h" -#include "web/server/web_client.h" - -int web_client_api_request_v2_ilove(RRDHOST *host, struct web_client *w, char *url); - -#include "web/api/web_api_v1.h" - -#endif /* NETDATA_WEB_API_ILOVE_H */ diff --git a/src/web/api/maps/contexts_alert_statuses.c b/src/web/api/maps/contexts_alert_statuses.c new file mode 100644 index 00000000..d3565c9e --- /dev/null +++ b/src/web/api/maps/contexts_alert_statuses.c @@ -0,0 +1,60 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "contexts_alert_statuses.h" + +static struct { + const char *name; + uint32_t hash; + CONTEXTS_ALERT_STATUS value; +} contexts_alert_status[] = { + {"uninitialized" , 0 , CONTEXT_ALERT_UNINITIALIZED} + , {"undefined" , 0 , CONTEXT_ALERT_UNDEFINED} + , {"clear" , 0 , CONTEXT_ALERT_CLEAR} + , {"raised" , 0 , CONTEXT_ALERT_RAISED} + , {"active" , 0 , CONTEXT_ALERT_RAISED} + , {"warning" , 0 , CONTEXT_ALERT_WARNING} + , {"critical" , 0 , CONTEXT_ALERT_CRITICAL} + , {NULL , 0 , 0} +}; + +CONTEXTS_ALERT_STATUS contexts_alert_status_str_to_id(char *o) { + CONTEXTS_ALERT_STATUS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + + uint32_t hash = simple_hash(tok); + int i; + for(i = 0; contexts_alert_status[i].name ; i++) { + if (unlikely(hash == contexts_alert_status[i].hash && !strcmp(tok, contexts_alert_status[i].name))) { + ret |= contexts_alert_status[i].value; + break; + } + } + } + + return ret; +} + +void contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, + CONTEXTS_ALERT_STATUS options) { + buffer_json_member_add_array(wb, key); + + CONTEXTS_ALERT_STATUS used = 0; // to prevent adding duplicates + for(int i = 0; contexts_alert_status[i].name ; i++) { + if (unlikely((contexts_alert_status[i].value & options) && !(contexts_alert_status[i].value & used))) { + const char *name = contexts_alert_status[i].name; + used |= contexts_alert_status[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void contexts_alert_statuses_init(void) { + for(size_t i = 0; contexts_alert_status[i].name ; i++) + contexts_alert_status[i].hash = simple_hash(contexts_alert_status[i].name); +} diff --git a/src/web/api/maps/contexts_alert_statuses.h b/src/web/api/maps/contexts_alert_statuses.h new file mode 100644 index 00000000..1c38cb97 --- /dev/null +++ b/src/web/api/maps/contexts_alert_statuses.h @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CONTEXTS_ALERT_STATUSES_H +#define NETDATA_CONTEXTS_ALERT_STATUSES_H + +#include "libnetdata/libnetdata.h" + +typedef enum contexts_alert_status { + CONTEXT_ALERT_UNINITIALIZED = (1 << 6), // include UNINITIALIZED alerts + CONTEXT_ALERT_UNDEFINED = (1 << 7), // include UNDEFINED alerts + CONTEXT_ALERT_CLEAR = (1 << 8), // include CLEAR alerts + CONTEXT_ALERT_RAISED = (1 << 9), // include WARNING & CRITICAL alerts + CONTEXT_ALERT_WARNING = (1 << 10), // include WARNING alerts + CONTEXT_ALERT_CRITICAL = (1 << 11), // include CRITICAL alerts +} CONTEXTS_ALERT_STATUS; + +#define CONTEXTS_ALERT_STATUSES (CONTEXT_ALERT_UNINITIALIZED | CONTEXT_ALERT_UNDEFINED | CONTEXT_ALERT_CLEAR | \ + CONTEXT_ALERT_RAISED | CONTEXT_ALERT_WARNING | CONTEXT_ALERT_CRITICAL) + +CONTEXTS_ALERT_STATUS contexts_alert_status_str_to_id(char *o); +void contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, + CONTEXTS_ALERT_STATUS options); + +void contexts_alert_statuses_init(void); + +#endif //NETDATA_CONTEXTS_ALERT_STATUSES_H diff --git a/src/web/api/maps/contexts_options.c b/src/web/api/maps/contexts_options.c new file mode 100644 index 00000000..22e50e8d --- /dev/null +++ b/src/web/api/maps/contexts_options.c @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "contexts_options.h" + +static struct { + const char *name; + uint32_t hash; + CONTEXTS_OPTIONS value; +} contexts_options[] = { + {"minify" , 0 , CONTEXTS_OPTION_MINIFY} + , {"debug" , 0 , CONTEXTS_OPTION_DEBUG} + , {"config" , 0 , CONTEXTS_OPTION_ALERTS_WITH_CONFIGURATIONS} + , {"instances" , 0 , CONTEXTS_OPTION_ALERTS_WITH_INSTANCES} + , {"values" , 0 , CONTEXTS_OPTION_ALERTS_WITH_VALUES} + , {"summary" , 0 , CONTEXTS_OPTION_ALERTS_WITH_SUMMARY} + , {NULL , 0 , 0} +}; + +CONTEXTS_OPTIONS contexts_options_str_to_id(char *o) { + CONTEXTS_OPTIONS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + + uint32_t hash = simple_hash(tok); + int i; + for(i = 0; contexts_options[i].name ; i++) { + if (unlikely(hash == contexts_options[i].hash && !strcmp(tok, contexts_options[i].name))) { + ret |= contexts_options[i].value; + break; + } + } + } + + return ret; +} + +void contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_OPTIONS options) { + buffer_json_member_add_array(wb, key); + + CONTEXTS_OPTIONS used = 0; // to prevent adding duplicates + for(int i = 0; contexts_options[i].name ; i++) { + if (unlikely((contexts_options[i].value & options) && !(contexts_options[i].value & used))) { + const char *name = contexts_options[i].name; + used |= contexts_options[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void contexts_options_init(void) { + for(size_t i = 0; contexts_options[i].name ; i++) + contexts_options[i].hash = simple_hash(contexts_options[i].name); +} diff --git a/src/web/api/maps/contexts_options.h b/src/web/api/maps/contexts_options.h new file mode 100644 index 00000000..a21bd76c --- /dev/null +++ b/src/web/api/maps/contexts_options.h @@ -0,0 +1,22 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_CONTEXTS_OPTIONS_H +#define NETDATA_CONTEXTS_OPTIONS_H + +#include "libnetdata/libnetdata.h" + +typedef enum contexts_options { + CONTEXTS_OPTION_MINIFY = (1 << 0), // remove JSON spaces and newlines from JSON output + CONTEXTS_OPTION_DEBUG = (1 << 1), // show the request + CONTEXTS_OPTION_ALERTS_WITH_CONFIGURATIONS = (1 << 2), // include alert configurations (used by /api/v2/alert_transitions) + CONTEXTS_OPTION_ALERTS_WITH_INSTANCES = (1 << 3), // include alert instances (used by /api/v2/alerts) + CONTEXTS_OPTION_ALERTS_WITH_VALUES = (1 << 4), // include alert latest values (used by /api/v2/alerts) + CONTEXTS_OPTION_ALERTS_WITH_SUMMARY = (1 << 5), // include alerts summary counters (used by /api/v2/alerts) +} CONTEXTS_OPTIONS; + +CONTEXTS_OPTIONS contexts_options_str_to_id(char *o); +void contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_OPTIONS options); + +void contexts_options_init(void); + +#endif //NETDATA_CONTEXTS_OPTIONS_H diff --git a/src/web/api/maps/datasource_formats.c b/src/web/api/maps/datasource_formats.c new file mode 100644 index 00000000..33e1e745 --- /dev/null +++ b/src/web/api/maps/datasource_formats.c @@ -0,0 +1,89 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "datasource_formats.h" + +static struct { + const char *name; + uint32_t hash; + DATASOURCE_FORMAT value; +} google_data_formats[] = { + // this is not an error - when Google requests json, it expects javascript + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source#responseformat + {"json", 0, DATASOURCE_DATATABLE_JSONP} + , {"html", 0, DATASOURCE_HTML} + , {"csv", 0, DATASOURCE_CSV} + , {"tsv-excel", 0, DATASOURCE_TSV} + + // terminator + , {NULL, 0, 0} +}; + +inline DATASOURCE_FORMAT google_data_format_str_to_id(char *name) { + uint32_t hash = simple_hash(name); + int i; + + for(i = 0; google_data_formats[i].name ; i++) { + if (unlikely(hash == google_data_formats[i].hash && !strcmp(name, google_data_formats[i].name))) { + return google_data_formats[i].value; + } + } + + return DATASOURCE_JSON; +} + +// -------------------------------------------------------------------------------------------------------------------- + +static struct { + const char *name; + uint32_t hash; + DATASOURCE_FORMAT value; +} datasource_formats[] = { + { "datatable" , 0 , DATASOURCE_DATATABLE_JSON} + , {"datasource" , 0 , DATASOURCE_DATATABLE_JSONP} + , {"json" , 0 , DATASOURCE_JSON} + , {"json2" , 0 , DATASOURCE_JSON2} + , {"jsonp" , 0 , DATASOURCE_JSONP} + , {"ssv" , 0 , DATASOURCE_SSV} + , {"csv" , 0 , DATASOURCE_CSV} + , {"tsv" , 0 , DATASOURCE_TSV} + , {"tsv-excel" , 0 , DATASOURCE_TSV} + , {"html" , 0 , DATASOURCE_HTML} + , {"array" , 0 , DATASOURCE_JS_ARRAY} + , {"ssvcomma" , 0 , DATASOURCE_SSV_COMMA} + , {"csvjsonarray" , 0 , DATASOURCE_CSV_JSON_ARRAY} + , {"markdown" , 0 , DATASOURCE_CSV_MARKDOWN} + + // terminator + , {NULL, 0, 0} +}; + +DATASOURCE_FORMAT datasource_format_str_to_id(char *name) { + uint32_t hash = simple_hash(name); + int i; + + for(i = 0; datasource_formats[i].name ; i++) { + if (unlikely(hash == datasource_formats[i].hash && !strcmp(name, datasource_formats[i].name))) { + return datasource_formats[i].value; + } + } + + return DATASOURCE_JSON; +} + +const char *rrdr_format_to_string(DATASOURCE_FORMAT format) { + for(size_t i = 0; datasource_formats[i].name ;i++) + if(unlikely(datasource_formats[i].value == format)) + return datasource_formats[i].name; + + return "unknown"; +} + +// -------------------------------------------------------------------------------------------------------------------- + +void datasource_formats_init(void) { + for(size_t i = 0; datasource_formats[i].name ; i++) + datasource_formats[i].hash = simple_hash(datasource_formats[i].name); + + for(size_t i = 0; google_data_formats[i].name ; i++) + google_data_formats[i].hash = simple_hash(google_data_formats[i].name); +} diff --git a/src/web/api/maps/datasource_formats.h b/src/web/api/maps/datasource_formats.h new file mode 100644 index 00000000..50d8a82b --- /dev/null +++ b/src/web/api/maps/datasource_formats.h @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_DATASOURCE_FORMATS_H +#define NETDATA_DATASOURCE_FORMATS_H + +#include "libnetdata/libnetdata.h" + +// type of JSON generations +typedef enum { + DATASOURCE_JSON = 0, + DATASOURCE_DATATABLE_JSON, + DATASOURCE_DATATABLE_JSONP, + DATASOURCE_SSV, + DATASOURCE_CSV, + DATASOURCE_JSONP, + DATASOURCE_TSV, + DATASOURCE_HTML, + DATASOURCE_JS_ARRAY, + DATASOURCE_SSV_COMMA, + DATASOURCE_CSV_JSON_ARRAY, + DATASOURCE_CSV_MARKDOWN, + DATASOURCE_JSON2, +} DATASOURCE_FORMAT; + +DATASOURCE_FORMAT datasource_format_str_to_id(char *name); +const char *rrdr_format_to_string(DATASOURCE_FORMAT format); + +DATASOURCE_FORMAT google_data_format_str_to_id(char *name); + +void datasource_formats_init(void); + +#endif //NETDATA_DATASOURCE_FORMATS_H diff --git a/src/web/api/maps/maps.h b/src/web/api/maps/maps.h new file mode 100644 index 00000000..25d21023 --- /dev/null +++ b/src/web/api/maps/maps.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_MAPS_H +#define NETDATA_MAPS_H + +#include "libnetdata/libnetdata.h" +#include "datasource_formats.h" +#include "contexts_options.h" +#include "rrdr_options.h" +#include "contexts_alert_statuses.h" + +#endif //NETDATA_MAPS_H diff --git a/src/web/api/maps/rrdr_options.c b/src/web/api/maps/rrdr_options.c new file mode 100644 index 00000000..41161d80 --- /dev/null +++ b/src/web/api/maps/rrdr_options.c @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "rrdr_options.h" + +static struct { + const char *name; + uint32_t hash; + RRDR_OPTIONS value; +} rrdr_options[] = { + { "nonzero" , 0 , RRDR_OPTION_NONZERO} + , {"flip" , 0 , RRDR_OPTION_REVERSED} + , {"reversed" , 0 , RRDR_OPTION_REVERSED} + , {"reverse" , 0 , RRDR_OPTION_REVERSED} + , {"jsonwrap" , 0 , RRDR_OPTION_JSON_WRAP} + , {"min2max" , 0 , RRDR_OPTION_DIMS_MIN2MAX} // rrdr2value() only + , {"average" , 0 , RRDR_OPTION_DIMS_AVERAGE} // rrdr2value() only + , {"min" , 0 , RRDR_OPTION_DIMS_MIN} // rrdr2value() only + , {"max" , 0 , RRDR_OPTION_DIMS_MAX} // rrdr2value() only + , {"ms" , 0 , RRDR_OPTION_MILLISECONDS} + , {"milliseconds" , 0 , RRDR_OPTION_MILLISECONDS} + , {"absolute" , 0 , RRDR_OPTION_ABSOLUTE} + , {"abs" , 0 , RRDR_OPTION_ABSOLUTE} + , {"absolute_sum" , 0 , RRDR_OPTION_ABSOLUTE} + , {"absolute-sum" , 0 , RRDR_OPTION_ABSOLUTE} + , {"display_absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} + , {"display-absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} + , {"seconds" , 0 , RRDR_OPTION_SECONDS} + , {"null2zero" , 0 , RRDR_OPTION_NULL2ZERO} + , {"objectrows" , 0 , RRDR_OPTION_OBJECTSROWS} + , {"google_json" , 0 , RRDR_OPTION_GOOGLE_JSON} + , {"google-json" , 0 , RRDR_OPTION_GOOGLE_JSON} + , {"percentage" , 0 , RRDR_OPTION_PERCENTAGE} + , {"unaligned" , 0 , RRDR_OPTION_NOT_ALIGNED} + , {"match_ids" , 0 , RRDR_OPTION_MATCH_IDS} + , {"match-ids" , 0 , RRDR_OPTION_MATCH_IDS} + , {"match_names" , 0 , RRDR_OPTION_MATCH_NAMES} + , {"match-names" , 0 , RRDR_OPTION_MATCH_NAMES} + , {"anomaly-bit" , 0 , RRDR_OPTION_ANOMALY_BIT} + , {"selected-tier" , 0 , RRDR_OPTION_SELECTED_TIER} + , {"raw" , 0 , RRDR_OPTION_RETURN_RAW} + , {"jw-anomaly-rates" , 0 , RRDR_OPTION_RETURN_JWAR} + , {"natural-points" , 0 , RRDR_OPTION_NATURAL_POINTS} + , {"virtual-points" , 0 , RRDR_OPTION_VIRTUAL_POINTS} + , {"all-dimensions" , 0 , RRDR_OPTION_ALL_DIMENSIONS} + , {"details" , 0 , RRDR_OPTION_SHOW_DETAILS} + , {"debug" , 0 , RRDR_OPTION_DEBUG} + , {"plan" , 0 , RRDR_OPTION_DEBUG} + , {"minify" , 0 , RRDR_OPTION_MINIFY} + , {"group-by-labels" , 0 , RRDR_OPTION_GROUP_BY_LABELS} + , {"label-quotes" , 0 , RRDR_OPTION_LABEL_QUOTES} + , {NULL , 0 , 0} +}; + +RRDR_OPTIONS rrdr_options_parse_one(const char *o) { + RRDR_OPTIONS ret = 0; + + if(!o || !*o) return ret; + + uint32_t hash = simple_hash(o); + int i; + for(i = 0; rrdr_options[i].name ; i++) { + if (unlikely(hash == rrdr_options[i].hash && !strcmp(o, rrdr_options[i].name))) { + ret |= rrdr_options[i].value; + break; + } + } + + return ret; +} + +RRDR_OPTIONS rrdr_options_parse(char *o) { + RRDR_OPTIONS ret = 0; + char *tok; + + while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { + if(!*tok) continue; + ret |= rrdr_options_parse_one(tok); + } + + return ret; +} + +void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options) { + buffer_json_member_add_array(wb, key); + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + buffer_json_add_array_item_string(wb, name); + } + } + + buffer_json_array_close(wb); +} + +void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options) { + RRDR_OPTIONS used = 0; // to prevent adding duplicates + size_t added = 0; + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + if(added++) buffer_strcat(wb, " "); + buffer_strcat(wb, name); + } + } +} + +void web_client_api_request_data_vX_options_to_string(char *buf, size_t size, RRDR_OPTIONS options) { + char *write = buf; + char *end = &buf[size - 1]; + + RRDR_OPTIONS used = 0; // to prevent adding duplicates + int added = 0; + for(int i = 0; rrdr_options[i].name ; i++) { + if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { + const char *name = rrdr_options[i].name; + used |= rrdr_options[i].value; + + if(added && write < end) + *write++ = ','; + + while(*name && write < end) + *write++ = *name++; + + added++; + } + } + *write = *end = '\0'; +} + +void rrdr_options_init(void) { + for(size_t i = 0; rrdr_options[i].name ; i++) + rrdr_options[i].hash = simple_hash(rrdr_options[i].name); +} diff --git a/src/web/api/maps/rrdr_options.h b/src/web/api/maps/rrdr_options.h new file mode 100644 index 00000000..4b6697db --- /dev/null +++ b/src/web/api/maps/rrdr_options.h @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_RRDR_OPTIONS_H +#define NETDATA_RRDR_OPTIONS_H + +#include "libnetdata/libnetdata.h" + +typedef enum rrdr_options { + RRDR_OPTION_NONZERO = (1 << 0), // don't output dimensions with just zero values + RRDR_OPTION_REVERSED = (1 << 1), // output the rows in reverse order (oldest to newest) + RRDR_OPTION_ABSOLUTE = (1 << 2), // values positive, for DATASOURCE_SSV before summing + RRDR_OPTION_DIMS_MIN2MAX = (1 << 3), // when adding dimensions, use max - min, instead of sum + RRDR_OPTION_DIMS_AVERAGE = (1 << 4), // when adding dimensions, use average, instead of sum + RRDR_OPTION_DIMS_MIN = (1 << 5), // when adding dimensions, use minimum, instead of sum + RRDR_OPTION_DIMS_MAX = (1 << 6), // when adding dimensions, use maximum, instead of sum + RRDR_OPTION_SECONDS = (1 << 7), // output seconds, instead of dates + RRDR_OPTION_MILLISECONDS = (1 << 8), // output milliseconds, instead of dates + RRDR_OPTION_NULL2ZERO = (1 << 9), // do not show nulls, convert them to zeros + RRDR_OPTION_OBJECTSROWS = (1 << 10), // each row of values should be an object, not an array + RRDR_OPTION_GOOGLE_JSON = (1 << 11), // comply with google JSON/JSONP specs + RRDR_OPTION_JSON_WRAP = (1 << 12), // wrap the response in a JSON header with info about the result + RRDR_OPTION_LABEL_QUOTES = (1 << 13), // in CSV output, wrap header labels in double quotes + RRDR_OPTION_PERCENTAGE = (1 << 14), // give values as percentage of total + RRDR_OPTION_NOT_ALIGNED = (1 << 15), // do not align charts for persistent timeframes + RRDR_OPTION_DISPLAY_ABS = (1 << 16), // for badges, display the absolute value, but calculate colors with sign + RRDR_OPTION_MATCH_IDS = (1 << 17), // when filtering dimensions, match only IDs + RRDR_OPTION_MATCH_NAMES = (1 << 18), // when filtering dimensions, match only names + RRDR_OPTION_NATURAL_POINTS = (1 << 19), // return the natural points of the database + RRDR_OPTION_VIRTUAL_POINTS = (1 << 20), // return virtual points + RRDR_OPTION_ANOMALY_BIT = (1 << 21), // Return the anomaly bit stored in each collected_number + RRDR_OPTION_RETURN_RAW = (1 << 22), // Return raw data for aggregating across multiple nodes + RRDR_OPTION_RETURN_JWAR = (1 << 23), // Return anomaly rates in jsonwrap + RRDR_OPTION_SELECTED_TIER = (1 << 24), // Use the selected tier for the query + RRDR_OPTION_ALL_DIMENSIONS = (1 << 25), // Return the full dimensions list + RRDR_OPTION_SHOW_DETAILS = (1 << 26), // v2 returns detailed object tree + RRDR_OPTION_DEBUG = (1 << 27), // v2 returns request description + RRDR_OPTION_MINIFY = (1 << 28), // remove JSON spaces and newlines from JSON output + RRDR_OPTION_GROUP_BY_LABELS = (1 << 29), // v2 returns flattened labels per dimension of the chart + + // internal ones - not to be exposed to the API + RRDR_OPTION_INTERNAL_AR = (1 << 31), // internal use only, to let the formatters know we want to render the anomaly rate +} RRDR_OPTIONS; + +void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options); +void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options); +void web_client_api_request_data_vX_options_to_string(char *buf, size_t size, RRDR_OPTIONS options); +void rrdr_options_init(void); + +RRDR_OPTIONS rrdr_options_parse(char *o); +RRDR_OPTIONS rrdr_options_parse_one(const char *o); + +#endif //NETDATA_RRDR_OPTIONS_H diff --git a/src/web/api/queries/average/README.md b/src/web/api/queries/average/README.md index 1ad78bee..97fb8beb 100644 --- a/src/web/api/queries/average/README.md +++ b/src/web/api/queries/average/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Average or Mean" -sidebar_label: "Average or Mean" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/average/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Average or Mean > This query is available as `average` and `mean`. diff --git a/src/web/api/queries/countif/README.md b/src/web/api/queries/countif/README.md index a4053539..1b7b682c 100644 --- a/src/web/api/queries/countif/README.md +++ b/src/web/api/queries/countif/README.md @@ -1,12 +1,3 @@ -<!-- -title: "CountIf" -sidebar_label: "CountIf" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/countif/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # CountIf > This query is available as `countif`. diff --git a/src/web/api/queries/des/README.md b/src/web/api/queries/des/README.md index 6dc19e73..fb053de5 100644 --- a/src/web/api/queries/des/README.md +++ b/src/web/api/queries/des/README.md @@ -1,12 +1,3 @@ -<!-- -title: "double exponential smoothing" -sidebar_label: "double exponential smoothing" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/des/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # double exponential smoothing Exponential smoothing is one of many window functions commonly applied to smooth data in signal diff --git a/src/web/api/queries/incremental_sum/README.md b/src/web/api/queries/incremental_sum/README.md index 6f02abe7..5e246289 100644 --- a/src/web/api/queries/incremental_sum/README.md +++ b/src/web/api/queries/incremental_sum/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Incremental Sum (`incremental_sum`)" -sidebar_label: "Incremental Sum (`incremental_sum`)" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/incremental_sum/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Incremental Sum (`incremental_sum`) This modules finds the incremental sum of a period, which `last value - first value`. diff --git a/src/web/api/queries/max/README.md b/src/web/api/queries/max/README.md index ae634e05..6a24a0a5 100644 --- a/src/web/api/queries/max/README.md +++ b/src/web/api/queries/max/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Max" -sidebar_label: "Max" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/max/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Max This module finds the max value in the time-frame given. diff --git a/src/web/api/queries/median/README.md b/src/web/api/queries/median/README.md index e6f6c04e..42a9afb1 100644 --- a/src/web/api/queries/median/README.md +++ b/src/web/api/queries/median/README.md @@ -1,13 +1,3 @@ -<!-- -title: "Median" -sidebar_label: "Median" -description: "Use median in API queries and health entities to find the 'middle' value from a sample, eliminating any unwanted spikes in the returned metrics." -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/median/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Median The median is the value separating the higher half from the lower half of a data sample diff --git a/src/web/api/queries/min/README.md b/src/web/api/queries/min/README.md index 35acb8c9..f2a35d62 100644 --- a/src/web/api/queries/min/README.md +++ b/src/web/api/queries/min/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Min" -sidebar_label: "Min" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/min/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Min This module finds the min value in the time-frame given. diff --git a/src/web/api/queries/percentile/README.md b/src/web/api/queries/percentile/README.md index 88abf8d5..0f9a2f39 100644 --- a/src/web/api/queries/percentile/README.md +++ b/src/web/api/queries/percentile/README.md @@ -1,13 +1,3 @@ -<!-- -title: "Percentile" -sidebar_label: "Percentile" -description: "Use percentile in API queries and health entities to find the 'percentile' value from a sample, eliminating any unwanted spikes in the returned metrics." -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/percentile/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Percentile The percentile is the average value of a series using only the smaller N percentile of the values. diff --git a/src/web/api/queries/rrdr.h b/src/web/api/queries/rrdr.h index d36d3f5b..860a375c 100644 --- a/src/web/api/queries/rrdr.h +++ b/src/web/api/queries/rrdr.h @@ -17,62 +17,6 @@ typedef enum tier_query_fetch { TIER_QUERY_FETCH_AVERAGE } TIER_QUERY_FETCH; -typedef enum rrdr_options { - RRDR_OPTION_NONZERO = (1 << 0), // don't output dimensions with just zero values - RRDR_OPTION_REVERSED = (1 << 1), // output the rows in reverse order (oldest to newest) - RRDR_OPTION_ABSOLUTE = (1 << 2), // values positive, for DATASOURCE_SSV before summing - RRDR_OPTION_DIMS_MIN2MAX = (1 << 3), // when adding dimensions, use max - min, instead of sum - RRDR_OPTION_DIMS_AVERAGE = (1 << 4), // when adding dimensions, use average, instead of sum - RRDR_OPTION_DIMS_MIN = (1 << 5), // when adding dimensions, use minimum, instead of sum - RRDR_OPTION_DIMS_MAX = (1 << 6), // when adding dimensions, use maximum, instead of sum - RRDR_OPTION_SECONDS = (1 << 7), // output seconds, instead of dates - RRDR_OPTION_MILLISECONDS = (1 << 8), // output milliseconds, instead of dates - RRDR_OPTION_NULL2ZERO = (1 << 9), // do not show nulls, convert them to zeros - RRDR_OPTION_OBJECTSROWS = (1 << 10), // each row of values should be an object, not an array - RRDR_OPTION_GOOGLE_JSON = (1 << 11), // comply with google JSON/JSONP specs - RRDR_OPTION_JSON_WRAP = (1 << 12), // wrap the response in a JSON header with info about the result - RRDR_OPTION_LABEL_QUOTES = (1 << 13), // in CSV output, wrap header labels in double quotes - RRDR_OPTION_PERCENTAGE = (1 << 14), // give values as percentage of total - RRDR_OPTION_NOT_ALIGNED = (1 << 15), // do not align charts for persistent timeframes - RRDR_OPTION_DISPLAY_ABS = (1 << 16), // for badges, display the absolute value, but calculate colors with sign - RRDR_OPTION_MATCH_IDS = (1 << 17), // when filtering dimensions, match only IDs - RRDR_OPTION_MATCH_NAMES = (1 << 18), // when filtering dimensions, match only names - RRDR_OPTION_NATURAL_POINTS = (1 << 19), // return the natural points of the database - RRDR_OPTION_VIRTUAL_POINTS = (1 << 20), // return virtual points - RRDR_OPTION_ANOMALY_BIT = (1 << 21), // Return the anomaly bit stored in each collected_number - RRDR_OPTION_RETURN_RAW = (1 << 22), // Return raw data for aggregating across multiple nodes - RRDR_OPTION_RETURN_JWAR = (1 << 23), // Return anomaly rates in jsonwrap - RRDR_OPTION_SELECTED_TIER = (1 << 24), // Use the selected tier for the query - RRDR_OPTION_ALL_DIMENSIONS = (1 << 25), // Return the full dimensions list - RRDR_OPTION_SHOW_DETAILS = (1 << 26), // v2 returns detailed object tree - RRDR_OPTION_DEBUG = (1 << 27), // v2 returns request description - RRDR_OPTION_MINIFY = (1 << 28), // remove JSON spaces and newlines from JSON output - RRDR_OPTION_GROUP_BY_LABELS = (1 << 29), // v2 returns flattened labels per dimension of the chart - - // internal ones - not to be exposed to the API - RRDR_OPTION_INTERNAL_AR = (1 << 31), // internal use only, to let the formatters know we want to render the anomaly rate -} RRDR_OPTIONS; - -typedef enum context_v2_options { - CONTEXT_V2_OPTION_MINIFY = (1 << 0), // remove JSON spaces and newlines from JSON output - CONTEXT_V2_OPTION_DEBUG = (1 << 1), // show the request - CONTEXT_V2_OPTION_ALERTS_WITH_CONFIGURATIONS = (1 << 2), // include alert configurations (used by /api/v2/alert_transitions) - CONTEXT_V2_OPTION_ALERTS_WITH_INSTANCES = (1 << 3), // include alert instances (used by /api/v2/alerts) - CONTEXT_V2_OPTION_ALERTS_WITH_VALUES = (1 << 4), // include alert latest values (used by /api/v2/alerts) - CONTEXT_V2_OPTION_ALERTS_WITH_SUMMARY = (1 << 5), // include alerts summary counters (used by /api/v2/alerts) -} CONTEXTS_V2_OPTIONS; - -typedef enum context_v2_alert_status { - CONTEXT_V2_ALERT_UNINITIALIZED = (1 << 5), // include UNINITIALIZED alerts - CONTEXT_V2_ALERT_UNDEFINED = (1 << 6), // include UNDEFINED alerts - CONTEXT_V2_ALERT_CLEAR = (1 << 7), // include CLEAR alerts - CONTEXT_V2_ALERT_RAISED = (1 << 8), // include WARNING & CRITICAL alerts - CONTEXT_V2_ALERT_WARNING = (1 << 9), // include WARNING alerts - CONTEXT_V2_ALERT_CRITICAL = (1 << 10), // include CRITICAL alerts -} CONTEXTS_V2_ALERT_STATUS; - -#define CONTEXTS_V2_ALERT_STATUSES (CONTEXT_V2_ALERT_UNINITIALIZED|CONTEXT_V2_ALERT_UNDEFINED|CONTEXT_V2_ALERT_CLEAR|CONTEXT_V2_ALERT_RAISED|CONTEXT_V2_ALERT_WARNING|CONTEXT_V2_ALERT_CRITICAL) - typedef enum __attribute__ ((__packed__)) rrdr_value_flag { // IMPORTANT: diff --git a/src/web/api/queries/ses/README.md b/src/web/api/queries/ses/README.md index e2fd65d7..58afdbe0 100644 --- a/src/web/api/queries/ses/README.md +++ b/src/web/api/queries/ses/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Single (or Simple) Exponential Smoothing (`ses`)" -sidebar_label: "Single (or Simple) Exponential Smoothing (`ses`)" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/ses/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Single (or Simple) Exponential Smoothing (`ses`) > This query is also available as `ema` and `ewma`. diff --git a/src/web/api/queries/stddev/README.md b/src/web/api/queries/stddev/README.md index 76cfee1f..62cda84f 100644 --- a/src/web/api/queries/stddev/README.md +++ b/src/web/api/queries/stddev/README.md @@ -1,12 +1,3 @@ -<!-- -title: "standard deviation (`stddev`)" -sidebar_label: "standard deviation (`stddev`)" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/stddev/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # standard deviation (`stddev`) The standard deviation is a measure that is used to quantify the amount of variation or dispersion diff --git a/src/web/api/queries/sum/README.md b/src/web/api/queries/sum/README.md index dd29b9c5..1bb71a62 100644 --- a/src/web/api/queries/sum/README.md +++ b/src/web/api/queries/sum/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Sum" -sidebar_label: "Sum" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/sum/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Sum This module sums all the values in the time-frame requested. diff --git a/src/web/api/queries/trimmed_mean/README.md b/src/web/api/queries/trimmed_mean/README.md index 96902329..66d86a53 100644 --- a/src/web/api/queries/trimmed_mean/README.md +++ b/src/web/api/queries/trimmed_mean/README.md @@ -1,13 +1,3 @@ -<!-- -title: "Trimmed Mean" -sidebar_label: "Trimmed Mean" -description: "Use trimmed-mean in API queries and health entities to find the average value from a sample, eliminating any unwanted spikes in the returned metrics." -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/queries/trimmed_mean/README.md -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api/Queries" ---> - # Trimmed Mean The trimmed mean is the average value of a series excluding the smallest and biggest points. diff --git a/src/web/api/queries/weights.c b/src/web/api/queries/weights.c index 44928fea..e34774f3 100644 --- a/src/web/api/queries/weights.c +++ b/src/web/api/queries/weights.c @@ -4,9 +4,7 @@ #include "database/KolmogorovSmirnovDist.h" #define MAX_POINTS 10000 -int enable_metric_correlations = CONFIG_BOOLEAN_YES; int metric_correlations_version = 1; -WEIGHTS_METHOD default_metric_correlations_method = WEIGHTS_METHOD_MC_KS2; typedef struct weights_stats { NETDATA_DOUBLE max_base_high_ratio; @@ -36,7 +34,7 @@ WEIGHTS_METHOD weights_string_to_method(const char *method) { if(strcmp(method, weights_methods[i].name) == 0) return weights_methods[i].value; - return default_metric_correlations_method; + return WEIGHTS_METHOD_MC_KS2; } const char *weights_method_to_string(WEIGHTS_METHOD method) { @@ -44,7 +42,7 @@ const char *weights_method_to_string(WEIGHTS_METHOD method) { if(weights_methods[i].value == method) return weights_methods[i].name; - return "unknown"; + return "ks2"; } // ---------------------------------------------------------------------------- @@ -978,6 +976,12 @@ static size_t registered_results_to_json_multinode_group_by( BUFFER *key = buffer_create(0, NULL); BUFFER *name = buffer_create(0, NULL); dfe_start_read(results, t) { + char node_uuid[UUID_STR_LEN]; + + if(UUIDiszero(t->host->node_id)) + uuid_unparse_lower(t->host->host_id.uuid, node_uuid); + else + uuid_unparse_lower(t->host->node_id.uuid, node_uuid); buffer_flush(key); buffer_flush(name); @@ -998,7 +1002,7 @@ static size_t registered_results_to_json_multinode_group_by( if(!(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_NODE)) { buffer_fast_strcat(key, "@", 1); buffer_fast_strcat(name, "@", 1); - buffer_strcat(key, t->host->machine_guid); + buffer_strcat(key, node_uuid); buffer_strcat(name, rrdhost_hostname(t->host)); } } @@ -1008,7 +1012,7 @@ static size_t registered_results_to_json_multinode_group_by( buffer_fast_strcat(name, ",", 1); } - buffer_strcat(key, t->host->machine_guid); + buffer_strcat(key, node_uuid); buffer_strcat(name, rrdhost_hostname(t->host)); } if(qwd->qwr->group_by.group_by & RRDR_GROUP_BY_CONTEXT) { diff --git a/src/web/api/queries/weights.h b/src/web/api/queries/weights.h index be7e5a8b..6d2bf8e0 100644 --- a/src/web/api/queries/weights.h +++ b/src/web/api/queries/weights.h @@ -18,9 +18,7 @@ typedef enum { WEIGHTS_FORMAT_MULTINODE = 3, } WEIGHTS_FORMAT; -extern int enable_metric_correlations; extern int metric_correlations_version; -extern WEIGHTS_METHOD default_metric_correlations_method; typedef bool (*weights_interrupt_callback_t)(void *data); diff --git a/src/web/api/v1/api_v1_aclk.c b/src/web/api/v1/api_v1_aclk.c new file mode 100644 index 00000000..b9878db2 --- /dev/null +++ b/src/web/api/v1/api_v1_aclk.c @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_aclk(RRDHOST *host, struct web_client *w, char *url) { + UNUSED(url); + UNUSED(host); + if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + char *str = aclk_state_json(); + buffer_strcat(wb, str); + freez(str); + + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + return HTTP_RESP_OK; +} + diff --git a/src/web/api/v1/api_v1_alarms.c b/src/web/api/v1/api_v1_alarms.c new file mode 100644 index 00000000..4f3af74b --- /dev/null +++ b/src/web/api/v1/api_v1_alarms.c @@ -0,0 +1,153 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +static int web_client_api_request_v1_alarms_select(char *url) { + int all = 0; + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + if(!strcmp(value, "all") || !strcmp(value, "all=true")) all = 1; + else if(!strcmp(value, "active") || !strcmp(value, "active=true")) all = 0; + } + + return all; +} + +int api_v1_alarms(RRDHOST *host, struct web_client *w, char *url) { + int all = web_client_api_request_v1_alarms_select(url); + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + health_alarms2json(host, w->response.data, all); + buffer_no_cacheable(w->response.data); + return HTTP_RESP_OK; +} + +int api_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url) { + int all = web_client_api_request_v1_alarms_select(url); + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + health_alarms_values2json(host, w->response.data, all); + buffer_no_cacheable(w->response.data); + return HTTP_RESP_OK; +} + +int api_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url) { + RRDCALC_STATUS status = RRDCALC_STATUS_RAISED; + BUFFER *contexts = NULL; + + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "["); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 alarm_count query param '%s' with value '%s'", w->id, name, value); + + char* p = value; + if(!strcmp(name, "status")) { + while ((*p = toupper(*p))) p++; + if (!strcmp("CRITICAL", value)) status = RRDCALC_STATUS_CRITICAL; + else if (!strcmp("WARNING", value)) status = RRDCALC_STATUS_WARNING; + else if (!strcmp("UNINITIALIZED", value)) status = RRDCALC_STATUS_UNINITIALIZED; + else if (!strcmp("UNDEFINED", value)) status = RRDCALC_STATUS_UNDEFINED; + else if (!strcmp("REMOVED", value)) status = RRDCALC_STATUS_REMOVED; + else if (!strcmp("CLEAR", value)) status = RRDCALC_STATUS_CLEAR; + } + else if(!strcmp(name, "context") || !strcmp(name, "ctx")) { + if(!contexts) contexts = buffer_create(255, &netdata_buffers_statistics.buffers_api); + buffer_strcat(contexts, "|"); + buffer_strcat(contexts, value); + } + } + + health_aggregate_alarms(host, w->response.data, contexts, status); + + buffer_sprintf(w->response.data, "]\n"); + w->response.data->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(w->response.data); + + buffer_free(contexts); + return 200; +} + +int api_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url) { + time_t after = 0; + char *chart = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + if (!strcmp(name, "after")) after = (time_t) strtoul(value, NULL, 0); + else if (!strcmp(name, "chart")) chart = value; + } + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + sql_health_alarm_log2json(host, w->response.data, after, chart); + return HTTP_RESP_OK; +} + +int api_v1_variable(RRDHOST *host, struct web_client *w, char *url) { + int ret = HTTP_RESP_BAD_REQUEST; + char *chart = NULL; + char *variable = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "chart")) chart = value; + else if(!strcmp(name, "variable")) variable = value; + } + + if(!chart || !*chart || !variable || !*variable) { + buffer_sprintf(w->response.data, "A chart= and a variable= are required."); + goto cleanup; + } + + RRDSET *st = rrdset_find(host, chart); + if(!st) st = rrdset_find_byname(host, chart); + if(!st) { + buffer_strcat(w->response.data, "Chart is not found: "); + buffer_strcat_htmlescape(w->response.data, chart); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + w->response.data->content_type = CT_APPLICATION_JSON; + st->last_accessed_time_s = now_realtime_sec(); + alert_variable_lookup_trace(host, st, variable, w->response.data); + + return HTTP_RESP_OK; + +cleanup: + return ret; +} + +int api_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url) { + return api_v1_single_chart_helper(host, w, url, health_api_v1_chart_variables2json); +} + diff --git a/src/web/api/exporters/shell/allmetrics_shell.c b/src/web/api/v1/api_v1_allmetrics.c index c8248c14..593475ef 100644 --- a/src/web/api/exporters/shell/allmetrics_shell.c +++ b/src/web/api/v1/api_v1_allmetrics.c @@ -1,6 +1,29 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "allmetrics_shell.h" +#include "api_v1_calls.h" + +#define ALLMETRICS_FORMAT_SHELL "shell" +#define ALLMETRICS_FORMAT_PROMETHEUS "prometheus" +#define ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "prometheus_all_hosts" +#define ALLMETRICS_FORMAT_JSON "json" + +#define ALLMETRICS_SHELL 1 +#define ALLMETRICS_PROMETHEUS 2 +#define ALLMETRICS_JSON 3 +#define ALLMETRICS_PROMETHEUS_ALL_HOSTS 4 + +struct prometheus_output_options { + char *name; + PROMETHEUS_OUTPUT_OPTIONS flag; +} prometheus_output_flags_root[] = { + { "names", PROMETHEUS_OUTPUT_NAMES }, + { "timestamps", PROMETHEUS_OUTPUT_TIMESTAMPS }, + { "variables", PROMETHEUS_OUTPUT_VARIABLES }, + { "oldunits", PROMETHEUS_OUTPUT_OLDUNITS }, + { "hideunits", PROMETHEUS_OUTPUT_HIDEUNITS }, + // terminator + { NULL, PROMETHEUS_OUTPUT_NONE }, +}; // ---------------------------------------------------------------------------- // BASH @@ -168,3 +191,118 @@ void rrd_stats_api_v1_charts_allmetrics_json(RRDHOST *host, const char *filter_s simple_pattern_free(filter); } +int api_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url) { + int format = ALLMETRICS_SHELL; + const char *filter = NULL; + const char *prometheus_server = w->client_ip; + + uint32_t prometheus_exporting_options; + if (prometheus_exporter_instance) + prometheus_exporting_options = prometheus_exporter_instance->config.options; + else + prometheus_exporting_options = global_exporting_options; + + PROMETHEUS_OUTPUT_OPTIONS prometheus_output_options = + PROMETHEUS_OUTPUT_TIMESTAMPS | + ((prometheus_exporting_options & EXPORTING_OPTION_SEND_NAMES) ? PROMETHEUS_OUTPUT_NAMES : 0); + + const char *prometheus_prefix; + if (prometheus_exporter_instance) + prometheus_prefix = prometheus_exporter_instance->config.prefix; + else + prometheus_prefix = global_exporting_prefix; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + if(!strcmp(name, "format")) { + if(!strcmp(value, ALLMETRICS_FORMAT_SHELL)) + format = ALLMETRICS_SHELL; + else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS)) + format = ALLMETRICS_PROMETHEUS; + else if(!strcmp(value, ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS)) + format = ALLMETRICS_PROMETHEUS_ALL_HOSTS; + else if(!strcmp(value, ALLMETRICS_FORMAT_JSON)) + format = ALLMETRICS_JSON; + else + format = 0; + } + else if(!strcmp(name, "filter")) { + filter = value; + } + else if(!strcmp(name, "server")) { + prometheus_server = value; + } + else if(!strcmp(name, "prefix")) { + prometheus_prefix = value; + } + else if(!strcmp(name, "data") || !strcmp(name, "source") || !strcmp(name, "data source") || !strcmp(name, "data-source") || !strcmp(name, "data_source") || !strcmp(name, "datasource")) { + prometheus_exporting_options = exporting_parse_data_source(value, prometheus_exporting_options); + } + else { + int i; + for(i = 0; prometheus_output_flags_root[i].name ; i++) { + if(!strcmp(name, prometheus_output_flags_root[i].name)) { + if(!strcmp(value, "yes") || !strcmp(value, "1") || !strcmp(value, "true")) + prometheus_output_options |= prometheus_output_flags_root[i].flag; + else { + prometheus_output_options &= ~prometheus_output_flags_root[i].flag; + } + + break; + } + } + } + } + + buffer_flush(w->response.data); + buffer_no_cacheable(w->response.data); + + switch(format) { + case ALLMETRICS_JSON: + w->response.data->content_type = CT_APPLICATION_JSON; + rrd_stats_api_v1_charts_allmetrics_json(host, filter, w->response.data); + return HTTP_RESP_OK; + + case ALLMETRICS_SHELL: + w->response.data->content_type = CT_TEXT_PLAIN; + rrd_stats_api_v1_charts_allmetrics_shell(host, filter, w->response.data); + return HTTP_RESP_OK; + + case ALLMETRICS_PROMETHEUS: + w->response.data->content_type = CT_PROMETHEUS; + rrd_stats_api_v1_charts_allmetrics_prometheus_single_host( + host + , filter + , w->response.data + , prometheus_server + , prometheus_prefix + , prometheus_exporting_options + , prometheus_output_options + ); + return HTTP_RESP_OK; + + case ALLMETRICS_PROMETHEUS_ALL_HOSTS: + w->response.data->content_type = CT_PROMETHEUS; + rrd_stats_api_v1_charts_allmetrics_prometheus_all_hosts( + host + , filter + , w->response.data + , prometheus_server + , prometheus_prefix + , prometheus_exporting_options + , prometheus_output_options + ); + return HTTP_RESP_OK; + + default: + w->response.data->content_type = CT_TEXT_PLAIN; + buffer_strcat(w->response.data, "Which format? '" ALLMETRICS_FORMAT_SHELL "', '" ALLMETRICS_FORMAT_PROMETHEUS "', '" ALLMETRICS_FORMAT_PROMETHEUS_ALL_HOSTS "' and '" ALLMETRICS_FORMAT_JSON "' are currently supported."); + return HTTP_RESP_BAD_REQUEST; + } +} diff --git a/src/web/api/badges/README.md b/src/web/api/v1/api_v1_badge/README.md index 1388fe0c..d6deb799 100644 --- a/src/web/api/badges/README.md +++ b/src/web/api/v1/api_v1_badge/README.md @@ -1,12 +1,3 @@ -<!-- -title: "Netdata badges" -custom_edit_url: https://github.com/netdata/netdata/edit/master/src/web/api/badges/README.md -sidebar_label: "Netdata badges" -learn_status: "Published" -learn_topic_type: "References" -learn_rel_path: "Developers/Web/Api" ---> - # Netdata badges **Badges are cool!** diff --git a/src/web/api/badges/web_buffer_svg.c b/src/web/api/v1/api_v1_badge/web_buffer_svg.c index 747c46d5..642261fd 100644 --- a/src/web/api/badges/web_buffer_svg.c +++ b/src/web/api/v1/api_v1_badge/web_buffer_svg.c @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "web_buffer_svg.h" +#include "libnetdata/libnetdata.h" +#include "../../../server/web_client.h" #define BADGE_HORIZONTAL_PADDING 4 #define VERDANA_KERNING 0.2 @@ -360,7 +361,7 @@ static struct units_formatter { { NULL, 0, UNITS_FORMAT_NONE } }; -inline char *format_value_and_unit(char *value_string, size_t value_string_len, +char *format_value_and_unit(char *value_string, size_t value_string_len, NETDATA_DOUBLE value, const char *units, int precision) { static int max = -1; int i; @@ -734,7 +735,7 @@ static const char *parse_color_argument(const char *arg, const char *def) return color_map(arg, def); } -void buffer_svg(BUFFER *wb, const char *label, +static void buffer_svg(BUFFER *wb, const char *label, NETDATA_DOUBLE value, const char *units, const char *label_color, const char *value_color, int precision, int scale, uint32_t options, int fixed_width_lbl, int fixed_width_val, const char* text_color_lbl, const char* text_color_val) { char value_color_buffer[COLOR_STRING_SIZE + 1] , value_string[VALUE_STRING_SIZE + 1] @@ -864,7 +865,7 @@ void buffer_svg(BUFFER *wb, const char *label, #define BADGE_URL_ARG_LBL_COLOR "text_color_lbl" #define BADGE_URL_ARG_VAL_COLOR "text_color_val" -int web_client_api_request_v1_badge(RRDHOST *host, struct web_client *w, char *url) { +int api_v1_badge(RRDHOST *host, struct web_client *w, char *url) { int ret = HTTP_RESP_BAD_REQUEST; buffer_flush(w->response.data); diff --git a/src/web/api/v1/api_v1_calls.h b/src/web/api/v1/api_v1_calls.h new file mode 100644 index 00000000..36a0605c --- /dev/null +++ b/src/web/api/v1/api_v1_calls.h @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_V1_CALLS_H +#define NETDATA_API_V1_CALLS_H + +#include "../web_api_v1.h" + +int api_v1_info(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_config(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_registry(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_manage(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_data(RRDHOST *host, struct web_client *w, char *url); +int api_v1_chart(RRDHOST *host, struct web_client *w, char *url); +int api_v1_charts(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_context(RRDHOST *host, struct web_client *w, char *url); +int api_v1_contexts(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_alarms(RRDHOST *host, struct web_client *w, char *url); +int api_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url); +int api_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url); +int api_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url); +int api_v1_variable(RRDHOST *host, struct web_client *w, char *url); +int api_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_dbengine_stats(RRDHOST *host, struct web_client *w, char *url); +int api_v1_ml_info(RRDHOST *host, struct web_client *w, char *url); +int api_v1_aclk(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_functions(RRDHOST *host, struct web_client *w, char *url); +int api_v1_function(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_metric_correlations(RRDHOST *host, struct web_client *w, char *url); +int api_v1_weights(RRDHOST *host, struct web_client *w, char *url); + +int api_v1_badge(RRDHOST *host, struct web_client *w, char *url); +int api_v1_allmetrics(RRDHOST *host, struct web_client *w, char *url); + +// common library calls +int api_v1_single_chart_helper(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)); +void api_v1_management_init(void); + +#endif //NETDATA_API_V1_CALLS_H diff --git a/src/web/api/v1/api_v1_charts.c b/src/web/api/v1/api_v1_charts.c new file mode 100644 index 00000000..afc67af6 --- /dev/null +++ b/src/web/api/v1/api_v1_charts.c @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_single_chart_helper(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)) { + int ret = HTTP_RESP_BAD_REQUEST; + char *chart = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "chart")) chart = value; + //else { + /// buffer_sprintf(w->response.data, "Unknown parameter '%s' in request.", name); + // goto cleanup; + //} + } + + if(!chart || !*chart) { + buffer_sprintf(w->response.data, "No chart id is given at the request."); + goto cleanup; + } + + RRDSET *st = rrdset_find(host, chart); + if(!st) st = rrdset_find_byname(host, chart); + if(!st) { + buffer_strcat(w->response.data, "Chart is not found: "); + buffer_strcat_htmlescape(w->response.data, chart); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + w->response.data->content_type = CT_APPLICATION_JSON; + st->last_accessed_time_s = now_realtime_sec(); + callback(st, w->response.data); + return HTTP_RESP_OK; + +cleanup: + return ret; +} + +int api_v1_charts(RRDHOST *host, struct web_client *w, char *url) { + (void)url; + + buffer_flush(w->response.data); + w->response.data->content_type = CT_APPLICATION_JSON; + charts2json(host, w->response.data); + return HTTP_RESP_OK; +} + +int api_v1_chart(RRDHOST *host, struct web_client *w, char *url) { + return api_v1_single_chart_helper(host, w, url, rrd_stats_api_v1_chart); +} + diff --git a/src/web/api/v1/api_v1_config.c b/src/web/api/v1/api_v1_config.c new file mode 100644 index 00000000..69bcde76 --- /dev/null +++ b/src/web/api/v1/api_v1_config.c @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web/api/v2/api_v2_calls.h" + +int api_v1_config(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { + char *action = "tree"; + char *path = "/"; + char *id = NULL; + char *add_name = NULL; + int timeout = 120; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "action")) + action = value; + else if(!strcmp(name, "path")) + path = value; + else if(!strcmp(name, "id")) + id = value; + else if(!strcmp(name, "name")) + add_name = value; + else if(!strcmp(name, "timeout")) { + timeout = (int)strtol(value, NULL, 10); + if(timeout < 10) + timeout = 10; + } + } + + char transaction[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->transaction, transaction); + + size_t len = strlen(action) + (id ? strlen(id) : 0) + strlen(path) + (add_name ? strlen(add_name) : 0) + 100; + + char cmd[len]; + if(strcmp(action, "tree") == 0) + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " tree '%s' '%s'", path, id?id:""); + else { + DYNCFG_CMDS c = dyncfg_cmds2id(action); + if(!id || !*id || !dyncfg_is_valid_id(id)) { + rrd_call_function_error(w->response.data, "Invalid id", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + + if(c == DYNCFG_CMD_NONE) { + rrd_call_function_error(w->response.data, "Invalid action", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + + if(c == DYNCFG_CMD_ADD || c == DYNCFG_CMD_USERCONFIG || c == DYNCFG_CMD_TEST) { + if(c == DYNCFG_CMD_TEST && (!add_name || !*add_name)) { + // backwards compatibility for TEST without a name + char *colon = strrchr(id, ':'); + if(colon) { + *colon = '\0'; + add_name = ++colon; + } + else + add_name = "test"; + } + + if(!add_name || !*add_name || !dyncfg_is_valid_id(add_name)) { + rrd_call_function_error(w->response.data, "Invalid name", HTTP_RESP_BAD_REQUEST); + return HTTP_RESP_BAD_REQUEST; + } + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s %s", id, dyncfg_id2cmd_one(c), add_name); + } + else + snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s", id, dyncfg_id2cmd_one(c)); + } + + CLEAN_BUFFER *source = buffer_create(100, NULL); + web_client_api_request_vX_source_to_buffer(w, source); + + buffer_flush(w->response.data); + int code = rrd_function_run(host, w->response.data, timeout, w->access, cmd, + true, transaction, + NULL, NULL, + web_client_progress_functions_update, w, + web_client_interrupt_callback, w, + w->payload, buffer_tostring(source), false); + + return code; +} diff --git a/src/web/api/v1/api_v1_context.c b/src/web/api/v1/api_v1_context.c new file mode 100644 index 00000000..5b7baf80 --- /dev/null +++ b/src/web/api/v1/api_v1_context.c @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_context(RRDHOST *host, struct web_client *w, char *url) { + char *context = NULL; + RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; + time_t after = 0, before = 0; + const char *chart_label_key = NULL, *chart_labels_filter = NULL; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "context") || !strcmp(name, "ctx")) context = value; + else if(!strcmp(name, "after")) after = str2l(value); + else if(!strcmp(name, "before")) before = str2l(value); + else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + } + + if(!context || !*context) { + buffer_sprintf(w->response.data, "No context is given at the request."); + return HTTP_RESP_BAD_REQUEST; + } + + SIMPLE_PATTERN *chart_label_key_pattern = NULL; + SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; + SIMPLE_PATTERN *chart_dimensions_pattern = NULL; + + if(chart_label_key) + chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); + + if(chart_labels_filter) + chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, + true); + + if(dimensions) { + chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", + SIMPLE_PATTERN_EXACT, true); + buffer_free(dimensions); + } + + w->response.data->content_type = CT_APPLICATION_JSON; + int ret = rrdcontext_to_json(host, w->response.data, after, before, options, context, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); + + simple_pattern_free(chart_label_key_pattern); + simple_pattern_free(chart_labels_filter_pattern); + simple_pattern_free(chart_dimensions_pattern); + + return ret; +} diff --git a/src/web/api/v1/api_v1_contexts.c b/src/web/api/v1/api_v1_contexts.c new file mode 100644 index 00000000..90d376d4 --- /dev/null +++ b/src/web/api/v1/api_v1_contexts.c @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_contexts(RRDHOST *host, struct web_client *w, char *url) { + RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; + time_t after = 0, before = 0; + const char *chart_label_key = NULL, *chart_labels_filter = NULL; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "after")) after = str2l(value); + else if(!strcmp(name, "before")) before = str2l(value); + else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + } + + SIMPLE_PATTERN *chart_label_key_pattern = NULL; + SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; + SIMPLE_PATTERN *chart_dimensions_pattern = NULL; + + if(chart_label_key) + chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); + + if(chart_labels_filter) + chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, + true); + + if(dimensions) { + chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", + SIMPLE_PATTERN_EXACT, true); + buffer_free(dimensions); + } + + w->response.data->content_type = CT_APPLICATION_JSON; + int ret = rrdcontexts_to_json(host, w->response.data, after, before, options, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); + + simple_pattern_free(chart_label_key_pattern); + simple_pattern_free(chart_labels_filter_pattern); + simple_pattern_free(chart_dimensions_pattern); + + return ret; +} diff --git a/src/web/api/v1/api_v1_data.c b/src/web/api/v1/api_v1_data.c new file mode 100644 index 00000000..30328ed3 --- /dev/null +++ b/src/web/api/v1/api_v1_data.c @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_data(RRDHOST *host, struct web_client *w, char *url) { + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data with URL '%s'", w->id, url); + + int ret = HTTP_RESP_BAD_REQUEST; + BUFFER *dimensions = NULL; + + buffer_flush(w->response.data); + + char *google_version = "0.6", + *google_reqId = "0", + *google_sig = "0", + *google_out = "json", + *responseHandler = NULL, + *outFileName = NULL; + + time_t last_timestamp_in_data = 0, google_timestamp = 0; + + char *chart = NULL; + char *before_str = NULL; + char *after_str = NULL; + char *group_time_str = NULL; + char *points_str = NULL; + char *timeout_str = NULL; + char *context = NULL; + char *chart_label_key = NULL; + char *chart_labels_filter = NULL; + char *group_options = NULL; + size_t tier = 0; + RRDR_TIME_GROUPING group = RRDR_GROUPING_AVERAGE; + DATASOURCE_FORMAT format = DATASOURCE_JSON; + RRDR_OPTIONS options = 0; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data query param '%s' with value '%s'", w->id, name, value); + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "context")) context = value; + else if(!strcmp(name, "chart_label_key")) chart_label_key = value; + else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; + else if(!strcmp(name, "chart")) chart = value; + else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { + if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); + buffer_strcat(dimensions, "|"); + buffer_strcat(dimensions, value); + } + else if(!strcmp(name, "show_dimensions")) options |= RRDR_OPTION_ALL_DIMENSIONS; + else if(!strcmp(name, "after")) after_str = value; + else if(!strcmp(name, "before")) before_str = value; + else if(!strcmp(name, "points")) points_str = value; + else if(!strcmp(name, "timeout")) timeout_str = value; + else if(!strcmp(name, "gtime")) group_time_str = value; + else if(!strcmp(name, "group_options")) group_options = value; + else if(!strcmp(name, "group")) { + group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + } + else if(!strcmp(name, "format")) { + format = datasource_format_str_to_id(value); + } + else if(!strcmp(name, "options")) { + options |= rrdr_options_parse(value); + } + else if(!strcmp(name, "callback")) { + responseHandler = value; + } + else if(!strcmp(name, "filename")) { + outFileName = value; + } + else if(!strcmp(name, "tqx")) { + // parse Google Visualization API options + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source + char *tqx_name, *tqx_value; + + while(value) { + tqx_value = strsep_skip_consecutive_separators(&value, ";"); + if(!tqx_value || !*tqx_value) continue; + + tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); + if(!tqx_name || !*tqx_name) continue; + if(!tqx_value || !*tqx_value) continue; + + if(!strcmp(tqx_name, "version")) + google_version = tqx_value; + else if(!strcmp(tqx_name, "reqId")) + google_reqId = tqx_value; + else if(!strcmp(tqx_name, "sig")) { + google_sig = tqx_value; + google_timestamp = strtoul(google_sig, NULL, 0); + } + else if(!strcmp(tqx_name, "out")) { + google_out = tqx_value; + format = google_data_format_str_to_id(google_out); + } + else if(!strcmp(tqx_name, "responseHandler")) + responseHandler = tqx_value; + else if(!strcmp(tqx_name, "outFileName")) + outFileName = tqx_value; + } + } + else if(!strcmp(name, "tier")) { + tier = str2ul(value); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + } + + // validate the google parameters given + fix_google_param(google_out); + fix_google_param(google_sig); + fix_google_param(google_reqId); + fix_google_param(google_version); + fix_google_param(responseHandler); + fix_google_param(outFileName); + + RRDSET *st = NULL; + ONEWAYALLOC *owa = onewayalloc_create(0); + QUERY_TARGET *qt = NULL; + + if(!is_valid_sp(chart) && !is_valid_sp(context)) { + buffer_sprintf(w->response.data, "No chart or context is given."); + goto cleanup; + } + + if(chart && !context) { + // check if this is a specific chart + st = rrdset_find(host, chart); + if (!st) st = rrdset_find_byname(host, chart); + } + + long long before = (before_str && *before_str)?str2l(before_str):0; + long long after = (after_str && *after_str) ?str2l(after_str):-600; + int points = (points_str && *points_str)?str2i(points_str):0; + int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; + long group_time = (group_time_str && *group_time_str)?str2l(group_time_str):0; + + QUERY_TARGET_REQUEST qtr = { + .version = 1, + .after = after, + .before = before, + .host = host, + .st = st, + .nodes = NULL, + .contexts = context, + .instances = chart, + .dimensions = (dimensions)?buffer_tostring(dimensions):NULL, + .timeout_ms = timeout, + .points = points, + .format = format, + .options = options, + .time_group_method = group, + .time_group_options = group_options, + .resampling_time = group_time, + .tier = tier, + .chart_label_key = chart_label_key, + .labels = chart_labels_filter, + .query_source = QUERY_SOURCE_API_DATA, + .priority = STORAGE_PRIORITY_NORMAL, + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + .transaction = &w->transaction, + }; + qt = query_target_create(&qtr); + + if(!qt || !qt->query.used) { + buffer_sprintf(w->response.data, "No metrics where matched to query."); + ret = HTTP_RESP_NOT_FOUND; + goto cleanup; + } + + web_client_timeout_checkpoint_set(w, timeout); + if(web_client_timeout_checkpoint_and_check(w, NULL)) { + ret = w->response.code; + goto cleanup; + } + + if(outFileName && *outFileName) { + buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); + netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); + } + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(responseHandler == NULL) + responseHandler = "google.visualization.Query.setResponse"; + + netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", + w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName + ); + + buffer_sprintf( + w->response.data, + "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", + responseHandler, + google_version, + google_reqId, + (int64_t)(st ? st->last_updated.tv_sec : 0)); + } + else if(format == DATASOURCE_JSONP) { + if(responseHandler == NULL) + responseHandler = "callback"; + + buffer_strcat(w->response.data, responseHandler); + buffer_strcat(w->response.data, "("); + } + + ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(google_timestamp < last_timestamp_in_data) + buffer_strcat(w->response.data, "});"); + + else { + // the client already has the latest data + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, + "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", + responseHandler, google_version, google_reqId); + } + } + else if(format == DATASOURCE_JSONP) + buffer_strcat(w->response.data, ");"); + + if(qt->internal.relative) + buffer_no_cacheable(w->response.data); + else + buffer_cacheable(w->response.data); + +cleanup: + query_target_release(qt); + onewayalloc_destroy(owa); + buffer_free(dimensions); + return ret; +} diff --git a/src/web/api/v1/api_v1_dbengine.c b/src/web/api/v1/api_v1_dbengine.c new file mode 100644 index 00000000..89855f88 --- /dev/null +++ b/src/web/api/v1/api_v1_dbengine.c @@ -0,0 +1,97 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +#ifndef ENABLE_DBENGINE +int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url __maybe_unused) { + return HTTP_RESP_NOT_FOUND; +} +#else +static void web_client_api_v1_dbengine_stats_for_tier(BUFFER *wb, size_t tier) { + RRDENG_SIZE_STATS stats = rrdeng_size_statistics(multidb_ctx[tier]); + + buffer_sprintf(wb, + "\n\t\t\"default_granularity_secs\":%zu" + ",\n\t\t\"sizeof_datafile\":%zu" + ",\n\t\t\"sizeof_page_in_cache\":%zu" + ",\n\t\t\"sizeof_point_data\":%zu" + ",\n\t\t\"sizeof_page_data\":%zu" + ",\n\t\t\"pages_per_extent\":%zu" + ",\n\t\t\"datafiles\":%zu" + ",\n\t\t\"extents\":%zu" + ",\n\t\t\"extents_pages\":%zu" + ",\n\t\t\"points\":%zu" + ",\n\t\t\"metrics\":%zu" + ",\n\t\t\"metrics_pages\":%zu" + ",\n\t\t\"extents_compressed_bytes\":%zu" + ",\n\t\t\"pages_uncompressed_bytes\":%zu" + ",\n\t\t\"pages_duration_secs\":%lld" + ",\n\t\t\"single_point_pages\":%zu" + ",\n\t\t\"first_t\":%ld" + ",\n\t\t\"last_t\":%ld" + ",\n\t\t\"database_retention_secs\":%lld" + ",\n\t\t\"average_compression_savings\":%0.2f" + ",\n\t\t\"average_point_duration_secs\":%0.2f" + ",\n\t\t\"average_metric_retention_secs\":%0.2f" + ",\n\t\t\"ephemeral_metrics_per_day_percent\":%0.2f" + ",\n\t\t\"average_page_size_bytes\":%0.2f" + ",\n\t\t\"estimated_concurrently_collected_metrics\":%zu" + ",\n\t\t\"currently_collected_metrics\":%zu" + ",\n\t\t\"disk_space\":%zu" + ",\n\t\t\"max_disk_space\":%zu" + , stats.default_granularity_secs + , stats.sizeof_datafile + , stats.sizeof_page_in_cache + , stats.sizeof_point_data + , stats.sizeof_page_data + , stats.pages_per_extent + , stats.datafiles + , stats.extents + , stats.extents_pages + , stats.points + , stats.metrics + , stats.metrics_pages + , stats.extents_compressed_bytes + , stats.pages_uncompressed_bytes + , (long long)stats.pages_duration_secs + , stats.single_point_pages + , stats.first_time_s + , stats.last_time_s + , (long long)stats.database_retention_secs + , stats.average_compression_savings + , stats.average_point_duration_secs + , stats.average_metric_retention_secs + , stats.ephemeral_metrics_per_day_percent + , stats.average_page_size_bytes + , stats.estimated_concurrently_collected_metrics + , stats.currently_collected_metrics + , stats.disk_space + , stats.max_disk_space + ); +} + +int api_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + + if(!dbengine_enabled) { + buffer_strcat(wb, "dbengine is not enabled"); + return HTTP_RESP_NOT_FOUND; + } + + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + buffer_strcat(wb, "{"); + for(size_t tier = 0; tier < storage_tiers ;tier++) { + buffer_sprintf(wb, "%s\n\t\"tier%zu\": {", tier?",":"", tier); + web_client_api_v1_dbengine_stats_for_tier(wb, tier); + buffer_strcat(wb, "\n\t}"); + } + buffer_strcat(wb, "\n}"); + + return HTTP_RESP_OK; +} +#endif diff --git a/src/web/api/v1/api_v1_function.c b/src/web/api/v1/api_v1_function.c new file mode 100644 index 00000000..761164fd --- /dev/null +++ b/src/web/api/v1/api_v1_function.c @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_function(RRDHOST *host, struct web_client *w, char *url) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + int timeout = 0; + const char *function = NULL; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) + continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) + continue; + + if (!strcmp(name, "function")) + function = value; + + else if (!strcmp(name, "timeout")) + timeout = (int) strtoul(value, NULL, 0); + } + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + + char transaction[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->transaction, transaction); + + CLEAN_BUFFER *source = buffer_create(100, NULL); + web_client_api_request_vX_source_to_buffer(w, source); + + return rrd_function_run(host, wb, timeout, w->access, function, true, transaction, + NULL, NULL, + web_client_progress_functions_update, w, + web_client_interrupt_callback, w, w->payload, + buffer_tostring(source), false); +} diff --git a/src/web/api/v1/api_v1_functions.c b/src/web/api/v1/api_v1_functions.c new file mode 100644 index 00000000..bc1c7df8 --- /dev/null +++ b/src/web/api/v1/api_v1_functions.c @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_functions(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + host_functions2json(host, wb); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} diff --git a/src/web/api/v1/api_v1_info.c b/src/web/api/v1/api_v1_info.c new file mode 100644 index 00000000..2395cea5 --- /dev/null +++ b/src/web/api/v1/api_v1_info.c @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +static void host_collectors(RRDHOST *host, BUFFER *wb) { + buffer_json_member_add_array(wb, "collectors"); + + DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE); + RRDSET *st; + char name[500]; + + time_t now = now_realtime_sec(); + + rrdset_foreach_read(st, host) { + if (!rrdset_is_available_for_viewers(st)) + continue; + + sprintf(name, "%s:%s", rrdset_plugin_name(st), rrdset_module_name(st)); + + bool old = 0; + bool *set = dictionary_set(dict, name, &old, sizeof(bool)); + if(!*set) { + *set = true; + st->last_accessed_time_s = now; + buffer_json_add_array_item_object(wb); + buffer_json_member_add_string(wb, "plugin", rrdset_plugin_name(st)); + buffer_json_member_add_string(wb, "module", rrdset_module_name(st)); + buffer_json_object_close(wb); + } + } + rrdset_foreach_done(st); + dictionary_destroy(dict); + + buffer_json_array_close(wb); +} + +static inline void web_client_api_request_v1_info_mirrored_hosts_status(BUFFER *wb, RRDHOST *host) { + buffer_json_add_array_item_object(wb); + + buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); + buffer_json_member_add_uint64(wb, "hops", host->system_info ? host->system_info->hops : (host == localhost) ? 0 : 1); + buffer_json_member_add_boolean(wb, "reachable", (host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))); + + buffer_json_member_add_string(wb, "guid", host->machine_guid); + buffer_json_member_add_uuid(wb, "node_id", host->node_id.uuid); + CLAIM_ID claim_id = rrdhost_claim_id_get(host); + buffer_json_member_add_string(wb, "claim_id", claim_id_is_set(claim_id) ? claim_id.str : NULL); + + buffer_json_object_close(wb); +} + +static inline void web_client_api_request_v1_info_mirrored_hosts(BUFFER *wb) { + RRDHOST *host; + + rrd_rdlock(); + + buffer_json_member_add_array(wb, "mirrored_hosts"); + rrdhost_foreach_read(host) + buffer_json_add_array_item_string(wb, rrdhost_hostname(host)); + buffer_json_array_close(wb); + + buffer_json_member_add_array(wb, "mirrored_hosts_status"); + rrdhost_foreach_read(host) { + if ((host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { + web_client_api_request_v1_info_mirrored_hosts_status(wb, host); + } + } + rrdhost_foreach_read(host) { + if ((host != localhost && rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { + web_client_api_request_v1_info_mirrored_hosts_status(wb, host); + } + } + buffer_json_array_close(wb); + + rrd_rdunlock(); +} + +static void web_client_api_request_v1_info_summary_alarm_statuses(RRDHOST *host, BUFFER *wb, const char *key) { + buffer_json_member_add_object(wb, key); + + size_t normal = 0, warning = 0, critical = 0; + RRDCALC *rc; + foreach_rrdcalc_in_rrdhost_read(host, rc) { + if(unlikely(!rc->rrdset || !rc->rrdset->last_collected_time.tv_sec)) + continue; + + switch(rc->status) { + case RRDCALC_STATUS_WARNING: + warning++; + break; + case RRDCALC_STATUS_CRITICAL: + critical++; + break; + default: + normal++; + } + } + foreach_rrdcalc_in_rrdhost_done(rc); + + buffer_json_member_add_uint64(wb, "normal", normal); + buffer_json_member_add_uint64(wb, "warning", warning); + buffer_json_member_add_uint64(wb, "critical", critical); + + buffer_json_object_close(wb); +} + +static int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb) { + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + + buffer_json_member_add_string(wb, "version", rrdhost_program_version(host)); + buffer_json_member_add_string(wb, "uid", host->machine_guid); + + buffer_json_member_add_uint64(wb, "hosts-available", rrdhost_hosts_available()); + web_client_api_request_v1_info_mirrored_hosts(wb); + + web_client_api_request_v1_info_summary_alarm_statuses(host, wb, "alarms"); + + buffer_json_member_add_string_or_empty(wb, "os_name", host->system_info->host_os_name); + buffer_json_member_add_string_or_empty(wb, "os_id", host->system_info->host_os_id); + buffer_json_member_add_string_or_empty(wb, "os_id_like", host->system_info->host_os_id_like); + buffer_json_member_add_string_or_empty(wb, "os_version", host->system_info->host_os_version); + buffer_json_member_add_string_or_empty(wb, "os_version_id", host->system_info->host_os_version_id); + buffer_json_member_add_string_or_empty(wb, "os_detection", host->system_info->host_os_detection); + buffer_json_member_add_string_or_empty(wb, "cores_total", host->system_info->host_cores); + buffer_json_member_add_string_or_empty(wb, "total_disk_space", host->system_info->host_disk_space); + buffer_json_member_add_string_or_empty(wb, "cpu_freq", host->system_info->host_cpu_freq); + buffer_json_member_add_string_or_empty(wb, "ram_total", host->system_info->host_ram_total); + + buffer_json_member_add_string_or_omit(wb, "container_os_name", host->system_info->container_os_name); + buffer_json_member_add_string_or_omit(wb, "container_os_id", host->system_info->container_os_id); + buffer_json_member_add_string_or_omit(wb, "container_os_id_like", host->system_info->container_os_id_like); + buffer_json_member_add_string_or_omit(wb, "container_os_version", host->system_info->container_os_version); + buffer_json_member_add_string_or_omit(wb, "container_os_version_id", host->system_info->container_os_version_id); + buffer_json_member_add_string_or_omit(wb, "container_os_detection", host->system_info->container_os_detection); + buffer_json_member_add_string_or_omit(wb, "is_k8s_node", host->system_info->is_k8s_node); + + buffer_json_member_add_string_or_empty(wb, "kernel_name", host->system_info->kernel_name); + buffer_json_member_add_string_or_empty(wb, "kernel_version", host->system_info->kernel_version); + buffer_json_member_add_string_or_empty(wb, "architecture", host->system_info->architecture); + buffer_json_member_add_string_or_empty(wb, "virtualization", host->system_info->virtualization); + buffer_json_member_add_string_or_empty(wb, "virt_detection", host->system_info->virt_detection); + buffer_json_member_add_string_or_empty(wb, "container", host->system_info->container); + buffer_json_member_add_string_or_empty(wb, "container_detection", host->system_info->container_detection); + + buffer_json_member_add_string_or_omit(wb, "cloud_provider_type", host->system_info->cloud_provider_type); + buffer_json_member_add_string_or_omit(wb, "cloud_instance_type", host->system_info->cloud_instance_type); + buffer_json_member_add_string_or_omit(wb, "cloud_instance_region", host->system_info->cloud_instance_region); + + host_labels2json(host, wb, "host_labels"); + host_functions2json(host, wb); + host_collectors(host, wb); + + buffer_json_member_add_boolean(wb, "cloud-enabled", true); + buffer_json_member_add_boolean(wb, "cloud-available", true); + buffer_json_member_add_boolean(wb, "agent-claimed", is_agent_claimed()); + buffer_json_member_add_boolean(wb, "aclk-available", aclk_online()); + + buffer_json_member_add_string(wb, "memory-mode", rrd_memory_mode_name(host->rrd_memory_mode)); +#ifdef ENABLE_DBENGINE + buffer_json_member_add_uint64(wb, "multidb-disk-quota", default_multidb_disk_quota_mb); + buffer_json_member_add_uint64(wb, "page-cache-size", default_rrdeng_page_cache_mb); +#endif // ENABLE_DBENGINE + buffer_json_member_add_boolean(wb, "web-enabled", web_server_mode != WEB_SERVER_MODE_NONE); + buffer_json_member_add_boolean(wb, "stream-enabled", stream_conf_send_enabled); + + buffer_json_member_add_boolean(wb, "stream-compression", + host->sender && host->sender->compressor.initialized); + + buffer_json_member_add_boolean(wb, "https-enabled", true); + + buffer_json_member_add_quoted_string(wb, "buildinfo", analytics_data.netdata_buildinfo); + buffer_json_member_add_quoted_string(wb, "release-channel", analytics_data.netdata_config_release_channel); + buffer_json_member_add_quoted_string(wb, "notification-methods", analytics_data.netdata_notification_methods); + + buffer_json_member_add_boolean(wb, "exporting-enabled", analytics_data.exporting_enabled); + buffer_json_member_add_quoted_string(wb, "exporting-connectors", analytics_data.netdata_exporting_connectors); + + buffer_json_member_add_uint64(wb, "allmetrics-prometheus-used", analytics_data.prometheus_hits); + buffer_json_member_add_uint64(wb, "allmetrics-shell-used", analytics_data.shell_hits); + buffer_json_member_add_uint64(wb, "allmetrics-json-used", analytics_data.json_hits); + buffer_json_member_add_uint64(wb, "dashboard-used", analytics_data.dashboard_hits); + + buffer_json_member_add_uint64(wb, "charts-count", analytics_data.charts_count); + buffer_json_member_add_uint64(wb, "metrics-count", analytics_data.metrics_count); + +#if defined(ENABLE_ML) + buffer_json_member_add_object(wb, "ml-info"); + ml_host_get_info(host, wb); + buffer_json_object_close(wb); +#endif + + buffer_json_finalize(wb); + return 0; +} + +int api_v1_info(RRDHOST *host, struct web_client *w, char *url) { + (void)url; + if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + web_client_api_request_v1_info_fill_buffer(host, wb); + + buffer_no_cacheable(wb); + return HTTP_RESP_OK; +} diff --git a/src/web/api/v1/api_v1_manage.c b/src/web/api/v1/api_v1_manage.c new file mode 100644 index 00000000..46611fbf --- /dev/null +++ b/src/web/api/v1/api_v1_manage.c @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +char *api_secret; + +static char *get_mgmt_api_key(void) { + char filename[FILENAME_MAX + 1]; + snprintfz(filename, FILENAME_MAX, "%s/netdata.api.key", netdata_configured_varlib_dir); + const char *api_key_filename = config_get(CONFIG_SECTION_REGISTRY, "netdata management api key file", filename); + static char guid[GUID_LEN + 1] = ""; + + if(likely(guid[0])) + return guid; + + // read it from disk + int fd = open(api_key_filename, O_RDONLY | O_CLOEXEC); + if(fd != -1) { + char buf[GUID_LEN + 1]; + if(read(fd, buf, GUID_LEN) != GUID_LEN) + netdata_log_error("Failed to read management API key from '%s'", api_key_filename); + else { + buf[GUID_LEN] = '\0'; + if(regenerate_guid(buf, guid) == -1) { + netdata_log_error("Failed to validate management API key '%s' from '%s'.", + buf, api_key_filename); + + guid[0] = '\0'; + } + } + close(fd); + } + + // generate a new one? + if(!guid[0]) { + nd_uuid_t uuid; + + uuid_generate_time(uuid); + uuid_unparse_lower(uuid, guid); + guid[GUID_LEN] = '\0'; + + // save it + fd = open(api_key_filename, O_WRONLY|O_CREAT|O_TRUNC | O_CLOEXEC, 444); + if(fd == -1) { + netdata_log_error("Cannot create unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file.", api_key_filename); + goto temp_key; + } + + if(write(fd, guid, GUID_LEN) != GUID_LEN) { + netdata_log_error("Cannot write the unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file with enough space left.", api_key_filename); + close(fd); + goto temp_key; + } + + close(fd); + } + + return guid; + +temp_key: + netdata_log_info("You can still continue to use the alarm management API using the authorization token %s during this Netdata session only.", guid); + return guid; +} + +void api_v1_management_init(void) { + api_secret = get_mgmt_api_key(); +} + +#define HLT_MGM "manage/health" +int api_v1_manage(RRDHOST *host, struct web_client *w, char *url) { + const char *haystack = buffer_tostring(w->url_path_decoded); + char *needle; + + buffer_flush(w->response.data); + + if ((needle = strstr(haystack, HLT_MGM)) == NULL) { + buffer_strcat(w->response.data, "Invalid management request. Curently only 'health' is supported."); + return HTTP_RESP_NOT_FOUND; + } + needle += strlen(HLT_MGM); + if (*needle != '\0') { + buffer_strcat(w->response.data, "Invalid management request. Currently only 'health' is supported."); + return HTTP_RESP_NOT_FOUND; + } + return web_client_api_request_v1_mgmt_health(host, w, url); +} diff --git a/src/web/api/v1/api_v1_ml_info.c b/src/web/api/v1/api_v1_ml_info.c new file mode 100644 index 00000000..121f9bf9 --- /dev/null +++ b/src/web/api/v1/api_v1_ml_info.c @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_ml_info(RRDHOST *host, struct web_client *w, char *url) { + (void) url; +#if defined(ENABLE_ML) + + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + ml_host_get_detection_info(host, wb); + buffer_json_finalize(wb); + + buffer_no_cacheable(wb); + + return HTTP_RESP_OK; +#else + UNUSED(host); + UNUSED(w); + return HTTP_RESP_SERVICE_UNAVAILABLE; +#endif // ENABLE_ML +} diff --git a/src/web/api/v1/api_v1_registry.c b/src/web/api/v1/api_v1_registry.c new file mode 100644 index 00000000..fa4ce4ca --- /dev/null +++ b/src/web/api/v1/api_v1_registry.c @@ -0,0 +1,198 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +// Pings a netdata server: +// /api/v1/registry?action=hello +// +// Access to a netdata registry: +// /api/v1/registry?action=access&machine=${machine_guid}&name=${hostname}&url=${url} +// +// Delete from a netdata registry: +// /api/v1/registry?action=delete&machine=${machine_guid}&name=${hostname}&url=${url}&delete_url=${delete_url} +// +// Search for the URLs of a machine: +// /api/v1/registry?action=search&for=${machine_guid} +// +// Impersonate: +// /api/v1/registry?action=switch&machine=${machine_guid}&name=${hostname}&url=${url}&to=${new_person_guid} +int api_v1_registry(RRDHOST *host, struct web_client *w, char *url) { + static uint32_t hash_action = 0, hash_access = 0, hash_hello = 0, hash_delete = 0, hash_search = 0, + hash_switch = 0, hash_machine = 0, hash_url = 0, hash_name = 0, hash_delete_url = 0, hash_for = 0, + hash_to = 0 /*, hash_redirects = 0 */; + + if(unlikely(!hash_action)) { + hash_action = simple_hash("action"); + hash_access = simple_hash("access"); + hash_hello = simple_hash("hello"); + hash_delete = simple_hash("delete"); + hash_search = simple_hash("search"); + hash_switch = simple_hash("switch"); + hash_machine = simple_hash("machine"); + hash_url = simple_hash("url"); + hash_name = simple_hash("name"); + hash_delete_url = simple_hash("delete_url"); + hash_for = simple_hash("for"); + hash_to = simple_hash("to"); + /* + hash_redirects = simple_hash("redirects"); +*/ + } + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry with URL '%s'", w->id, url); + + // TODO + // The browser may send multiple cookies with our id + + char person_guid[UUID_STR_LEN] = ""; + char *cookie = strstr(w->response.data->buffer, NETDATA_REGISTRY_COOKIE_NAME "="); + if(cookie) + strncpyz(person_guid, &cookie[sizeof(NETDATA_REGISTRY_COOKIE_NAME)], UUID_STR_LEN - 1); + else if(!extract_bearer_token_from_request(w, person_guid, sizeof(person_guid))) + person_guid[0] = '\0'; + + char action = '\0'; + char *machine_guid = NULL, + *machine_url = NULL, + *url_name = NULL, + *search_machine_guid = NULL, + *delete_url = NULL, + *to_person_guid = NULL; + /* + int redirects = 0; +*/ + + // Don't cache registry responses + buffer_no_cacheable(w->response.data); + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry query param '%s' with value '%s'", w->id, name, value); + + uint32_t hash = simple_hash(name); + + if(hash == hash_action && !strcmp(name, "action")) { + uint32_t vhash = simple_hash(value); + + if(vhash == hash_access && !strcmp(value, "access")) action = 'A'; + else if(vhash == hash_hello && !strcmp(value, "hello")) action = 'H'; + else if(vhash == hash_delete && !strcmp(value, "delete")) action = 'D'; + else if(vhash == hash_search && !strcmp(value, "search")) action = 'S'; + else if(vhash == hash_switch && !strcmp(value, "switch")) action = 'W'; +#ifdef NETDATA_INTERNAL_CHECKS + else netdata_log_error("unknown registry action '%s'", value); +#endif /* NETDATA_INTERNAL_CHECKS */ + } + /* + else if(hash == hash_redirects && !strcmp(name, "redirects")) + redirects = atoi(value); +*/ + else if(hash == hash_machine && !strcmp(name, "machine")) + machine_guid = value; + + else if(hash == hash_url && !strcmp(name, "url")) + machine_url = value; + + else if(action == 'A') { + if(hash == hash_name && !strcmp(name, "name")) + url_name = value; + } + else if(action == 'D') { + if(hash == hash_delete_url && !strcmp(name, "delete_url")) + delete_url = value; + } + else if(action == 'S') { + if(hash == hash_for && !strcmp(name, "for")) + search_machine_guid = value; + } + else if(action == 'W') { + if(hash == hash_to && !strcmp(name, "to")) + to_person_guid = value; + } +#ifdef NETDATA_INTERNAL_CHECKS + else netdata_log_error("unused registry URL parameter '%s' with value '%s'", name, value); +#endif /* NETDATA_INTERNAL_CHECKS */ + } + + bool do_not_track = respect_web_browser_do_not_track_policy && web_client_has_donottrack(w); + + if(unlikely(action == 'H')) { + // HELLO request, dashboard ACL + analytics_log_dashboard(); + if(unlikely(!http_can_access_dashboard(w))) + return web_client_permission_denied_acl(w); + } + else { + // everything else, registry ACL + if(unlikely(!http_can_access_registry(w))) + return web_client_permission_denied_acl(w); + + if(unlikely(do_not_track)) { + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, "Your web browser is sending 'DNT: 1' (Do Not Track). The registry requires persistent cookies on your browser to work."); + return HTTP_RESP_BAD_REQUEST; + } + } + + buffer_no_cacheable(w->response.data); + + switch(action) { + case 'A': + if(unlikely(!machine_guid || !machine_url || !url_name)) { + netdata_log_error("Invalid registry request - access requires these parameters: machine ('%s'), url ('%s'), name ('%s')", machine_guid ? machine_guid : "UNSET", machine_url ? machine_url : "UNSET", url_name ? url_name : "UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Access request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_access_json(host, w, person_guid, machine_guid, machine_url, url_name, now_realtime_sec()); + + case 'D': + if(unlikely(!machine_guid || !machine_url || !delete_url)) { + netdata_log_error("Invalid registry request - delete requires these parameters: machine ('%s'), url ('%s'), delete_url ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", delete_url?delete_url:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Delete request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_delete_json(host, w, person_guid, machine_guid, machine_url, delete_url, now_realtime_sec()); + + case 'S': + if(unlikely(!search_machine_guid)) { + netdata_log_error("Invalid registry request - search requires these parameters: for ('%s')", search_machine_guid?search_machine_guid:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Search request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_search_json(host, w, person_guid, search_machine_guid); + + case 'W': + if(unlikely(!machine_guid || !machine_url || !to_person_guid)) { + netdata_log_error("Invalid registry request - switching identity requires these parameters: machine ('%s'), url ('%s'), to ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", to_person_guid?to_person_guid:"UNSET"); + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry Switch request."); + return HTTP_RESP_BAD_REQUEST; + } + + web_client_enable_tracking_required(w); + return registry_request_switch_json(host, w, person_guid, machine_guid, machine_url, to_person_guid, now_realtime_sec()); + + case 'H': + return registry_request_hello_json(host, w, do_not_track); + + default: + buffer_flush(w->response.data); + buffer_strcat(w->response.data, "Invalid registry request - you need to set an action: hello, access, delete, search"); + return HTTP_RESP_BAD_REQUEST; + } +} diff --git a/src/web/api/v1/api_v1_weights.c b/src/web/api/v1/api_v1_weights.c new file mode 100644 index 00000000..e39fceae --- /dev/null +++ b/src/web/api/v1/api_v1_weights.c @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v1_calls.h" + +int api_v1_metric_correlations(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_MC_KS2, WEIGHTS_FORMAT_CHARTS, 1); +} + +int api_v1_weights(RRDHOST *host, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_ANOMALY_RATE, WEIGHTS_FORMAT_CONTEXTS, 1); +} diff --git a/src/web/api/v2/api_v2_alert_config.c b/src/web/api/v2/api_v2_alert_config.c new file mode 100644 index 00000000..b4f5344e --- /dev/null +++ b/src/web/api/v2/api_v2_alert_config.c @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_alert_config(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + const char *config = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "config")) + config = value; + } + + buffer_flush(w->response.data); + + if(!config) { + w->response.data->content_type = CT_TEXT_PLAIN; + buffer_strcat(w->response.data, "A config hash ID is required. Add ?config=UUID query param"); + return HTTP_RESP_BAD_REQUEST; + } + + return contexts_v2_alert_config_to_json(w, config); +} diff --git a/src/web/api/v2/api_v2_alert_transitions.c b/src/web/api/v2/api_v2_alert_transitions.c new file mode 100644 index 00000000..e84b8018 --- /dev/null +++ b/src/web/api/v2/api_v2_alert_transitions.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_alert_transitions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERT_TRANSITIONS | CONTEXTS_V2_NODES); +} diff --git a/src/web/api/v2/api_v2_alerts.c b/src/web/api/v2/api_v2_alerts.c new file mode 100644 index 00000000..c5d1922e --- /dev/null +++ b/src/web/api/v2/api_v2_alerts.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_alerts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERTS | CONTEXTS_V2_NODES); +} diff --git a/src/web/api/v2/api_v2_bearer.c b/src/web/api/v2/api_v2_bearer.c new file mode 100644 index 00000000..312ca4e4 --- /dev/null +++ b/src/web/api/v2/api_v2_bearer.c @@ -0,0 +1,139 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +static bool verify_host_uuids(RRDHOST *host, const char *machine_guid, const char *node_id) { + if(!machine_guid || !node_id) + return false; + + if(strcmp(machine_guid, host->machine_guid) != 0) + return false; + + if(UUIDiszero(host->node_id)) + return false; + + char buf[UUID_STR_LEN]; + uuid_unparse_lower(host->node_id.uuid, buf); + + return strcmp(node_id, buf) == 0; +} + +int api_v2_bearer_protection(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url) { + char *machine_guid = NULL; + char *claim_id = NULL; + char *node_id = NULL; + bool protection = netdata_is_protected_by_bearer; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + if(!strcmp(name, "bearer_protection")) { + if(!strcmp(value, "on") || !strcmp(value, "true") || !strcmp(value, "yes")) + protection = true; + else + protection = false; + } + else if(!strcmp(name, "machine_guid")) + machine_guid = value; + else if(!strcmp(name, "claim_id")) + claim_id = value; + else if(!strcmp(name, "node_id")) + node_id = value; + } + + if(!claim_id_matches(claim_id)) { + buffer_reset(w->response.data); + buffer_strcat(w->response.data, "The request is for a different claimed agent"); + return HTTP_RESP_BAD_REQUEST; + } + + if(!verify_host_uuids(localhost, machine_guid, node_id)) { + buffer_reset(w->response.data); + buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); + return HTTP_RESP_BAD_REQUEST; + } + + netdata_is_protected_by_bearer = protection; + + BUFFER *wb = w->response.data; + buffer_reset(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); + buffer_json_finalize(wb); + + return HTTP_RESP_OK; +} + +int bearer_get_token_json_response(BUFFER *wb, RRDHOST *host, const char *claim_id, const char *machine_guid, const char *node_id, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name) { + if(!claim_id_matches_any(claim_id)) + return rrd_call_function_error(wb, "The request is for a different agent", HTTP_RESP_BAD_REQUEST); + + if(!verify_host_uuids(host, machine_guid, node_id)) + return rrd_call_function_error(wb, "The request is missing or not matching local node UUIDs", HTTP_RESP_BAD_REQUEST); + + nd_uuid_t uuid; + time_t expires_s = bearer_create_token(&uuid, user_role, access, cloud_account_id, client_name); + + buffer_reset(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + buffer_json_member_add_int64(wb, "status", HTTP_RESP_OK); + buffer_json_member_add_string(wb, "mg", host->machine_guid); + buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); + buffer_json_member_add_uuid(wb, "token", uuid); + buffer_json_member_add_time_t(wb, "expiration", expires_s); + buffer_json_finalize(wb); + return HTTP_RESP_OK; +} + +int api_v2_bearer_get_token(RRDHOST *host, struct web_client *w, char *url) { + char *machine_guid = NULL; + char *claim_id = NULL; + char *node_id = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + if(!strcmp(name, "machine_guid")) + machine_guid = value; + else if(!strcmp(name, "claim_id")) + claim_id = value; + else if(!strcmp(name, "node_id")) + node_id = value; + } + + if(!claim_id_matches(claim_id)) { + buffer_reset(w->response.data); + buffer_strcat(w->response.data, "The request is for a different claimed agent"); + return HTTP_RESP_BAD_REQUEST; + } + + if(!verify_host_uuids(host, machine_guid, node_id)) { + buffer_reset(w->response.data); + buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); + return HTTP_RESP_BAD_REQUEST; + } + + if(host != localhost) + return call_function_bearer_get_token(host, w, claim_id, machine_guid, node_id); + + return bearer_get_token_json_response( + w->response.data, + host, + claim_id, + machine_guid, + node_id, + w->user_role, + w->access, + w->auth.cloud_account_id, + w->auth.client_name); +} diff --git a/src/web/api/v2/api_v2_calls.h b/src/web/api/v2/api_v2_calls.h new file mode 100644 index 00000000..809af966 --- /dev/null +++ b/src/web/api/v2/api_v2_calls.h @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_V2_CALLS_H +#define NETDATA_API_V2_CALLS_H + +#include "../web_api_v2.h" + +int api_v2_info(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_data(RRDHOST *host, struct web_client *w, char *url); +int api_v2_weights(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_alert_config(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_contexts_internal(RRDHOST *host, struct web_client *w, char *url, CONTEXTS_V2_MODE mode); +int api_v2_contexts(RRDHOST *host, struct web_client *w, char *url); +int api_v2_alert_transitions(RRDHOST *host, struct web_client *w, char *url); +int api_v2_alerts(RRDHOST *host, struct web_client *w, char *url); +int api_v2_functions(RRDHOST *host, struct web_client *w, char *url); +int api_v2_versions(RRDHOST *host, struct web_client *w, char *url); +int api_v2_q(RRDHOST *host, struct web_client *w, char *url); +int api_v2_nodes(RRDHOST *host, struct web_client *w, char *url); +int api_v2_node_instances(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_ilove(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_claim(RRDHOST *host, struct web_client *w, char *url); +int api_v3_claim(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_webrtc(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_progress(RRDHOST *host, struct web_client *w, char *url); + +int api_v2_bearer_get_token(RRDHOST *host, struct web_client *w, char *url); +int bearer_get_token_json_response(BUFFER *wb, RRDHOST *host, const char *claim_id, const char *machine_guid, const char *node_id, HTTP_USER_ROLE user_role, HTTP_ACCESS access, nd_uuid_t cloud_account_id, const char *client_name); +int api_v2_bearer_protection(RRDHOST *host, struct web_client *w, char *url); + +#endif //NETDATA_API_V2_CALLS_H diff --git a/src/web/api/v2/api_v2_claim.c b/src/web/api/v2/api_v2_claim.c new file mode 100644 index 00000000..90698610 --- /dev/null +++ b/src/web/api/v2/api_v2_claim.c @@ -0,0 +1,236 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" +#include "claim/claim.h" + +static char *netdata_random_session_id_filename = NULL; +static nd_uuid_t netdata_random_session_id = { 0 }; + +bool netdata_random_session_id_generate(void) { + static char guid[UUID_STR_LEN] = ""; + + uuid_generate_random(netdata_random_session_id); + uuid_unparse_lower(netdata_random_session_id, guid); + + char filename[FILENAME_MAX + 1]; + snprintfz(filename, FILENAME_MAX, "%s/netdata_random_session_id", netdata_configured_varlib_dir); + + bool ret = true; + + (void)unlink(filename); + + // save it + int fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC|O_CLOEXEC, 640); + if(fd == -1) { + netdata_log_error("Cannot create random session id file '%s'.", filename); + ret = false; + } + else { + if (write(fd, guid, UUID_STR_LEN - 1) != UUID_STR_LEN - 1) { + netdata_log_error("Cannot write the random session id file '%s'.", filename); + ret = false; + } else { + ssize_t bytes = write(fd, "\n", 1); + UNUSED(bytes); + } + close(fd); + } + + if(ret && (!netdata_random_session_id_filename || strcmp(netdata_random_session_id_filename, filename) != 0)) { + freez(netdata_random_session_id_filename); + netdata_random_session_id_filename = strdupz(filename); + } + + return ret; +} + +static const char *netdata_random_session_id_get_filename(void) { + if(!netdata_random_session_id_filename) + netdata_random_session_id_generate(); + + return netdata_random_session_id_filename; +} + +static bool netdata_random_session_id_matches(const char *guid) { + if(uuid_is_null(netdata_random_session_id)) + return false; + + nd_uuid_t uuid; + + if(uuid_parse(guid, uuid)) + return false; + + if(uuid_compare(netdata_random_session_id, uuid) == 0) + return true; + + return false; +} + +static bool check_claim_param(const char *s) { + if(!s || !*s) return true; + + do { + if(isalnum((uint8_t)*s) || *s == '.' || *s == ',' || *s == '-' || *s == ':' || *s == '/' || *s == '_') + ; + else + return false; + + } while(*++s); + + return true; +} + +static bool agent_can_be_claimed(void) { + CLOUD_STATUS status = cloud_status(); + switch(status) { + case CLOUD_STATUS_AVAILABLE: + case CLOUD_STATUS_OFFLINE: + case CLOUD_STATUS_INDIRECT: + return true; + + case CLOUD_STATUS_BANNED: + case CLOUD_STATUS_ONLINE: + return false; + } + + return false; +} + +typedef enum { + CLAIM_RESP_INFO, + CLAIM_RESP_ERROR, + CLAIM_RESP_ACTION_OK, + CLAIM_RESP_ACTION_FAILED, +} CLAIM_RESPONSE; + +static void claim_add_user_info_command(BUFFER *wb) { + const char *filename = netdata_random_session_id_get_filename(); + CLEAN_BUFFER *os_cmd = buffer_create(0, NULL); + + const char *os_filename; + const char *os_prefix; + const char *os_quote; + const char *os_message; + +#if defined(OS_WINDOWS) + char win_path[MAX_PATH]; + cygwin_conv_path(CCP_POSIX_TO_WIN_A, filename, win_path, sizeof(win_path)); + os_filename = win_path; + os_prefix = "more"; + os_message = "We need to verify this Windows server is yours. So, open a Command Prompt on this server to run the command. It will give you a UUID. Copy and paste this UUID to this box:"; +#else + os_filename = filename; + os_prefix = "sudo cat"; + os_message = "We need to verify this server is yours. SSH to this server and run this command. It will give you a UUID. Copy and paste this UUID to this box:"; +#endif + + // add quotes only when the filename has a space + if(strchr(os_filename, ' ')) + os_quote = "\""; + else + os_quote = ""; + + buffer_sprintf(os_cmd, "%s %s%s%s", os_prefix, os_quote, os_filename, os_quote); + + buffer_json_member_add_string(wb, "key_filename", os_filename); + buffer_json_member_add_string(wb, "cmd", buffer_tostring(os_cmd)); + buffer_json_member_add_string(wb, "help", os_message); +} + +static int claim_json_response(BUFFER *wb, CLAIM_RESPONSE response, const char *msg) { + time_t now_s = now_realtime_sec(); + buffer_reset(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); + + if(response != CLAIM_RESP_INFO) { + // this is not an info, so it needs a status report + buffer_json_member_add_boolean(wb, "success", response == CLAIM_RESP_ACTION_OK ? true : false); + buffer_json_member_add_string_or_empty(wb, "message", msg ? msg : ""); + } + + buffer_json_cloud_status(wb, now_s); + + if(response != CLAIM_RESP_ACTION_OK) { + buffer_json_member_add_boolean(wb, "can_be_claimed", agent_can_be_claimed()); + claim_add_user_info_command(wb); + } + + buffer_json_agents_v2(wb, NULL, now_s, false, false); + buffer_json_finalize(wb); + + return (response == CLAIM_RESP_ERROR) ? HTTP_RESP_BAD_REQUEST : HTTP_RESP_OK; +} + +static int claim_txt_response(BUFFER *wb, const char *msg) { + buffer_reset(wb); + buffer_strcat(wb, msg); + return HTTP_RESP_BAD_REQUEST; +} + +static int api_claim(uint8_t version, struct web_client *w, char *url) { + char *key = NULL; + char *token = NULL; + char *rooms = NULL; + char *base_url = NULL; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) continue; + if (!value || !*value) continue; + + if(!strcmp(name, "key")) + key = value; + else if(!strcmp(name, "token")) + token = value; + else if(!strcmp(name, "rooms")) + rooms = value; + else if(!strcmp(name, "url")) + base_url = value; + } + + BUFFER *wb = w->response.data; + + CLAIM_RESPONSE response = CLAIM_RESP_INFO; + const char *msg = NULL; + bool can_be_claimed = agent_can_be_claimed(); + + if(can_be_claimed && key) { + if(!netdata_random_session_id_matches(key)) { + netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it + if(version < 3) return claim_txt_response(wb, "invalid key"); + return claim_json_response(wb, CLAIM_RESP_ERROR, "invalid key"); + } + + if(!token || !base_url || !check_claim_param(token) || !check_claim_param(base_url) || (rooms && !check_claim_param(rooms))) { + netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it + if(version < 3) return claim_txt_response(wb, "invalid parameters"); + return claim_json_response(wb, CLAIM_RESP_ERROR, "invalid parameters"); + } + + netdata_random_session_id_generate(); // generate a new key, to avoid an attack to find it + + if(claim_agent(base_url, token, rooms, cloud_config_proxy_get(), cloud_config_insecure_get())) { + msg = "ok"; + can_be_claimed = false; + claim_reload_and_wait_online(); + response = CLAIM_RESP_ACTION_OK; + } + else { + msg = claim_agent_failure_reason_get(); + response = CLAIM_RESP_ACTION_FAILED; + } + } + + return claim_json_response(wb, response, msg); +} + +int api_v2_claim(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_claim(2, w, url); +} + +int api_v3_claim(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_claim(3, w, url); +} diff --git a/src/web/api/v2/api_v2_contexts.c b/src/web/api/v2/api_v2_contexts.c new file mode 100644 index 00000000..bbe36ab3 --- /dev/null +++ b/src/web/api/v2/api_v2_contexts.c @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +// -------------------------------------------------------------------------------------------------------------------- + +int api_v2_contexts_internal(RRDHOST *host __maybe_unused, struct web_client *w, char *url, CONTEXTS_V2_MODE mode) { + struct api_v2_contexts_request req = { 0 }; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "scope_nodes")) + req.scope_nodes = value; + else if(!strcmp(name, "nodes")) + req.nodes = value; + else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "scope_contexts")) + req.scope_contexts = value; + else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "contexts")) + req.contexts = value; + else if((mode & CONTEXTS_V2_SEARCH) && !strcmp(name, "q")) + req.q = value; + else if(!strcmp(name, "options")) + req.options = contexts_options_str_to_id(value); + else if(!strcmp(name, "after")) + req.after = str2l(value); + else if(!strcmp(name, "before")) + req.before = str2l(value); + else if(!strcmp(name, "timeout")) + req.timeout_ms = str2l(value); + else if(mode & (CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) { + if (!strcmp(name, "alert")) + req.alerts.alert = value; + else if (!strcmp(name, "transition")) + req.alerts.transition = value; + else if(mode & CONTEXTS_V2_ALERTS) { + if (!strcmp(name, "status")) + req.alerts.status = contexts_alert_status_str_to_id(value); + } + else if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { + if (!strcmp(name, "last")) + req.alerts.last = strtoul(value, NULL, 0); + else if(!strcmp(name, "context")) + req.contexts = value; + else if (!strcmp(name, "anchor_gi")) { + req.alerts.global_id_anchor = str2ull(value, NULL); + } + else { + for(int i = 0; i < ATF_TOTAL_ENTRIES ;i++) { + if(!strcmp(name, alert_transition_facets[i].query_param)) + req.alerts.facets[i] = value; + } + } + } + } + } + + if ((mode & CONTEXTS_V2_ALERT_TRANSITIONS) && !req.alerts.last) + req.alerts.last = 1; + + buffer_flush(w->response.data); + buffer_no_cacheable(w->response.data); + return rrdcontext_to_json_v2(w->response.data, &req, mode); +} + +int api_v2_contexts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal( + host, w, url, CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} + diff --git a/src/web/api/v2/api_v2_data.c b/src/web/api/v2/api_v2_data.c new file mode 100644 index 00000000..4eb54e9a --- /dev/null +++ b/src/web/api/v2/api_v2_data.c @@ -0,0 +1,302 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +#define GROUP_BY_KEY_MAX_LENGTH 30 +static struct { + char group_by[GROUP_BY_KEY_MAX_LENGTH + 1]; + char aggregation[GROUP_BY_KEY_MAX_LENGTH + 1]; + char group_by_label[GROUP_BY_KEY_MAX_LENGTH + 1]; +} group_by_keys[MAX_QUERY_GROUP_BY_PASSES]; + +__attribute__((constructor)) void initialize_group_by_keys(void) { + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + snprintfz(group_by_keys[g].group_by, GROUP_BY_KEY_MAX_LENGTH, "group_by[%zu]", g); + snprintfz(group_by_keys[g].aggregation, GROUP_BY_KEY_MAX_LENGTH, "aggregation[%zu]", g); + snprintfz(group_by_keys[g].group_by_label, GROUP_BY_KEY_MAX_LENGTH, "group_by_label[%zu]", g); + } +} + +int api_v2_data(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + usec_t received_ut = now_monotonic_usec(); + + int ret = HTTP_RESP_BAD_REQUEST; + + buffer_flush(w->response.data); + + char *google_version = "0.6", + *google_reqId = "0", + *google_sig = "0", + *google_out = "json", + *responseHandler = NULL, + *outFileName = NULL; + + time_t last_timestamp_in_data = 0, google_timestamp = 0; + + char *scope_nodes = NULL; + char *scope_contexts = NULL; + char *nodes = NULL; + char *contexts = NULL; + char *instances = NULL; + char *dimensions = NULL; + char *before_str = NULL; + char *after_str = NULL; + char *resampling_time_str = NULL; + char *points_str = NULL; + char *timeout_str = NULL; + char *labels = NULL; + char *alerts = NULL; + char *time_group_options = NULL; + char *tier_str = NULL; + size_t tier = 0; + RRDR_TIME_GROUPING time_group = RRDR_GROUPING_AVERAGE; + DATASOURCE_FORMAT format = DATASOURCE_JSON2; + RRDR_OPTIONS options = RRDR_OPTION_VIRTUAL_POINTS | RRDR_OPTION_JSON_WRAP | RRDR_OPTION_RETURN_JWAR; + + struct group_by_pass group_by[MAX_QUERY_GROUP_BY_PASSES] = { + { + .group_by = RRDR_GROUP_BY_DIMENSION, + .group_by_label = NULL, + .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, + }, + }; + + size_t group_by_idx = 0, group_by_label_idx = 0, aggregation_idx = 0; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "scope_nodes")) scope_nodes = value; + else if(!strcmp(name, "scope_contexts")) scope_contexts = value; + else if(!strcmp(name, "nodes")) nodes = value; + else if(!strcmp(name, "contexts")) contexts = value; + else if(!strcmp(name, "instances")) instances = value; + else if(!strcmp(name, "dimensions")) dimensions = value; + else if(!strcmp(name, "labels")) labels = value; + else if(!strcmp(name, "alerts")) alerts = value; + else if(!strcmp(name, "after")) after_str = value; + else if(!strcmp(name, "before")) before_str = value; + else if(!strcmp(name, "points")) points_str = value; + else if(!strcmp(name, "timeout")) timeout_str = value; + else if(!strcmp(name, "group_by")) { + group_by[group_by_idx++].group_by = group_by_parse(value); + if(group_by_idx >= MAX_QUERY_GROUP_BY_PASSES) + group_by_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "group_by_label")) { + group_by[group_by_label_idx++].group_by_label = value; + if(group_by_label_idx >= MAX_QUERY_GROUP_BY_PASSES) + group_by_label_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "aggregation")) { + group_by[aggregation_idx++].aggregation = group_by_aggregate_function_parse(value); + if(aggregation_idx >= MAX_QUERY_GROUP_BY_PASSES) + aggregation_idx = MAX_QUERY_GROUP_BY_PASSES - 1; + } + else if(!strcmp(name, "format")) format = datasource_format_str_to_id(value); + else if(!strcmp(name, "options")) options |= rrdr_options_parse(value); + else if(!strcmp(name, "time_group")) time_group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + else if(!strcmp(name, "time_group_options")) time_group_options = value; + else if(!strcmp(name, "time_resampling")) resampling_time_str = value; + else if(!strcmp(name, "tier")) tier_str = value; + else if(!strcmp(name, "callback")) responseHandler = value; + else if(!strcmp(name, "filename")) outFileName = value; + else if(!strcmp(name, "tqx")) { + // parse Google Visualization API options + // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source + char *tqx_name, *tqx_value; + + while(value) { + tqx_value = strsep_skip_consecutive_separators(&value, ";"); + if(!tqx_value || !*tqx_value) continue; + + tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); + if(!tqx_name || !*tqx_name) continue; + if(!tqx_value || !*tqx_value) continue; + + if(!strcmp(tqx_name, "version")) + google_version = tqx_value; + else if(!strcmp(tqx_name, "reqId")) + google_reqId = tqx_value; + else if(!strcmp(tqx_name, "sig")) { + google_sig = tqx_value; + google_timestamp = strtoul(google_sig, NULL, 0); + } + else if(!strcmp(tqx_name, "out")) { + google_out = tqx_value; + format = google_data_format_str_to_id(google_out); + } + else if(!strcmp(tqx_name, "responseHandler")) + responseHandler = tqx_value; + else if(!strcmp(tqx_name, "outFileName")) + outFileName = tqx_value; + } + } + else { + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if(!strcmp(name, group_by_keys[g].group_by)) + group_by[g].group_by = group_by_parse(value); + else if(!strcmp(name, group_by_keys[g].group_by_label)) + group_by[g].group_by_label = value; + else if(!strcmp(name, group_by_keys[g].aggregation)) + group_by[g].aggregation = group_by_aggregate_function_parse(value); + } + } + } + + // validate the google parameters given + fix_google_param(google_out); + fix_google_param(google_sig); + fix_google_param(google_reqId); + fix_google_param(google_version); + fix_google_param(responseHandler); + fix_google_param(outFileName); + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if (group_by[g].group_by_label && *group_by[g].group_by_label) + group_by[g].group_by |= RRDR_GROUP_BY_LABEL; + } + + if(group_by[0].group_by == RRDR_GROUP_BY_NONE) + group_by[0].group_by = RRDR_GROUP_BY_DIMENSION; + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { + if ((group_by[g].group_by & ~(RRDR_GROUP_BY_DIMENSION)) || (options & RRDR_OPTION_PERCENTAGE)) { + options |= RRDR_OPTION_ABSOLUTE; + break; + } + } + + if(options & RRDR_OPTION_DEBUG) + options &= ~RRDR_OPTION_MINIFY; + + if(tier_str && *tier_str) { + tier = str2ul(tier_str); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + + time_t before = (before_str && *before_str)?str2l(before_str):0; + time_t after = (after_str && *after_str) ?str2l(after_str):-600; + size_t points = (points_str && *points_str)?str2u(points_str):0; + int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; + time_t resampling_time = (resampling_time_str && *resampling_time_str) ? str2l(resampling_time_str) : 0; + + QUERY_TARGET_REQUEST qtr = { + .version = 2, + .scope_nodes = scope_nodes, + .scope_contexts = scope_contexts, + .after = after, + .before = before, + .host = NULL, + .st = NULL, + .nodes = nodes, + .contexts = contexts, + .instances = instances, + .dimensions = dimensions, + .alerts = alerts, + .timeout_ms = timeout, + .points = points, + .format = format, + .options = options, + .time_group_method = time_group, + .time_group_options = time_group_options, + .resampling_time = resampling_time, + .tier = tier, + .chart_label_key = NULL, + .labels = labels, + .query_source = QUERY_SOURCE_API_DATA, + .priority = STORAGE_PRIORITY_NORMAL, + .received_ut = received_ut, + + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + + .transaction = &w->transaction, + }; + + for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) + qtr.group_by[g] = group_by[g]; + + QUERY_TARGET *qt = query_target_create(&qtr); + ONEWAYALLOC *owa = NULL; + + if(!qt) { + buffer_sprintf(w->response.data, "Failed to prepare the query."); + ret = HTTP_RESP_INTERNAL_SERVER_ERROR; + goto cleanup; + } + + web_client_timeout_checkpoint_set(w, timeout); + if(web_client_timeout_checkpoint_and_check(w, NULL)) { + ret = w->response.code; + goto cleanup; + } + + if(outFileName && *outFileName) { + buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); + netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); + } + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(responseHandler == NULL) + responseHandler = "google.visualization.Query.setResponse"; + + netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", + w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName + ); + + buffer_sprintf( + w->response.data, + "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", + responseHandler, + google_version, + google_reqId, + (int64_t)now_realtime_sec()); + } + else if(format == DATASOURCE_JSONP) { + if(responseHandler == NULL) + responseHandler = "callback"; + + buffer_strcat(w->response.data, responseHandler); + buffer_strcat(w->response.data, "("); + } + + owa = onewayalloc_create(0); + ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); + + if(format == DATASOURCE_DATATABLE_JSONP) { + if(google_timestamp < last_timestamp_in_data) + buffer_strcat(w->response.data, "});"); + + else { + // the client already has the latest data + buffer_flush(w->response.data); + buffer_sprintf(w->response.data, + "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", + responseHandler, google_version, google_reqId); + } + } + else if(format == DATASOURCE_JSONP) + buffer_strcat(w->response.data, ");"); + + if(qt->internal.relative) + buffer_no_cacheable(w->response.data); + else + buffer_cacheable(w->response.data); + +cleanup: + query_target_release(qt); + onewayalloc_destroy(owa); + return ret; +} diff --git a/src/web/api/v2/api_v2_functions.c b/src/web/api/v2/api_v2_functions.c new file mode 100644 index 00000000..286efd13 --- /dev/null +++ b/src/web/api/v2/api_v2_functions.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_functions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal( + host, w, url, CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} diff --git a/src/web/api/ilove/README.md b/src/web/api/v2/api_v2_ilove/README.md index e69de29b..e69de29b 100644 --- a/src/web/api/ilove/README.md +++ b/src/web/api/v2/api_v2_ilove/README.md diff --git a/src/web/api/ilove/ilove.c b/src/web/api/v2/api_v2_ilove/ilove.c index 67489ec4..501e0012 100644 --- a/src/web/api/ilove/ilove.c +++ b/src/web/api/v2/api_v2_ilove/ilove.c @@ -1,6 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-or-later -#include "ilove.h" +#include "../api_v2_calls.h" static const unsigned short int ibm_plex_sans_bold_250[128][128] = { {0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* */, 0 /* ! */, 0 /* " */, 0 /* # */, 0 /* $ */, 0 /* % */, 0 /* & */, 0 /* ' */, 0 /* ( */, 0 /* ) */, 0 /* * */, 0 /* + */, 0 /* , */, 0 /* - */, 0 /* . */, 0 /* / */, 0 /* 0 */, 0 /* 1 */, 0 /* 2 */, 0 /* 3 */, 0 /* 4 */, 0 /* 5 */, 0 /* 6 */, 0 /* 7 */, 0 /* 8 */, 0 /* 9 */, 0 /* : */, 0 /* ; */, 0 /* < */, 0 /* = */, 0 /* > */, 0 /* ? */, 0 /* @ */, 0 /* A */, 0 /* B */, 0 /* C */, 0 /* D */, 0 /* E */, 0 /* F */, 0 /* G */, 0 /* H */, 0 /* I */, 0 /* J */, 0 /* K */, 0 /* L */, 0 /* M */, 0 /* N */, 0 /* O */, 0 /* P */, 0 /* Q */, 0 /* R */, 0 /* S */, 0 /* T */, 0 /* U */, 0 /* V */, 0 /* W */, 0 /* X */, 0 /* Y */, 0 /* Z */, 0 /* [ */, 0 /* \ */, 0 /* ] */, 0 /* ^ */, 0 /* _ */, 0 /* ` */, 0 /* a */, 0 /* b */, 0 /* c */, 0 /* d */, 0 /* e */, 0 /* f */, 0 /* g */, 0 /* h */, 0 /* i */, 0 /* j */, 0 /* k */, 0 /* l */, 0 /* m */, 0 /* n */, 0 /* o */, 0 /* p */, 0 /* q */, 0 /* r */, 0 /* s */, 0 /* t */, 0 /* u */, 0 /* v */, 0 /* w */, 0 /* x */, 0 /* y */, 0 /* z */, 0 /* { */, 0 /* | */, 0 /* } */, 0 /* ~ */}, @@ -277,7 +277,7 @@ static void generate_ilove_svg(BUFFER *wb, const char *love) { wb->content_type = CT_IMAGE_SVG_XML; } -int web_client_api_request_v2_ilove(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { +int api_v2_ilove(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { char *love = "TROUBLE"; while(url) { diff --git a/src/web/api/ilove/measure-text.js b/src/web/api/v2/api_v2_ilove/measure-text.js index e2a2a6e9..e2a2a6e9 100644 --- a/src/web/api/ilove/measure-text.js +++ b/src/web/api/v2/api_v2_ilove/measure-text.js diff --git a/src/web/api/v2/api_v2_info.c b/src/web/api/v2/api_v2_info.c new file mode 100644 index 00000000..fd2aba63 --- /dev/null +++ b/src/web/api/v2/api_v2_info.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_info(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal(host, w, url, CONTEXTS_V2_AGENTS | CONTEXTS_V2_AGENTS_INFO); +} diff --git a/src/web/api/v2/api_v2_node_instances.c b/src/web/api/v2/api_v2_node_instances.c new file mode 100644 index 00000000..03719143 --- /dev/null +++ b/src/web/api/v2/api_v2_node_instances.c @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_node_instances(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal( + host, w, url, + CONTEXTS_V2_NODES | CONTEXTS_V2_NODE_INSTANCES | CONTEXTS_V2_AGENTS | + CONTEXTS_V2_AGENTS_INFO | CONTEXTS_V2_VERSIONS); +} diff --git a/src/web/api/v2/api_v2_nodes.c b/src/web/api/v2/api_v2_nodes.c new file mode 100644 index 00000000..3880f279 --- /dev/null +++ b/src/web/api/v2/api_v2_nodes.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_nodes(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal(host, w, url, CONTEXTS_V2_NODES | CONTEXTS_V2_NODES_INFO); +} diff --git a/src/web/api/v2/api_v2_progress.c b/src/web/api/v2/api_v2_progress.c new file mode 100644 index 00000000..ebb53ca8 --- /dev/null +++ b/src/web/api/v2/api_v2_progress.c @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_progress(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + char *transaction = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "transaction")) transaction = value; + } + + nd_uuid_t tr; + uuid_parse_flexi(transaction, tr); + + rrd_function_call_progresser(&tr); + + return web_api_v2_report_progress(&tr, w->response.data); +} diff --git a/src/web/api/v2/api_v2_q.c b/src/web/api/v2/api_v2_q.c new file mode 100644 index 00000000..57fcec7d --- /dev/null +++ b/src/web/api/v2/api_v2_q.c @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_q(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal( + host, w, url, + CONTEXTS_V2_SEARCH | CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); +} diff --git a/src/web/api/v2/api_v2_versions.c b/src/web/api/v2/api_v2_versions.c new file mode 100644 index 00000000..299e7a30 --- /dev/null +++ b/src/web/api/v2/api_v2_versions.c @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int api_v2_versions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return api_v2_contexts_internal(host, w, url, CONTEXTS_V2_VERSIONS); +} diff --git a/src/web/api/v2/api_v2_webrtc.c b/src/web/api/v2/api_v2_webrtc.c new file mode 100644 index 00000000..dcd383d4 --- /dev/null +++ b/src/web/api/v2/api_v2_webrtc.c @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" +#include "../../rtc/webrtc.h" + +int api_v2_webrtc(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { + return webrtc_new_connection(buffer_tostring(w->payload), w->response.data); +} diff --git a/src/web/api/v2/api_v2_weights.c b/src/web/api/v2/api_v2_weights.c new file mode 100644 index 00000000..442c8b75 --- /dev/null +++ b/src/web/api/v2/api_v2_weights.c @@ -0,0 +1,152 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v2_calls.h" + +int web_client_api_request_weights(RRDHOST *host, struct web_client *w, char *url, WEIGHTS_METHOD method, WEIGHTS_FORMAT format, size_t api_version) { + if (!netdata_ready) + return HTTP_RESP_SERVICE_UNAVAILABLE; + + time_t baseline_after = 0, baseline_before = 0, after = 0, before = 0; + size_t points = 0; + RRDR_OPTIONS options = 0; + RRDR_TIME_GROUPING time_group_method = RRDR_GROUPING_AVERAGE; + time_t timeout_ms = 0; + size_t tier = 0; + const char *time_group_options = NULL, *scope_contexts = NULL, *scope_nodes = NULL, *contexts = NULL, *nodes = NULL, + *instances = NULL, *dimensions = NULL, *labels = NULL, *alerts = NULL; + + struct group_by_pass group_by = { + .group_by = RRDR_GROUP_BY_NONE, + .group_by_label = NULL, + .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, + }; + + while (url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if (!value || !*value) + continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if (!name || !*name) + continue; + if (!value || !*value) + continue; + + if (!strcmp(name, "baseline_after")) + baseline_after = str2l(value); + + else if (!strcmp(name, "baseline_before")) + baseline_before = str2l(value); + + else if (!strcmp(name, "after") || !strcmp(name, "highlight_after")) + after = str2l(value); + + else if (!strcmp(name, "before") || !strcmp(name, "highlight_before")) + before = str2l(value); + + else if (!strcmp(name, "points") || !strcmp(name, "max_points")) + points = str2ul(value); + + else if (!strcmp(name, "timeout")) + timeout_ms = str2l(value); + + else if((api_version == 1 && !strcmp(name, "group")) || (api_version >= 2 && !strcmp(name, "time_group"))) + time_group_method = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); + + else if((api_version == 1 && !strcmp(name, "group_options")) || (api_version >= 2 && !strcmp(name, "time_group_options"))) + time_group_options = value; + + else if(!strcmp(name, "options")) + options |= rrdr_options_parse(value); + + else if(!strcmp(name, "method")) + method = weights_string_to_method(value); + + else if(api_version == 1 && (!strcmp(name, "context") || !strcmp(name, "contexts"))) + scope_contexts = value; + + else if(api_version >= 2 && !strcmp(name, "scope_nodes")) scope_nodes = value; + else if(api_version >= 2 && !strcmp(name, "scope_contexts")) scope_contexts = value; + else if(api_version >= 2 && !strcmp(name, "nodes")) nodes = value; + else if(api_version >= 2 && !strcmp(name, "contexts")) contexts = value; + else if(api_version >= 2 && !strcmp(name, "instances")) instances = value; + else if(api_version >= 2 && !strcmp(name, "dimensions")) dimensions = value; + else if(api_version >= 2 && !strcmp(name, "labels")) labels = value; + else if(api_version >= 2 && !strcmp(name, "alerts")) alerts = value; + else if(api_version >= 2 && (!strcmp(name, "group_by") || !strcmp(name, "group_by[0]"))) { + group_by.group_by = group_by_parse(value); + } + else if(api_version >= 2 && (!strcmp(name, "group_by_label") || !strcmp(name, "group_by_label[0]"))) { + group_by.group_by_label = value; + } + else if(api_version >= 2 && (!strcmp(name, "aggregation") || !strcmp(name, "aggregation[0]"))) { + group_by.aggregation = group_by_aggregate_function_parse(value); + } + + else if(!strcmp(name, "tier")) { + tier = str2ul(value); + if(tier < storage_tiers) + options |= RRDR_OPTION_SELECTED_TIER; + else + tier = 0; + } + } + + if(options == 0) + // the user did not set any options + options = RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO | RRDR_OPTION_NONZERO; + else + // the user set some options, add also these + options |= RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO; + + if(options & RRDR_OPTION_PERCENTAGE) + options |= RRDR_OPTION_ABSOLUTE; + + if(options & RRDR_OPTION_DEBUG) + options &= ~RRDR_OPTION_MINIFY; + + BUFFER *wb = w->response.data; + buffer_flush(wb); + wb->content_type = CT_APPLICATION_JSON; + + QUERY_WEIGHTS_REQUEST qwr = { + .version = api_version, + .host = (api_version == 1) ? NULL : host, + .scope_nodes = scope_nodes, + .scope_contexts = scope_contexts, + .nodes = nodes, + .contexts = contexts, + .instances = instances, + .dimensions = dimensions, + .labels = labels, + .alerts = alerts, + .group_by = { + .group_by = group_by.group_by, + .group_by_label = group_by.group_by_label, + .aggregation = group_by.aggregation, + }, + .method = method, + .format = format, + .time_group_method = time_group_method, + .time_group_options = time_group_options, + .baseline_after = baseline_after, + .baseline_before = baseline_before, + .after = after, + .before = before, + .points = points, + .options = options, + .tier = tier, + .timeout_ms = timeout_ms, + + .interrupt_callback = web_client_interrupt_callback, + .interrupt_callback_data = w, + + .transaction = &w->transaction, + }; + + return web_api_v12_weights(wb, &qwr); +} + +int api_v2_weights(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { + return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_VALUE, WEIGHTS_FORMAT_MULTINODE, 2); +} diff --git a/src/web/api/v3/api_v3_calls.h b/src/web/api/v3/api_v3_calls.h new file mode 100644 index 00000000..4cee766f --- /dev/null +++ b/src/web/api/v3/api_v3_calls.h @@ -0,0 +1,11 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_API_V3_CALLS_H +#define NETDATA_API_V3_CALLS_H + +#include "../web_api_v3.h" + +int api_v3_settings(RRDHOST *host, struct web_client *w, char *url); +int api_v3_me(RRDHOST *host, struct web_client *w, char *url); + +#endif //NETDATA_API_V3_CALLS_H diff --git a/src/web/api/v3/api_v3_me.c b/src/web/api/v3/api_v3_me.c new file mode 100644 index 00000000..39ba2c29 --- /dev/null +++ b/src/web/api/v3/api_v3_me.c @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "api_v3_calls.h" + +int api_v3_me(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { + BUFFER *wb = w->response.data; + buffer_reset(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + + const char *auth; + switch(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_CLOUD|WEB_CLIENT_FLAG_AUTH_BEARER|WEB_CLIENT_FLAG_AUTH_GOD)) { + case WEB_CLIENT_FLAG_AUTH_CLOUD: + auth = "cloud"; + break; + + case WEB_CLIENT_FLAG_AUTH_BEARER: + auth = "bearer"; + break; + + case WEB_CLIENT_FLAG_AUTH_GOD: + auth = "god"; + break; + + default: + auth = "none"; + break; + } + buffer_json_member_add_string(wb, "auth", auth); + + buffer_json_member_add_uuid(wb, "cloud_account_id", w->auth.cloud_account_id); + buffer_json_member_add_string(wb, "client_name", w->auth.client_name); + http_access2buffer_json_array(wb, "access", w->access); + buffer_json_member_add_string(wb, "user_role", http_id2user_role(w->user_role)); + + buffer_json_finalize(wb); + return HTTP_RESP_OK; +} diff --git a/src/web/api/v3/api_v3_settings.c b/src/web/api/v3/api_v3_settings.c new file mode 100644 index 00000000..3b02e6b6 --- /dev/null +++ b/src/web/api/v3/api_v3_settings.c @@ -0,0 +1,285 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +/* + * /api/v3/settings + * + * QUERY STRING PARAMETERS: + * - file=a file name (alphanumerics, dashes, underscores) + * When the user is not authenticated with a bearer token + * only the 'default' file is allowed. + * Authenticated users can create, store and update any + * settings file. + * + * HTTP METHODS + * - GET to retrieve a file + * - PUT to create or update a file + * + * PAYLOAD + * - The payload MUST have the member 'version'. + * - The payload MAY have anything else. + * - The maximum payload size in JSON is 20MiB. + * - When updating the payload, the caller must specify the + * version of the existing file. If this check fails, + * Netdata will return 409 (conflict). + * When the caller receives 409, it means there are updates + * in the payload outside its control and the object MUST + * be loaded again to find its current version to update it. + * After loading it, the caller must reapply the changes and + * PUT it again. + * - Netdata will increase the version on every PUT action. + * So, the payload MUST specify the version found on disk + * but, Netdata will increment the version before saving it. + */ + +#include "api_v3_calls.h" + +#define MAX_SETTINGS_SIZE_BYTES (20 * 1024 * 1024) + +// we need an r/w spinlock to ensure that reads and write do not happen +// concurrently for settings files +static RW_SPINLOCK settings_spinlock = NETDATA_RW_SPINLOCK_INITIALIZER; + +static inline void settings_path(char out[FILENAME_MAX]) { + filename_from_path_entry(out, netdata_configured_varlib_dir, "settings", NULL); +} + +static inline void settings_filename(char out[FILENAME_MAX], const char *file, const char *extension) { + char path[FILENAME_MAX]; + settings_path(path); + filename_from_path_entry(out, path, file, extension); +} + +static inline bool settings_ensure_path_exists(void) { + char path[FILENAME_MAX]; + settings_path(path); + return filename_is_dir(path, true); +} + +static inline size_t settings_extract_json_version(const char *json) { + if(!json || !*json) return 0; + + // Parse the JSON string into a JSON-C object + CLEAN_JSON_OBJECT *jobj = json_tokener_parse(json); + if (jobj == NULL) + return 0; + + // Access the "version" field + struct json_object *version_obj; + if (json_object_object_get_ex(jobj, "version", &version_obj)) + // Extract the integer value of the version + return (size_t)json_object_get_int(version_obj); + + return 0; +} + +static inline void settings_initial_version(BUFFER *wb) { + buffer_reset(wb); + buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_MINIFY); + buffer_json_member_add_uint64(wb, "version", 1); + buffer_json_finalize(wb); +} + +static inline void settings_get(BUFFER *wb, const char *file, bool have_lock) { + char filename[FILENAME_MAX]; + settings_filename(filename, file, NULL); + + buffer_reset(wb); + + if(!have_lock) + rw_spinlock_read_lock(&settings_spinlock); + + bool rc = read_txt_file_to_buffer(filename, wb, MAX_SETTINGS_SIZE_BYTES); + + if(!have_lock) + rw_spinlock_read_unlock(&settings_spinlock); + + if(rc) { + size_t version = settings_extract_json_version(buffer_tostring(wb)); + if (!version) { + nd_log(NDLS_DAEMON, NDLP_ERR, "file '%s' cannot be parsed to extract version", filename); + settings_initial_version(wb); + } + else { + wb->content_type = CT_APPLICATION_JSON; + buffer_no_cacheable(wb); + } + } + else + settings_initial_version(wb); +} + +static inline size_t settings_get_version(const char *path, bool have_lock) { + CLEAN_BUFFER *wb = buffer_create(0, NULL); + settings_get(wb, path, have_lock); + + return settings_extract_json_version(buffer_tostring(wb)); +} + +static inline int settings_put(struct web_client *w, char *file) { + rw_spinlock_write_lock(&settings_spinlock); + + if(!settings_ensure_path_exists()) { + rw_spinlock_write_unlock(&settings_spinlock); + return rrd_call_function_error( + w->response.data, + "Settings path cannot be created or accessed.", + HTTP_RESP_BAD_REQUEST); + } + + size_t old_version = settings_get_version(file, true); + + // Parse the JSON string into a JSON-C object + CLEAN_JSON_OBJECT *jobj = json_tokener_parse(buffer_tostring(w->payload)); + if (jobj == NULL) { + rw_spinlock_write_unlock(&settings_spinlock); + return rrd_call_function_error( + w->response.data, + "Payload cannot be parsed as a JSON object", + HTTP_RESP_BAD_REQUEST); + } + + // Access the "version" field + struct json_object *version_obj; + if (!json_object_object_get_ex(jobj, "version", &version_obj)) { + rw_spinlock_write_unlock(&settings_spinlock); + return rrd_call_function_error( + w->response.data, + "Field version is not found in payload", + HTTP_RESP_BAD_REQUEST); + } + + size_t new_version = (size_t)json_object_get_int(version_obj); + + if (old_version != new_version) { + rw_spinlock_write_unlock(&settings_spinlock); + return rrd_call_function_error( + w->response.data, + "Payload version does not match the version of the stored object", + HTTP_RESP_CONFLICT); + } + + new_version++; + // Set the new version back into the JSON object + json_object_object_add(jobj, "version", json_object_new_int((int)new_version)); + + // Convert the updated JSON object back to a string + const char *updated_json_str = json_object_to_json_string(jobj); + + char tmp_filename[FILENAME_MAX]; + settings_filename(tmp_filename, file, "new"); + + // Save the updated JSON string to a file + FILE *fp = fopen(tmp_filename, "w"); + if (fp == NULL) { + rw_spinlock_write_unlock(&settings_spinlock); + nd_log(NDLS_DAEMON, NDLP_ERR, "cannot open/create settings file '%s'", tmp_filename); + return rrd_call_function_error( + w->response.data, + "Cannot create payload file '%s'", + HTTP_RESP_INTERNAL_SERVER_ERROR); + } + size_t len = strlen(updated_json_str); + if(fwrite(updated_json_str, 1, len, fp) != len) { + fclose(fp); + unlink(tmp_filename); + rw_spinlock_write_unlock(&settings_spinlock); + nd_log(NDLS_DAEMON, NDLP_ERR, "cannot save settings to file '%s'", tmp_filename); + return rrd_call_function_error( + w->response.data, + "Cannot save payload to file '%s'", + HTTP_RESP_INTERNAL_SERVER_ERROR); + } + fclose(fp); + + char filename[FILENAME_MAX]; + settings_filename(filename, file, NULL); + + bool renamed = rename(tmp_filename, filename) == 0; + + rw_spinlock_write_unlock(&settings_spinlock); + + if(!renamed) { + nd_log(NDLS_DAEMON, NDLP_ERR, "cannot rename file '%s' to '%s'", tmp_filename, filename); + return rrd_call_function_error( + w->response.data, + "Failed to move the payload file to its final location", + HTTP_RESP_INTERNAL_SERVER_ERROR); + } + + return rrd_call_function_error( + w->response.data, + "OK", + HTTP_RESP_OK); +} + +static inline bool is_settings_file_valid(char *file) { + char *s = file; + + if(!s || !*s) + return false; + + while(*s) { + if(!isalnum((uint8_t)*s) && *s != '-' && *s != '_') + return false; + s++; + } + + return true; +} + +int api_v3_settings(RRDHOST *host, struct web_client *w, char *url) { + char *file = NULL; + + while(url) { + char *value = strsep_skip_consecutive_separators(&url, "&"); + if(!value || !*value) continue; + + char *name = strsep_skip_consecutive_separators(&value, "="); + if(!name || !*name) continue; + if(!value || !*value) continue; + + // name and value are now the parameters + // they are not null and not empty + + if(!strcmp(name, "file")) + file = value; + } + + if(!is_settings_file_valid(file)) + return rrd_call_function_error( + w->response.data, + "Invalid settings file given.", + HTTP_RESP_BAD_REQUEST); + + if(host != localhost) + return rrd_call_function_error( + w->response.data, + "Settings API is only allowed for the agent node.", + HTTP_RESP_BAD_REQUEST); + + if(web_client_flags_check_auth(w) != WEB_CLIENT_FLAG_AUTH_BEARER && strcmp(file, "default") != 0) + return rrd_call_function_error( + w->response.data, + "Only the 'default' settings file is allowed for anonymous users", + HTTP_RESP_BAD_REQUEST); + + switch(w->mode) { + case HTTP_REQUEST_MODE_GET: + settings_get(w->response.data, file, false); + return HTTP_RESP_OK; + + case HTTP_REQUEST_MODE_PUT: + if(!w->payload || !buffer_strlen(w->payload)) + return rrd_call_function_error( + w->response.data, + "Settings API PUT action requires a payload.", + HTTP_RESP_BAD_REQUEST); + + return settings_put(w, file); + + default: + return rrd_call_function_error(w->response.data, + "Invalid HTTP mode. HTTP modes GET and PUT are supported.", + HTTP_RESP_BAD_REQUEST); + } +} diff --git a/src/web/api/web_api.c b/src/web/api/web_api.c index 4e936be5..88be32a4 100644 --- a/src/web/api/web_api.c +++ b/src/web/api/web_api.c @@ -2,6 +2,12 @@ #include "web_api.h" +void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key) { + buffer_json_member_add_object(wb, key); + rrdlabels_to_buffer_json_members(host->rrdlabels, wb); + buffer_json_object_close(wb); +} + int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_path_endpoint, struct web_api_command *api_commands) { buffer_no_cacheable(w->response.data); @@ -11,15 +17,16 @@ int web_client_api_request_vX(RRDHOST *host, struct web_client *w, char *url_pat internal_fatal(!web_client_flags_check_auth(w) && (w->access & HTTP_ACCESS_SIGNED_ID), "signed-in permission is set, but it shouldn't"); +#ifdef NETDATA_GOD_MODE + web_client_set_permissions(w, HTTP_ACCESS_ALL, HTTP_USER_ROLE_ADMIN, WEB_CLIENT_FLAG_AUTH_GOD); +#else if(!web_client_flags_check_auth(w)) { - w->user_role = (netdata_is_protected_by_bearer) ? HTTP_USER_ROLE_NONE : HTTP_USER_ROLE_ANY; - w->access = (netdata_is_protected_by_bearer) ? HTTP_ACCESS_NONE : HTTP_ACCESS_ANONYMOUS_DATA; + web_client_set_permissions( + w, + (netdata_is_protected_by_bearer) ? HTTP_ACCESS_NONE : HTTP_ACCESS_ANONYMOUS_DATA, + (netdata_is_protected_by_bearer) ? HTTP_USER_ROLE_NONE : HTTP_USER_ROLE_ANY, + 0); } - -#ifdef NETDATA_GOD_MODE - web_client_flag_set(w, WEB_CLIENT_FLAG_AUTH_GOD); - w->user_role = HTTP_USER_ROLE_ADMIN; - w->access = HTTP_ACCESS_ALL; #endif if(unlikely(!url_path_endpoint || !*url_path_endpoint)) { @@ -110,156 +117,66 @@ RRDCONTEXT_TO_JSON_OPTIONS rrdcontext_to_json_parse_options(char *o) { return options; } -int web_client_api_request_weights(RRDHOST *host, struct web_client *w, char *url, WEIGHTS_METHOD method, WEIGHTS_FORMAT format, size_t api_version) { - if (!netdata_ready) - return HTTP_RESP_SERVICE_UNAVAILABLE; - - time_t baseline_after = 0, baseline_before = 0, after = 0, before = 0; - size_t points = 0; - RRDR_OPTIONS options = 0; - RRDR_TIME_GROUPING time_group_method = RRDR_GROUPING_AVERAGE; - time_t timeout_ms = 0; - size_t tier = 0; - const char *time_group_options = NULL, *scope_contexts = NULL, *scope_nodes = NULL, *contexts = NULL, *nodes = NULL, - *instances = NULL, *dimensions = NULL, *labels = NULL, *alerts = NULL; - - struct group_by_pass group_by = { - .group_by = RRDR_GROUP_BY_NONE, - .group_by_label = NULL, - .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, - }; - - while (url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) - continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) - continue; - if (!value || !*value) - continue; - - if (!strcmp(name, "baseline_after")) - baseline_after = str2l(value); - - else if (!strcmp(name, "baseline_before")) - baseline_before = str2l(value); - - else if (!strcmp(name, "after") || !strcmp(name, "highlight_after")) - after = str2l(value); - - else if (!strcmp(name, "before") || !strcmp(name, "highlight_before")) - before = str2l(value); - - else if (!strcmp(name, "points") || !strcmp(name, "max_points")) - points = str2ul(value); - - else if (!strcmp(name, "timeout")) - timeout_ms = str2l(value); - - else if((api_version == 1 && !strcmp(name, "group")) || (api_version >= 2 && !strcmp(name, "time_group"))) - time_group_method = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); - - else if((api_version == 1 && !strcmp(name, "group_options")) || (api_version >= 2 && !strcmp(name, "time_group_options"))) - time_group_options = value; - - else if(!strcmp(name, "options")) - options |= rrdr_options_parse(value); - - else if(!strcmp(name, "method")) - method = weights_string_to_method(value); - - else if(api_version == 1 && (!strcmp(name, "context") || !strcmp(name, "contexts"))) - scope_contexts = value; - - else if(api_version >= 2 && !strcmp(name, "scope_nodes")) scope_nodes = value; - else if(api_version >= 2 && !strcmp(name, "scope_contexts")) scope_contexts = value; - else if(api_version >= 2 && !strcmp(name, "nodes")) nodes = value; - else if(api_version >= 2 && !strcmp(name, "contexts")) contexts = value; - else if(api_version >= 2 && !strcmp(name, "instances")) instances = value; - else if(api_version >= 2 && !strcmp(name, "dimensions")) dimensions = value; - else if(api_version >= 2 && !strcmp(name, "labels")) labels = value; - else if(api_version >= 2 && !strcmp(name, "alerts")) alerts = value; - else if(api_version >= 2 && (!strcmp(name, "group_by") || !strcmp(name, "group_by[0]"))) { - group_by.group_by = group_by_parse(value); - } - else if(api_version >= 2 && (!strcmp(name, "group_by_label") || !strcmp(name, "group_by_label[0]"))) { - group_by.group_by_label = value; - } - else if(api_version >= 2 && (!strcmp(name, "aggregation") || !strcmp(name, "aggregation[0]"))) { - group_by.aggregation = group_by_aggregate_function_parse(value); - } - else if(!strcmp(name, "tier")) { - tier = str2ul(value); - if(tier < storage_tiers) - options |= RRDR_OPTION_SELECTED_TIER; - else - tier = 0; - } - } +bool web_client_interrupt_callback(void *data) { + struct web_client *w = data; - if(options == 0) - // the user did not set any options - options = RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO | RRDR_OPTION_NONZERO; + bool ret; + if(w->interrupt.callback) + ret = w->interrupt.callback(w, w->interrupt.callback_data); else - // the user set some options, add also these - options |= RRDR_OPTION_NOT_ALIGNED | RRDR_OPTION_NULL2ZERO; - - if(options & RRDR_OPTION_PERCENTAGE) - options |= RRDR_OPTION_ABSOLUTE; - - if(options & RRDR_OPTION_DEBUG) - options &= ~RRDR_OPTION_MINIFY; - - BUFFER *wb = w->response.data; - buffer_flush(wb); - wb->content_type = CT_APPLICATION_JSON; - - QUERY_WEIGHTS_REQUEST qwr = { - .version = api_version, - .host = (api_version == 1) ? NULL : host, - .scope_nodes = scope_nodes, - .scope_contexts = scope_contexts, - .nodes = nodes, - .contexts = contexts, - .instances = instances, - .dimensions = dimensions, - .labels = labels, - .alerts = alerts, - .group_by = { - .group_by = group_by.group_by, - .group_by_label = group_by.group_by_label, - .aggregation = group_by.aggregation, - }, - .method = method, - .format = format, - .time_group_method = time_group_method, - .time_group_options = time_group_options, - .baseline_after = baseline_after, - .baseline_before = baseline_before, - .after = after, - .before = before, - .points = points, - .options = options, - .tier = tier, - .timeout_ms = timeout_ms, - - .interrupt_callback = web_client_interrupt_callback, - .interrupt_callback_data = w, - - .transaction = &w->transaction, - }; - - return web_api_v12_weights(wb, &qwr); + ret = is_socket_closed(w->ofd); + + return ret; } -bool web_client_interrupt_callback(void *data) { - struct web_client *w = data; +void nd_web_api_init(void) { + contexts_alert_statuses_init(); + rrdr_options_init(); + contexts_options_init(); + datasource_formats_init(); + time_grouping_init(); +} - if(w->interrupt.callback) - return w->interrupt.callback(w, w->interrupt.callback_data); - return sock_has_output_error(w->ofd); +bool request_source_is_cloud(const char *source) { + return source && *source && strstartswith(source, "method=NC,"); +} + +void web_client_api_request_vX_source_to_buffer(struct web_client *w, BUFFER *source) { + if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_CLOUD)) + buffer_sprintf(source, "method=NC"); + else if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_BEARER)) + buffer_sprintf(source, "method=api-bearer"); + else + buffer_sprintf(source, "method=api"); + + if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_GOD)) + buffer_strcat(source, ",role=god"); + else + buffer_sprintf(source, ",role=%s", http_id2user_role(w->user_role)); + + buffer_sprintf(source, ",permissions="HTTP_ACCESS_FORMAT, (HTTP_ACCESS_FORMAT_CAST)w->access); + + if(w->auth.client_name[0]) + buffer_sprintf(source, ",user=%s", w->auth.client_name); + + if(!uuid_is_null(w->auth.cloud_account_id)) { + char uuid_str[UUID_COMPACT_STR_LEN]; + uuid_unparse_lower_compact(w->auth.cloud_account_id, uuid_str); + buffer_sprintf(source, ",account=%s", uuid_str); + } + + if(w->client_ip[0]) + buffer_sprintf(source, ",ip=%s", w->client_ip); + + if(w->forwarded_for) + buffer_sprintf(source, ",forwarded_for=%s", w->forwarded_for); } + +void web_client_progress_functions_update(void *data, size_t done, size_t all) { + // handle progress updates from the plugin + struct web_client *w = data; + query_progress_functions_update(&w->transaction, done, all); +} + diff --git a/src/web/api/web_api.h b/src/web/api/web_api.h index 634e5965..cb694a33 100644 --- a/src/web/api/web_api.h +++ b/src/web/api/web_api.h @@ -3,14 +3,28 @@ #ifndef NETDATA_WEB_API_H #define NETDATA_WEB_API_H 1 +#define ENABLE_API_V1 1 +#define ENABLE_API_v2 1 + +struct web_client; + #include "daemon/common.h" +#include "maps/maps.h" +#include "functions/functions.h" + #include "web/api/http_header.h" #include "web/api/http_auth.h" -#include "web/api/badges/web_buffer_svg.h" -#include "web/api/ilove/ilove.h" #include "web/api/formatters/rrd2json.h" #include "web/api/queries/weights.h" +void nd_web_api_init(void); + +bool request_source_is_cloud(const char *source); +void web_client_api_request_vX_source_to_buffer(struct web_client *w, BUFFER *source); +void web_client_progress_functions_update(void *data, size_t done, size_t all); + +void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key); + struct web_api_command { const char *api; uint32_t hash; @@ -37,7 +51,11 @@ int web_client_api_request_weights(RRDHOST *host, struct web_client *w, char *ur bool web_client_interrupt_callback(void *data); +char *format_value_and_unit(char *value_string, size_t value_string_len, + NETDATA_DOUBLE value, const char *units, int precision); + #include "web_api_v1.h" #include "web_api_v2.h" +#include "web_api_v3.h" #endif //NETDATA_WEB_API_H diff --git a/src/web/api/web_api_v1.c b/src/web/api/web_api_v1.c index bfaa4f6f..8d38e01d 100644 --- a/src/web/api/web_api_v1.c +++ b/src/web/api/web_api_v1.c @@ -1,1958 +1,232 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "web_api_v1.h" - -char *api_secret; - -static struct { - const char *name; - uint32_t hash; - RRDR_OPTIONS value; -} rrdr_options[] = { - { "nonzero" , 0 , RRDR_OPTION_NONZERO} - , {"flip" , 0 , RRDR_OPTION_REVERSED} - , {"reversed" , 0 , RRDR_OPTION_REVERSED} - , {"reverse" , 0 , RRDR_OPTION_REVERSED} - , {"jsonwrap" , 0 , RRDR_OPTION_JSON_WRAP} - , {"min2max" , 0 , RRDR_OPTION_DIMS_MIN2MAX} // rrdr2value() only - , {"average" , 0 , RRDR_OPTION_DIMS_AVERAGE} // rrdr2value() only - , {"min" , 0 , RRDR_OPTION_DIMS_MIN} // rrdr2value() only - , {"max" , 0 , RRDR_OPTION_DIMS_MAX} // rrdr2value() only - , {"ms" , 0 , RRDR_OPTION_MILLISECONDS} - , {"milliseconds" , 0 , RRDR_OPTION_MILLISECONDS} - , {"absolute" , 0 , RRDR_OPTION_ABSOLUTE} - , {"abs" , 0 , RRDR_OPTION_ABSOLUTE} - , {"absolute_sum" , 0 , RRDR_OPTION_ABSOLUTE} - , {"absolute-sum" , 0 , RRDR_OPTION_ABSOLUTE} - , {"display_absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} - , {"display-absolute" , 0 , RRDR_OPTION_DISPLAY_ABS} - , {"seconds" , 0 , RRDR_OPTION_SECONDS} - , {"null2zero" , 0 , RRDR_OPTION_NULL2ZERO} - , {"objectrows" , 0 , RRDR_OPTION_OBJECTSROWS} - , {"google_json" , 0 , RRDR_OPTION_GOOGLE_JSON} - , {"google-json" , 0 , RRDR_OPTION_GOOGLE_JSON} - , {"percentage" , 0 , RRDR_OPTION_PERCENTAGE} - , {"unaligned" , 0 , RRDR_OPTION_NOT_ALIGNED} - , {"match_ids" , 0 , RRDR_OPTION_MATCH_IDS} - , {"match-ids" , 0 , RRDR_OPTION_MATCH_IDS} - , {"match_names" , 0 , RRDR_OPTION_MATCH_NAMES} - , {"match-names" , 0 , RRDR_OPTION_MATCH_NAMES} - , {"anomaly-bit" , 0 , RRDR_OPTION_ANOMALY_BIT} - , {"selected-tier" , 0 , RRDR_OPTION_SELECTED_TIER} - , {"raw" , 0 , RRDR_OPTION_RETURN_RAW} - , {"jw-anomaly-rates" , 0 , RRDR_OPTION_RETURN_JWAR} - , {"natural-points" , 0 , RRDR_OPTION_NATURAL_POINTS} - , {"virtual-points" , 0 , RRDR_OPTION_VIRTUAL_POINTS} - , {"all-dimensions" , 0 , RRDR_OPTION_ALL_DIMENSIONS} - , {"details" , 0 , RRDR_OPTION_SHOW_DETAILS} - , {"debug" , 0 , RRDR_OPTION_DEBUG} - , {"plan" , 0 , RRDR_OPTION_DEBUG} - , {"minify" , 0 , RRDR_OPTION_MINIFY} - , {"group-by-labels" , 0 , RRDR_OPTION_GROUP_BY_LABELS} - , {"label-quotes" , 0 , RRDR_OPTION_LABEL_QUOTES} - , {NULL , 0 , 0} -}; - -static struct { - const char *name; - uint32_t hash; - CONTEXTS_V2_OPTIONS value; -} contexts_v2_options[] = { - {"minify" , 0 , CONTEXT_V2_OPTION_MINIFY} - , {"debug" , 0 , CONTEXT_V2_OPTION_DEBUG} - , {"config" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_CONFIGURATIONS} - , {"instances" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_INSTANCES} - , {"values" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_VALUES} - , {"summary" , 0 , CONTEXT_V2_OPTION_ALERTS_WITH_SUMMARY} - , {NULL , 0 , 0} -}; - -static struct { - const char *name; - uint32_t hash; - CONTEXTS_V2_ALERT_STATUS value; -} contexts_v2_alert_status[] = { - {"uninitialized" , 0 , CONTEXT_V2_ALERT_UNINITIALIZED} - , {"undefined" , 0 , CONTEXT_V2_ALERT_UNDEFINED} - , {"clear" , 0 , CONTEXT_V2_ALERT_CLEAR} - , {"raised" , 0 , CONTEXT_V2_ALERT_RAISED} - , {"active" , 0 , CONTEXT_V2_ALERT_RAISED} - , {"warning" , 0 , CONTEXT_V2_ALERT_WARNING} - , {"critical" , 0 , CONTEXT_V2_ALERT_CRITICAL} - , {NULL , 0 , 0} -}; - -static struct { - const char *name; - uint32_t hash; - DATASOURCE_FORMAT value; -} api_v1_data_formats[] = { - { DATASOURCE_FORMAT_DATATABLE_JSON , 0 , DATASOURCE_DATATABLE_JSON} - , {DATASOURCE_FORMAT_DATATABLE_JSONP, 0 , DATASOURCE_DATATABLE_JSONP} - , {DATASOURCE_FORMAT_JSON , 0 , DATASOURCE_JSON} - , {DATASOURCE_FORMAT_JSON2 , 0 , DATASOURCE_JSON2} - , {DATASOURCE_FORMAT_JSONP , 0 , DATASOURCE_JSONP} - , {DATASOURCE_FORMAT_SSV , 0 , DATASOURCE_SSV} - , {DATASOURCE_FORMAT_CSV , 0 , DATASOURCE_CSV} - , {DATASOURCE_FORMAT_TSV , 0 , DATASOURCE_TSV} - , {"tsv-excel" , 0 , DATASOURCE_TSV} - , {DATASOURCE_FORMAT_HTML , 0 , DATASOURCE_HTML} - , {DATASOURCE_FORMAT_JS_ARRAY , 0 , DATASOURCE_JS_ARRAY} - , {DATASOURCE_FORMAT_SSV_COMMA , 0 , DATASOURCE_SSV_COMMA} - , {DATASOURCE_FORMAT_CSV_JSON_ARRAY , 0 , DATASOURCE_CSV_JSON_ARRAY} - , {DATASOURCE_FORMAT_CSV_MARKDOWN , 0 , DATASOURCE_CSV_MARKDOWN} - - // terminator - , {NULL, 0, 0} -}; - -static struct { - const char *name; - uint32_t hash; - DATASOURCE_FORMAT value; -} api_v1_data_google_formats[] = { - // this is not an error - when Google requests json, it expects javascript - // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source#responseformat - {"json", 0, DATASOURCE_DATATABLE_JSONP} - , {"html", 0, DATASOURCE_HTML} - , {"csv", 0, DATASOURCE_CSV} - , {"tsv-excel", 0, DATASOURCE_TSV} - - // terminator - , {NULL, 0, 0} -}; - -void web_client_api_v1_init(void) { - int i; - - for(i = 0; contexts_v2_alert_status[i].name ; i++) - contexts_v2_alert_status[i].hash = simple_hash(contexts_v2_alert_status[i].name); - - for(i = 0; rrdr_options[i].name ; i++) - rrdr_options[i].hash = simple_hash(rrdr_options[i].name); - - for(i = 0; contexts_v2_options[i].name ; i++) - contexts_v2_options[i].hash = simple_hash(contexts_v2_options[i].name); - - for(i = 0; api_v1_data_formats[i].name ; i++) - api_v1_data_formats[i].hash = simple_hash(api_v1_data_formats[i].name); - - for(i = 0; api_v1_data_google_formats[i].name ; i++) - api_v1_data_google_formats[i].hash = simple_hash(api_v1_data_google_formats[i].name); - - time_grouping_init(); - - nd_uuid_t uuid; - - // generate - uuid_generate(uuid); - - // unparse (to string) - char uuid_str[37]; - uuid_unparse_lower(uuid, uuid_str); -} - -char *get_mgmt_api_key(void) { - char filename[FILENAME_MAX + 1]; - snprintfz(filename, FILENAME_MAX, "%s/netdata.api.key", netdata_configured_varlib_dir); - char *api_key_filename=config_get(CONFIG_SECTION_REGISTRY, "netdata management api key file", filename); - static char guid[GUID_LEN + 1] = ""; - - if(likely(guid[0])) - return guid; - - // read it from disk - int fd = open(api_key_filename, O_RDONLY | O_CLOEXEC); - if(fd != -1) { - char buf[GUID_LEN + 1]; - if(read(fd, buf, GUID_LEN) != GUID_LEN) - netdata_log_error("Failed to read management API key from '%s'", api_key_filename); - else { - buf[GUID_LEN] = '\0'; - if(regenerate_guid(buf, guid) == -1) { - netdata_log_error("Failed to validate management API key '%s' from '%s'.", - buf, api_key_filename); - - guid[0] = '\0'; - } - } - close(fd); - } - - // generate a new one? - if(!guid[0]) { - nd_uuid_t uuid; - - uuid_generate_time(uuid); - uuid_unparse_lower(uuid, guid); - guid[GUID_LEN] = '\0'; - - // save it - fd = open(api_key_filename, O_WRONLY|O_CREAT|O_TRUNC | O_CLOEXEC, 444); - if(fd == -1) { - netdata_log_error("Cannot create unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file.", api_key_filename); - goto temp_key; - } - - if(write(fd, guid, GUID_LEN) != GUID_LEN) { - netdata_log_error("Cannot write the unique management API key file '%s'. Please adjust config parameter 'netdata management api key file' to a proper path and file with enough space left.", api_key_filename); - close(fd); - goto temp_key; - } - - close(fd); - } - - return guid; - -temp_key: - netdata_log_info("You can still continue to use the alarm management API using the authorization token %s during this Netdata session only.", guid); - return guid; -} - -void web_client_api_v1_management_init(void) { - api_secret = get_mgmt_api_key(); -} - -inline RRDR_OPTIONS rrdr_options_parse_one(const char *o) { - RRDR_OPTIONS ret = 0; - - if(!o || !*o) return ret; - - uint32_t hash = simple_hash(o); - int i; - for(i = 0; rrdr_options[i].name ; i++) { - if (unlikely(hash == rrdr_options[i].hash && !strcmp(o, rrdr_options[i].name))) { - ret |= rrdr_options[i].value; - break; - } - } - - return ret; -} - -inline RRDR_OPTIONS rrdr_options_parse(char *o) { - RRDR_OPTIONS ret = 0; - char *tok; - - while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { - if(!*tok) continue; - ret |= rrdr_options_parse_one(tok); - } - - return ret; -} - -inline CONTEXTS_V2_OPTIONS web_client_api_request_v2_context_options(char *o) { - CONTEXTS_V2_OPTIONS ret = 0; - char *tok; - - while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { - if(!*tok) continue; - - uint32_t hash = simple_hash(tok); - int i; - for(i = 0; contexts_v2_options[i].name ; i++) { - if (unlikely(hash == contexts_v2_options[i].hash && !strcmp(tok, contexts_v2_options[i].name))) { - ret |= contexts_v2_options[i].value; - break; - } - } - } - - return ret; -} - -inline CONTEXTS_V2_ALERT_STATUS web_client_api_request_v2_alert_status(char *o) { - CONTEXTS_V2_ALERT_STATUS ret = 0; - char *tok; - - while(o && *o && (tok = strsep_skip_consecutive_separators(&o, ", |"))) { - if(!*tok) continue; - - uint32_t hash = simple_hash(tok); - int i; - for(i = 0; contexts_v2_alert_status[i].name ; i++) { - if (unlikely(hash == contexts_v2_alert_status[i].hash && !strcmp(tok, contexts_v2_alert_status[i].name))) { - ret |= contexts_v2_alert_status[i].value; - break; - } - } - } - - return ret; -} - -void web_client_api_request_v2_contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_ALERT_STATUS options) { - buffer_json_member_add_array(wb, key); - - RRDR_OPTIONS used = 0; // to prevent adding duplicates - for(int i = 0; contexts_v2_alert_status[i].name ; i++) { - if (unlikely((contexts_v2_alert_status[i].value & options) && !(contexts_v2_alert_status[i].value & used))) { - const char *name = contexts_v2_alert_status[i].name; - used |= contexts_v2_alert_status[i].value; - - buffer_json_add_array_item_string(wb, name); - } - } - - buffer_json_array_close(wb); -} - -void web_client_api_request_v2_contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_OPTIONS options) { - buffer_json_member_add_array(wb, key); - - RRDR_OPTIONS used = 0; // to prevent adding duplicates - for(int i = 0; contexts_v2_options[i].name ; i++) { - if (unlikely((contexts_v2_options[i].value & options) && !(contexts_v2_options[i].value & used))) { - const char *name = contexts_v2_options[i].name; - used |= contexts_v2_options[i].value; - - buffer_json_add_array_item_string(wb, name); - } - } - - buffer_json_array_close(wb); -} - -void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options) { - buffer_json_member_add_array(wb, key); - - RRDR_OPTIONS used = 0; // to prevent adding duplicates - for(int i = 0; rrdr_options[i].name ; i++) { - if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { - const char *name = rrdr_options[i].name; - used |= rrdr_options[i].value; - - buffer_json_add_array_item_string(wb, name); - } - } - - buffer_json_array_close(wb); -} - -void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options) { - RRDR_OPTIONS used = 0; // to prevent adding duplicates - size_t added = 0; - for(int i = 0; rrdr_options[i].name ; i++) { - if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { - const char *name = rrdr_options[i].name; - used |= rrdr_options[i].value; - - if(added++) buffer_strcat(wb, " "); - buffer_strcat(wb, name); - } - } -} - -void web_client_api_request_v1_data_options_to_string(char *buf, size_t size, RRDR_OPTIONS options) { - char *write = buf; - char *end = &buf[size - 1]; - - RRDR_OPTIONS used = 0; // to prevent adding duplicates - int added = 0; - for(int i = 0; rrdr_options[i].name ; i++) { - if (unlikely((rrdr_options[i].value & options) && !(rrdr_options[i].value & used))) { - const char *name = rrdr_options[i].name; - used |= rrdr_options[i].value; - - if(added && write < end) - *write++ = ','; - - while(*name && write < end) - *write++ = *name++; - - added++; - } - } - *write = *end = '\0'; -} - -inline uint32_t web_client_api_request_v1_data_format(char *name) { - uint32_t hash = simple_hash(name); - int i; - - for(i = 0; api_v1_data_formats[i].name ; i++) { - if (unlikely(hash == api_v1_data_formats[i].hash && !strcmp(name, api_v1_data_formats[i].name))) { - return api_v1_data_formats[i].value; - } - } - - return DATASOURCE_JSON; -} - -inline uint32_t web_client_api_request_v1_data_google_format(char *name) { - uint32_t hash = simple_hash(name); - int i; - - for(i = 0; api_v1_data_google_formats[i].name ; i++) { - if (unlikely(hash == api_v1_data_google_formats[i].hash && !strcmp(name, api_v1_data_google_formats[i].name))) { - return api_v1_data_google_formats[i].value; - } - } - - return DATASOURCE_JSON; -} - -int web_client_api_request_v1_alarms_select (char *url) { - int all = 0; - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - if(!strcmp(value, "all") || !strcmp(value, "all=true")) all = 1; - else if(!strcmp(value, "active") || !strcmp(value, "active=true")) all = 0; - } - - return all; -} - -inline int web_client_api_request_v1_alarms(RRDHOST *host, struct web_client *w, char *url) { - int all = web_client_api_request_v1_alarms_select(url); - - buffer_flush(w->response.data); - w->response.data->content_type = CT_APPLICATION_JSON; - health_alarms2json(host, w->response.data, all); - buffer_no_cacheable(w->response.data); - return HTTP_RESP_OK; -} - -inline int web_client_api_request_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url) { - int all = web_client_api_request_v1_alarms_select(url); - - buffer_flush(w->response.data); - w->response.data->content_type = CT_APPLICATION_JSON; - health_alarms_values2json(host, w->response.data, all); - buffer_no_cacheable(w->response.data); - return HTTP_RESP_OK; -} - -inline int web_client_api_request_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url) { - RRDCALC_STATUS status = RRDCALC_STATUS_RAISED; - BUFFER *contexts = NULL; - - buffer_flush(w->response.data); - buffer_sprintf(w->response.data, "["); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 alarm_count query param '%s' with value '%s'", w->id, name, value); - - char* p = value; - if(!strcmp(name, "status")) { - while ((*p = toupper(*p))) p++; - if (!strcmp("CRITICAL", value)) status = RRDCALC_STATUS_CRITICAL; - else if (!strcmp("WARNING", value)) status = RRDCALC_STATUS_WARNING; - else if (!strcmp("UNINITIALIZED", value)) status = RRDCALC_STATUS_UNINITIALIZED; - else if (!strcmp("UNDEFINED", value)) status = RRDCALC_STATUS_UNDEFINED; - else if (!strcmp("REMOVED", value)) status = RRDCALC_STATUS_REMOVED; - else if (!strcmp("CLEAR", value)) status = RRDCALC_STATUS_CLEAR; - } - else if(!strcmp(name, "context") || !strcmp(name, "ctx")) { - if(!contexts) contexts = buffer_create(255, &netdata_buffers_statistics.buffers_api); - buffer_strcat(contexts, "|"); - buffer_strcat(contexts, value); - } - } - - health_aggregate_alarms(host, w->response.data, contexts, status); - - buffer_sprintf(w->response.data, "]\n"); - w->response.data->content_type = CT_APPLICATION_JSON; - buffer_no_cacheable(w->response.data); - - buffer_free(contexts); - return 200; -} - -inline int web_client_api_request_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url) { - time_t after = 0; - char *chart = NULL; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - if (!strcmp(name, "after")) after = (time_t) strtoul(value, NULL, 0); - else if (!strcmp(name, "chart")) chart = value; - } - - buffer_flush(w->response.data); - w->response.data->content_type = CT_APPLICATION_JSON; - sql_health_alarm_log2json(host, w->response.data, after, chart); - return HTTP_RESP_OK; -} - -inline int web_client_api_request_single_chart(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)) { - int ret = HTTP_RESP_BAD_REQUEST; - char *chart = NULL; - - buffer_flush(w->response.data); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "chart")) chart = value; - //else { - /// buffer_sprintf(w->response.data, "Unknown parameter '%s' in request.", name); - // goto cleanup; - //} - } - - if(!chart || !*chart) { - buffer_sprintf(w->response.data, "No chart id is given at the request."); - goto cleanup; - } - - RRDSET *st = rrdset_find(host, chart); - if(!st) st = rrdset_find_byname(host, chart); - if(!st) { - buffer_strcat(w->response.data, "Chart is not found: "); - buffer_strcat_htmlescape(w->response.data, chart); - ret = HTTP_RESP_NOT_FOUND; - goto cleanup; - } - - w->response.data->content_type = CT_APPLICATION_JSON; - st->last_accessed_time_s = now_realtime_sec(); - callback(st, w->response.data); - return HTTP_RESP_OK; - - cleanup: - return ret; -} - -static inline int web_client_api_request_variable(RRDHOST *host, struct web_client *w, char *url) { - int ret = HTTP_RESP_BAD_REQUEST; - char *chart = NULL; - char *variable = NULL; - - buffer_flush(w->response.data); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "chart")) chart = value; - else if(!strcmp(name, "variable")) variable = value; - } - - if(!chart || !*chart || !variable || !*variable) { - buffer_sprintf(w->response.data, "A chart= and a variable= are required."); - goto cleanup; - } - - RRDSET *st = rrdset_find(host, chart); - if(!st) st = rrdset_find_byname(host, chart); - if(!st) { - buffer_strcat(w->response.data, "Chart is not found: "); - buffer_strcat_htmlescape(w->response.data, chart); - ret = HTTP_RESP_NOT_FOUND; - goto cleanup; - } - - w->response.data->content_type = CT_APPLICATION_JSON; - st->last_accessed_time_s = now_realtime_sec(); - alert_variable_lookup_trace(host, st, variable, w->response.data); - - return HTTP_RESP_OK; - -cleanup: - return ret; -} - -inline int web_client_api_request_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url) { - return web_client_api_request_single_chart(host, w, url, health_api_v1_chart_variables2json); -} - -static int web_client_api_request_v1_context(RRDHOST *host, struct web_client *w, char *url) { - char *context = NULL; - RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; - time_t after = 0, before = 0; - const char *chart_label_key = NULL, *chart_labels_filter = NULL; - BUFFER *dimensions = NULL; - - buffer_flush(w->response.data); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "context") || !strcmp(name, "ctx")) context = value; - else if(!strcmp(name, "after")) after = str2l(value); - else if(!strcmp(name, "before")) before = str2l(value); - else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); - else if(!strcmp(name, "chart_label_key")) chart_label_key = value; - else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; - else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { - if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); - buffer_strcat(dimensions, "|"); - buffer_strcat(dimensions, value); - } - } - - if(!context || !*context) { - buffer_sprintf(w->response.data, "No context is given at the request."); - return HTTP_RESP_BAD_REQUEST; - } - - SIMPLE_PATTERN *chart_label_key_pattern = NULL; - SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; - SIMPLE_PATTERN *chart_dimensions_pattern = NULL; - - if(chart_label_key) - chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); - - if(chart_labels_filter) - chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, - true); - - if(dimensions) { - chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", - SIMPLE_PATTERN_EXACT, true); - buffer_free(dimensions); - } - - w->response.data->content_type = CT_APPLICATION_JSON; - int ret = rrdcontext_to_json(host, w->response.data, after, before, options, context, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); - - simple_pattern_free(chart_label_key_pattern); - simple_pattern_free(chart_labels_filter_pattern); - simple_pattern_free(chart_dimensions_pattern); - - return ret; -} - -static int web_client_api_request_v1_contexts(RRDHOST *host, struct web_client *w, char *url) { - RRDCONTEXT_TO_JSON_OPTIONS options = RRDCONTEXT_OPTION_NONE; - time_t after = 0, before = 0; - const char *chart_label_key = NULL, *chart_labels_filter = NULL; - BUFFER *dimensions = NULL; - - buffer_flush(w->response.data); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "after")) after = str2l(value); - else if(!strcmp(name, "before")) before = str2l(value); - else if(!strcmp(name, "options")) options = rrdcontext_to_json_parse_options(value); - else if(!strcmp(name, "chart_label_key")) chart_label_key = value; - else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; - else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { - if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); - buffer_strcat(dimensions, "|"); - buffer_strcat(dimensions, value); - } - } - - SIMPLE_PATTERN *chart_label_key_pattern = NULL; - SIMPLE_PATTERN *chart_labels_filter_pattern = NULL; - SIMPLE_PATTERN *chart_dimensions_pattern = NULL; - - if(chart_label_key) - chart_label_key_pattern = simple_pattern_create(chart_label_key, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, true); - - if(chart_labels_filter) - chart_labels_filter_pattern = simple_pattern_create(chart_labels_filter, ",|\t\r\n\f\v", SIMPLE_PATTERN_EXACT, - true); - - if(dimensions) { - chart_dimensions_pattern = simple_pattern_create(buffer_tostring(dimensions), ",|\t\r\n\f\v", - SIMPLE_PATTERN_EXACT, true); - buffer_free(dimensions); - } - - w->response.data->content_type = CT_APPLICATION_JSON; - int ret = rrdcontexts_to_json(host, w->response.data, after, before, options, chart_label_key_pattern, chart_labels_filter_pattern, chart_dimensions_pattern); - - simple_pattern_free(chart_label_key_pattern); - simple_pattern_free(chart_labels_filter_pattern); - simple_pattern_free(chart_dimensions_pattern); - - return ret; -} - -inline int web_client_api_request_v1_charts(RRDHOST *host, struct web_client *w, char *url) { - (void)url; - - buffer_flush(w->response.data); - w->response.data->content_type = CT_APPLICATION_JSON; - charts2json(host, w->response.data); - return HTTP_RESP_OK; -} - -inline int web_client_api_request_v1_chart(RRDHOST *host, struct web_client *w, char *url) { - return web_client_api_request_single_chart(host, w, url, rrd_stats_api_v1_chart); -} - -// returns the HTTP code -static inline int web_client_api_request_v1_data(RRDHOST *host, struct web_client *w, char *url) { - netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data with URL '%s'", w->id, url); - - int ret = HTTP_RESP_BAD_REQUEST; - BUFFER *dimensions = NULL; - - buffer_flush(w->response.data); - - char *google_version = "0.6", - *google_reqId = "0", - *google_sig = "0", - *google_out = "json", - *responseHandler = NULL, - *outFileName = NULL; - - time_t last_timestamp_in_data = 0, google_timestamp = 0; - - char *chart = NULL; - char *before_str = NULL; - char *after_str = NULL; - char *group_time_str = NULL; - char *points_str = NULL; - char *timeout_str = NULL; - char *context = NULL; - char *chart_label_key = NULL; - char *chart_labels_filter = NULL; - char *group_options = NULL; - size_t tier = 0; - RRDR_TIME_GROUPING group = RRDR_GROUPING_AVERAGE; - DATASOURCE_FORMAT format = DATASOURCE_JSON; - RRDR_OPTIONS options = 0; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 data query param '%s' with value '%s'", w->id, name, value); - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "context")) context = value; - else if(!strcmp(name, "chart_label_key")) chart_label_key = value; - else if(!strcmp(name, "chart_labels_filter")) chart_labels_filter = value; - else if(!strcmp(name, "chart")) chart = value; - else if(!strcmp(name, "dimension") || !strcmp(name, "dim") || !strcmp(name, "dimensions") || !strcmp(name, "dims")) { - if(!dimensions) dimensions = buffer_create(100, &netdata_buffers_statistics.buffers_api); - buffer_strcat(dimensions, "|"); - buffer_strcat(dimensions, value); - } - else if(!strcmp(name, "show_dimensions")) options |= RRDR_OPTION_ALL_DIMENSIONS; - else if(!strcmp(name, "after")) after_str = value; - else if(!strcmp(name, "before")) before_str = value; - else if(!strcmp(name, "points")) points_str = value; - else if(!strcmp(name, "timeout")) timeout_str = value; - else if(!strcmp(name, "gtime")) group_time_str = value; - else if(!strcmp(name, "group_options")) group_options = value; - else if(!strcmp(name, "group")) { - group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); - } - else if(!strcmp(name, "format")) { - format = web_client_api_request_v1_data_format(value); - } - else if(!strcmp(name, "options")) { - options |= rrdr_options_parse(value); - } - else if(!strcmp(name, "callback")) { - responseHandler = value; - } - else if(!strcmp(name, "filename")) { - outFileName = value; - } - else if(!strcmp(name, "tqx")) { - // parse Google Visualization API options - // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source - char *tqx_name, *tqx_value; - - while(value) { - tqx_value = strsep_skip_consecutive_separators(&value, ";"); - if(!tqx_value || !*tqx_value) continue; - - tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); - if(!tqx_name || !*tqx_name) continue; - if(!tqx_value || !*tqx_value) continue; - - if(!strcmp(tqx_name, "version")) - google_version = tqx_value; - else if(!strcmp(tqx_name, "reqId")) - google_reqId = tqx_value; - else if(!strcmp(tqx_name, "sig")) { - google_sig = tqx_value; - google_timestamp = strtoul(google_sig, NULL, 0); - } - else if(!strcmp(tqx_name, "out")) { - google_out = tqx_value; - format = web_client_api_request_v1_data_google_format(google_out); - } - else if(!strcmp(tqx_name, "responseHandler")) - responseHandler = tqx_value; - else if(!strcmp(tqx_name, "outFileName")) - outFileName = tqx_value; - } - } - else if(!strcmp(name, "tier")) { - tier = str2ul(value); - if(tier < storage_tiers) - options |= RRDR_OPTION_SELECTED_TIER; - else - tier = 0; - } - } - - // validate the google parameters given - fix_google_param(google_out); - fix_google_param(google_sig); - fix_google_param(google_reqId); - fix_google_param(google_version); - fix_google_param(responseHandler); - fix_google_param(outFileName); - - RRDSET *st = NULL; - ONEWAYALLOC *owa = onewayalloc_create(0); - QUERY_TARGET *qt = NULL; - - if(!is_valid_sp(chart) && !is_valid_sp(context)) { - buffer_sprintf(w->response.data, "No chart or context is given."); - goto cleanup; - } - - if(chart && !context) { - // check if this is a specific chart - st = rrdset_find(host, chart); - if (!st) st = rrdset_find_byname(host, chart); - } - - long long before = (before_str && *before_str)?str2l(before_str):0; - long long after = (after_str && *after_str) ?str2l(after_str):-600; - int points = (points_str && *points_str)?str2i(points_str):0; - int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; - long group_time = (group_time_str && *group_time_str)?str2l(group_time_str):0; - - QUERY_TARGET_REQUEST qtr = { - .version = 1, - .after = after, - .before = before, - .host = host, - .st = st, - .nodes = NULL, - .contexts = context, - .instances = chart, - .dimensions = (dimensions)?buffer_tostring(dimensions):NULL, - .timeout_ms = timeout, - .points = points, - .format = format, - .options = options, - .time_group_method = group, - .time_group_options = group_options, - .resampling_time = group_time, - .tier = tier, - .chart_label_key = chart_label_key, - .labels = chart_labels_filter, - .query_source = QUERY_SOURCE_API_DATA, - .priority = STORAGE_PRIORITY_NORMAL, - .interrupt_callback = web_client_interrupt_callback, - .interrupt_callback_data = w, - .transaction = &w->transaction, - }; - qt = query_target_create(&qtr); - - if(!qt || !qt->query.used) { - buffer_sprintf(w->response.data, "No metrics where matched to query."); - ret = HTTP_RESP_NOT_FOUND; - goto cleanup; - } - - web_client_timeout_checkpoint_set(w, timeout); - if(web_client_timeout_checkpoint_and_check(w, NULL)) { - ret = w->response.code; - goto cleanup; - } - - if(outFileName && *outFileName) { - buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); - netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); - } - - if(format == DATASOURCE_DATATABLE_JSONP) { - if(responseHandler == NULL) - responseHandler = "google.visualization.Query.setResponse"; - - netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", - w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName - ); - - buffer_sprintf( - w->response.data, - "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", - responseHandler, - google_version, - google_reqId, - (int64_t)(st ? st->last_updated.tv_sec : 0)); - } - else if(format == DATASOURCE_JSONP) { - if(responseHandler == NULL) - responseHandler = "callback"; - - buffer_strcat(w->response.data, responseHandler); - buffer_strcat(w->response.data, "("); - } - - ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); - - if(format == DATASOURCE_DATATABLE_JSONP) { - if(google_timestamp < last_timestamp_in_data) - buffer_strcat(w->response.data, "});"); - - else { - // the client already has the latest data - buffer_flush(w->response.data); - buffer_sprintf(w->response.data, - "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", - responseHandler, google_version, google_reqId); - } - } - else if(format == DATASOURCE_JSONP) - buffer_strcat(w->response.data, ");"); - - if(qt->internal.relative) - buffer_no_cacheable(w->response.data); - else - buffer_cacheable(w->response.data); - -cleanup: - query_target_release(qt); - onewayalloc_destroy(owa); - buffer_free(dimensions); - return ret; -} - -// Pings a netdata server: -// /api/v1/registry?action=hello -// -// Access to a netdata registry: -// /api/v1/registry?action=access&machine=${machine_guid}&name=${hostname}&url=${url} -// -// Delete from a netdata registry: -// /api/v1/registry?action=delete&machine=${machine_guid}&name=${hostname}&url=${url}&delete_url=${delete_url} -// -// Search for the URLs of a machine: -// /api/v1/registry?action=search&for=${machine_guid} -// -// Impersonate: -// /api/v1/registry?action=switch&machine=${machine_guid}&name=${hostname}&url=${url}&to=${new_person_guid} -inline int web_client_api_request_v1_registry(RRDHOST *host, struct web_client *w, char *url) { - static uint32_t hash_action = 0, hash_access = 0, hash_hello = 0, hash_delete = 0, hash_search = 0, - hash_switch = 0, hash_machine = 0, hash_url = 0, hash_name = 0, hash_delete_url = 0, hash_for = 0, - hash_to = 0 /*, hash_redirects = 0 */; - - if(unlikely(!hash_action)) { - hash_action = simple_hash("action"); - hash_access = simple_hash("access"); - hash_hello = simple_hash("hello"); - hash_delete = simple_hash("delete"); - hash_search = simple_hash("search"); - hash_switch = simple_hash("switch"); - hash_machine = simple_hash("machine"); - hash_url = simple_hash("url"); - hash_name = simple_hash("name"); - hash_delete_url = simple_hash("delete_url"); - hash_for = simple_hash("for"); - hash_to = simple_hash("to"); -/* - hash_redirects = simple_hash("redirects"); -*/ - } - - netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry with URL '%s'", w->id, url); - - // TODO - // The browser may send multiple cookies with our id - - char person_guid[UUID_STR_LEN] = ""; - char *cookie = strstr(w->response.data->buffer, NETDATA_REGISTRY_COOKIE_NAME "="); - if(cookie) - strncpyz(person_guid, &cookie[sizeof(NETDATA_REGISTRY_COOKIE_NAME)], UUID_STR_LEN - 1); - else if(!extract_bearer_token_from_request(w, person_guid, sizeof(person_guid))) - person_guid[0] = '\0'; - - char action = '\0'; - char *machine_guid = NULL, - *machine_url = NULL, - *url_name = NULL, - *search_machine_guid = NULL, - *delete_url = NULL, - *to_person_guid = NULL; -/* - int redirects = 0; -*/ - - // Don't cache registry responses - buffer_no_cacheable(w->response.data); - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) continue; - if (!value || !*value) continue; - - netdata_log_debug(D_WEB_CLIENT, "%llu: API v1 registry query param '%s' with value '%s'", w->id, name, value); - - uint32_t hash = simple_hash(name); - - if(hash == hash_action && !strcmp(name, "action")) { - uint32_t vhash = simple_hash(value); - - if(vhash == hash_access && !strcmp(value, "access")) action = 'A'; - else if(vhash == hash_hello && !strcmp(value, "hello")) action = 'H'; - else if(vhash == hash_delete && !strcmp(value, "delete")) action = 'D'; - else if(vhash == hash_search && !strcmp(value, "search")) action = 'S'; - else if(vhash == hash_switch && !strcmp(value, "switch")) action = 'W'; -#ifdef NETDATA_INTERNAL_CHECKS - else netdata_log_error("unknown registry action '%s'", value); -#endif /* NETDATA_INTERNAL_CHECKS */ - } -/* - else if(hash == hash_redirects && !strcmp(name, "redirects")) - redirects = atoi(value); -*/ - else if(hash == hash_machine && !strcmp(name, "machine")) - machine_guid = value; - - else if(hash == hash_url && !strcmp(name, "url")) - machine_url = value; - - else if(action == 'A') { - if(hash == hash_name && !strcmp(name, "name")) - url_name = value; - } - else if(action == 'D') { - if(hash == hash_delete_url && !strcmp(name, "delete_url")) - delete_url = value; - } - else if(action == 'S') { - if(hash == hash_for && !strcmp(name, "for")) - search_machine_guid = value; - } - else if(action == 'W') { - if(hash == hash_to && !strcmp(name, "to")) - to_person_guid = value; - } -#ifdef NETDATA_INTERNAL_CHECKS - else netdata_log_error("unused registry URL parameter '%s' with value '%s'", name, value); -#endif /* NETDATA_INTERNAL_CHECKS */ - } - - bool do_not_track = respect_web_browser_do_not_track_policy && web_client_has_donottrack(w); - - if(unlikely(action == 'H')) { - // HELLO request, dashboard ACL - analytics_log_dashboard(); - if(unlikely(!http_can_access_dashboard(w))) - return web_client_permission_denied_acl(w); - } - else { - // everything else, registry ACL - if(unlikely(!http_can_access_registry(w))) - return web_client_permission_denied_acl(w); - - if(unlikely(do_not_track)) { - buffer_flush(w->response.data); - buffer_sprintf(w->response.data, "Your web browser is sending 'DNT: 1' (Do Not Track). The registry requires persistent cookies on your browser to work."); - return HTTP_RESP_BAD_REQUEST; - } - } - - buffer_no_cacheable(w->response.data); - - switch(action) { - case 'A': - if(unlikely(!machine_guid || !machine_url || !url_name)) { - netdata_log_error("Invalid registry request - access requires these parameters: machine ('%s'), url ('%s'), name ('%s')", machine_guid ? machine_guid : "UNSET", machine_url ? machine_url : "UNSET", url_name ? url_name : "UNSET"); - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "Invalid registry Access request."); - return HTTP_RESP_BAD_REQUEST; - } - - web_client_enable_tracking_required(w); - return registry_request_access_json(host, w, person_guid, machine_guid, machine_url, url_name, now_realtime_sec()); - - case 'D': - if(unlikely(!machine_guid || !machine_url || !delete_url)) { - netdata_log_error("Invalid registry request - delete requires these parameters: machine ('%s'), url ('%s'), delete_url ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", delete_url?delete_url:"UNSET"); - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "Invalid registry Delete request."); - return HTTP_RESP_BAD_REQUEST; - } - - web_client_enable_tracking_required(w); - return registry_request_delete_json(host, w, person_guid, machine_guid, machine_url, delete_url, now_realtime_sec()); - - case 'S': - if(unlikely(!search_machine_guid)) { - netdata_log_error("Invalid registry request - search requires these parameters: for ('%s')", search_machine_guid?search_machine_guid:"UNSET"); - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "Invalid registry Search request."); - return HTTP_RESP_BAD_REQUEST; - } - - web_client_enable_tracking_required(w); - return registry_request_search_json(host, w, person_guid, search_machine_guid); - - case 'W': - if(unlikely(!machine_guid || !machine_url || !to_person_guid)) { - netdata_log_error("Invalid registry request - switching identity requires these parameters: machine ('%s'), url ('%s'), to ('%s')", machine_guid?machine_guid:"UNSET", machine_url?machine_url:"UNSET", to_person_guid?to_person_guid:"UNSET"); - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "Invalid registry Switch request."); - return HTTP_RESP_BAD_REQUEST; - } - - web_client_enable_tracking_required(w); - return registry_request_switch_json(host, w, person_guid, machine_guid, machine_url, to_person_guid, now_realtime_sec()); - - case 'H': - return registry_request_hello_json(host, w, do_not_track); - - default: - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "Invalid registry request - you need to set an action: hello, access, delete, search"); - return HTTP_RESP_BAD_REQUEST; - } -} - -void web_client_api_request_v1_info_summary_alarm_statuses(RRDHOST *host, BUFFER *wb, const char *key) { - buffer_json_member_add_object(wb, key); - - size_t normal = 0, warning = 0, critical = 0; - RRDCALC *rc; - foreach_rrdcalc_in_rrdhost_read(host, rc) { - if(unlikely(!rc->rrdset || !rc->rrdset->last_collected_time.tv_sec)) - continue; - - switch(rc->status) { - case RRDCALC_STATUS_WARNING: - warning++; - break; - case RRDCALC_STATUS_CRITICAL: - critical++; - break; - default: - normal++; - } - } - foreach_rrdcalc_in_rrdhost_done(rc); - - buffer_json_member_add_uint64(wb, "normal", normal); - buffer_json_member_add_uint64(wb, "warning", warning); - buffer_json_member_add_uint64(wb, "critical", critical); - - buffer_json_object_close(wb); -} - -static inline void web_client_api_request_v1_info_mirrored_hosts_status(BUFFER *wb, RRDHOST *host) { - buffer_json_add_array_item_object(wb); - - buffer_json_member_add_string(wb, "hostname", rrdhost_hostname(host)); - buffer_json_member_add_uint64(wb, "hops", host->system_info ? host->system_info->hops : (host == localhost) ? 0 : 1); - buffer_json_member_add_boolean(wb, "reachable", (host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))); - - buffer_json_member_add_string(wb, "guid", host->machine_guid); - buffer_json_member_add_uuid(wb, "node_id", host->node_id); - rrdhost_aclk_state_lock(host); - buffer_json_member_add_string(wb, "claim_id", host->aclk_state.claimed_id); - rrdhost_aclk_state_unlock(host); - - buffer_json_object_close(wb); -} - -static inline void web_client_api_request_v1_info_mirrored_hosts(BUFFER *wb) { - RRDHOST *host; - - rrd_rdlock(); - - buffer_json_member_add_array(wb, "mirrored_hosts"); - rrdhost_foreach_read(host) - buffer_json_add_array_item_string(wb, rrdhost_hostname(host)); - buffer_json_array_close(wb); - - buffer_json_member_add_array(wb, "mirrored_hosts_status"); - rrdhost_foreach_read(host) { - if ((host == localhost || !rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { - web_client_api_request_v1_info_mirrored_hosts_status(wb, host); - } - } - rrdhost_foreach_read(host) { - if ((host != localhost && rrdhost_flag_check(host, RRDHOST_FLAG_ORPHAN))) { - web_client_api_request_v1_info_mirrored_hosts_status(wb, host); - } - } - buffer_json_array_close(wb); - - rrd_rdunlock(); -} - -void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key) { - buffer_json_member_add_object(wb, key); - rrdlabels_to_buffer_json_members(host->rrdlabels, wb); - buffer_json_object_close(wb); -} - -static void host_collectors(RRDHOST *host, BUFFER *wb) { - buffer_json_member_add_array(wb, "collectors"); - - DICTIONARY *dict = dictionary_create(DICT_OPTION_SINGLE_THREADED|DICT_OPTION_DONT_OVERWRITE_VALUE); - RRDSET *st; - char name[500]; - - time_t now = now_realtime_sec(); - - rrdset_foreach_read(st, host) { - if (!rrdset_is_available_for_viewers(st)) - continue; - - sprintf(name, "%s:%s", rrdset_plugin_name(st), rrdset_module_name(st)); - - bool old = 0; - bool *set = dictionary_set(dict, name, &old, sizeof(bool)); - if(!*set) { - *set = true; - st->last_accessed_time_s = now; - buffer_json_add_array_item_object(wb); - buffer_json_member_add_string(wb, "plugin", rrdset_plugin_name(st)); - buffer_json_member_add_string(wb, "module", rrdset_module_name(st)); - buffer_json_object_close(wb); - } - } - rrdset_foreach_done(st); - dictionary_destroy(dict); - - buffer_json_array_close(wb); -} - -extern int aclk_connected; -inline int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb) { - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - - buffer_json_member_add_string(wb, "version", rrdhost_program_version(host)); - buffer_json_member_add_string(wb, "uid", host->machine_guid); - - buffer_json_member_add_uint64(wb, "hosts-available", rrdhost_hosts_available()); - web_client_api_request_v1_info_mirrored_hosts(wb); - - web_client_api_request_v1_info_summary_alarm_statuses(host, wb, "alarms"); - - buffer_json_member_add_string_or_empty(wb, "os_name", host->system_info->host_os_name); - buffer_json_member_add_string_or_empty(wb, "os_id", host->system_info->host_os_id); - buffer_json_member_add_string_or_empty(wb, "os_id_like", host->system_info->host_os_id_like); - buffer_json_member_add_string_or_empty(wb, "os_version", host->system_info->host_os_version); - buffer_json_member_add_string_or_empty(wb, "os_version_id", host->system_info->host_os_version_id); - buffer_json_member_add_string_or_empty(wb, "os_detection", host->system_info->host_os_detection); - buffer_json_member_add_string_or_empty(wb, "cores_total", host->system_info->host_cores); - buffer_json_member_add_string_or_empty(wb, "total_disk_space", host->system_info->host_disk_space); - buffer_json_member_add_string_or_empty(wb, "cpu_freq", host->system_info->host_cpu_freq); - buffer_json_member_add_string_or_empty(wb, "ram_total", host->system_info->host_ram_total); - - buffer_json_member_add_string_or_omit(wb, "container_os_name", host->system_info->container_os_name); - buffer_json_member_add_string_or_omit(wb, "container_os_id", host->system_info->container_os_id); - buffer_json_member_add_string_or_omit(wb, "container_os_id_like", host->system_info->container_os_id_like); - buffer_json_member_add_string_or_omit(wb, "container_os_version", host->system_info->container_os_version); - buffer_json_member_add_string_or_omit(wb, "container_os_version_id", host->system_info->container_os_version_id); - buffer_json_member_add_string_or_omit(wb, "container_os_detection", host->system_info->container_os_detection); - buffer_json_member_add_string_or_omit(wb, "is_k8s_node", host->system_info->is_k8s_node); - - buffer_json_member_add_string_or_empty(wb, "kernel_name", host->system_info->kernel_name); - buffer_json_member_add_string_or_empty(wb, "kernel_version", host->system_info->kernel_version); - buffer_json_member_add_string_or_empty(wb, "architecture", host->system_info->architecture); - buffer_json_member_add_string_or_empty(wb, "virtualization", host->system_info->virtualization); - buffer_json_member_add_string_or_empty(wb, "virt_detection", host->system_info->virt_detection); - buffer_json_member_add_string_or_empty(wb, "container", host->system_info->container); - buffer_json_member_add_string_or_empty(wb, "container_detection", host->system_info->container_detection); - - buffer_json_member_add_string_or_omit(wb, "cloud_provider_type", host->system_info->cloud_provider_type); - buffer_json_member_add_string_or_omit(wb, "cloud_instance_type", host->system_info->cloud_instance_type); - buffer_json_member_add_string_or_omit(wb, "cloud_instance_region", host->system_info->cloud_instance_region); - - host_labels2json(host, wb, "host_labels"); - host_functions2json(host, wb); - host_collectors(host, wb); - - buffer_json_member_add_boolean(wb, "cloud-enabled", netdata_cloud_enabled); - -#ifdef ENABLE_ACLK - buffer_json_member_add_boolean(wb, "cloud-available", true); -#else - buffer_json_member_add_boolean(wb, "cloud-available", false); -#endif - - char *agent_id = get_agent_claimid(); - buffer_json_member_add_boolean(wb, "agent-claimed", agent_id != NULL); - freez(agent_id); - -#ifdef ENABLE_ACLK - buffer_json_member_add_boolean(wb, "aclk-available", aclk_connected); -#else - buffer_json_member_add_boolean(wb, "aclk-available", false); -#endif - - buffer_json_member_add_string(wb, "memory-mode", rrd_memory_mode_name(host->rrd_memory_mode)); -#ifdef ENABLE_DBENGINE - buffer_json_member_add_uint64(wb, "multidb-disk-quota", default_multidb_disk_quota_mb); - buffer_json_member_add_uint64(wb, "page-cache-size", default_rrdeng_page_cache_mb); -#endif // ENABLE_DBENGINE - buffer_json_member_add_boolean(wb, "web-enabled", web_server_mode != WEB_SERVER_MODE_NONE); - buffer_json_member_add_boolean(wb, "stream-enabled", default_rrdpush_enabled); - - buffer_json_member_add_boolean(wb, "stream-compression", - host->sender && host->sender->compressor.initialized); - -#ifdef ENABLE_HTTPS - buffer_json_member_add_boolean(wb, "https-enabled", true); -#else - buffer_json_member_add_boolean(wb, "https-enabled", false); -#endif - - buffer_json_member_add_quoted_string(wb, "buildinfo", analytics_data.netdata_buildinfo); - buffer_json_member_add_quoted_string(wb, "release-channel", analytics_data.netdata_config_release_channel); - buffer_json_member_add_quoted_string(wb, "notification-methods", analytics_data.netdata_notification_methods); - - buffer_json_member_add_boolean(wb, "exporting-enabled", analytics_data.exporting_enabled); - buffer_json_member_add_quoted_string(wb, "exporting-connectors", analytics_data.netdata_exporting_connectors); - - buffer_json_member_add_uint64(wb, "allmetrics-prometheus-used", analytics_data.prometheus_hits); - buffer_json_member_add_uint64(wb, "allmetrics-shell-used", analytics_data.shell_hits); - buffer_json_member_add_uint64(wb, "allmetrics-json-used", analytics_data.json_hits); - buffer_json_member_add_uint64(wb, "dashboard-used", analytics_data.dashboard_hits); - - buffer_json_member_add_uint64(wb, "charts-count", analytics_data.charts_count); - buffer_json_member_add_uint64(wb, "metrics-count", analytics_data.metrics_count); - -#if defined(ENABLE_ML) - buffer_json_member_add_object(wb, "ml-info"); - ml_host_get_info(host, wb); - buffer_json_object_close(wb); -#endif - - buffer_json_finalize(wb); - return 0; -} - -#if defined(ENABLE_ML) -int web_client_api_request_v1_ml_info(RRDHOST *host, struct web_client *w, char *url) { - (void) url; - - if (!netdata_ready) - return HTTP_RESP_SERVICE_UNAVAILABLE; - - BUFFER *wb = w->response.data; - buffer_flush(wb); - wb->content_type = CT_APPLICATION_JSON; - - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - ml_host_get_detection_info(host, wb); - buffer_json_finalize(wb); - - buffer_no_cacheable(wb); - - return HTTP_RESP_OK; -} -#endif // ENABLE_ML - -inline int web_client_api_request_v1_info(RRDHOST *host, struct web_client *w, char *url) { - (void)url; - if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; - BUFFER *wb = w->response.data; - buffer_flush(wb); - wb->content_type = CT_APPLICATION_JSON; - - web_client_api_request_v1_info_fill_buffer(host, wb); - - buffer_no_cacheable(wb); - return HTTP_RESP_OK; -} - -static int web_client_api_request_v1_aclk_state(RRDHOST *host, struct web_client *w, char *url) { - UNUSED(url); - UNUSED(host); - if (!netdata_ready) return HTTP_RESP_SERVICE_UNAVAILABLE; - - BUFFER *wb = w->response.data; - buffer_flush(wb); -#ifdef ENABLE_ACLK - char *str = aclk_state_json(); - buffer_strcat(wb, str); - freez(str); -#else - buffer_strcat(wb, "{\"aclk-available\":false}"); -#endif - - wb->content_type = CT_APPLICATION_JSON; - buffer_no_cacheable(wb); - return HTTP_RESP_OK; -} - -int web_client_api_request_v1_metric_correlations(RRDHOST *host, struct web_client *w, char *url) { - return web_client_api_request_weights(host, w, url, default_metric_correlations_method, WEIGHTS_FORMAT_CHARTS, 1); -} - -int web_client_api_request_v1_weights(RRDHOST *host, struct web_client *w, char *url) { - return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_ANOMALY_RATE, WEIGHTS_FORMAT_CONTEXTS, 1); -} - -static void web_client_progress_functions_update(void *data, size_t done, size_t all) { - // handle progress updates from the plugin - struct web_client *w = data; - query_progress_functions_update(&w->transaction, done, all); -} - -int web_client_api_request_v1_function(RRDHOST *host, struct web_client *w, char *url) { - if (!netdata_ready) - return HTTP_RESP_SERVICE_UNAVAILABLE; - - int timeout = 0; - const char *function = NULL; - - while (url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) - continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) - continue; - - if (!strcmp(name, "function")) - function = value; - - else if (!strcmp(name, "timeout")) - timeout = (int) strtoul(value, NULL, 0); - } - - BUFFER *wb = w->response.data; - buffer_flush(wb); - wb->content_type = CT_APPLICATION_JSON; - buffer_no_cacheable(wb); - - char transaction[UUID_COMPACT_STR_LEN]; - uuid_unparse_lower_compact(w->transaction, transaction); - - CLEAN_BUFFER *source = buffer_create(100, NULL); - web_client_source2buffer(w, source); - - return rrd_function_run(host, wb, timeout, w->access, function, true, transaction, - NULL, NULL, - web_client_progress_functions_update, w, - web_client_interrupt_callback, w, NULL, - buffer_tostring(source)); -} - -int web_client_api_request_v1_functions(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { - if (!netdata_ready) - return HTTP_RESP_SERVICE_UNAVAILABLE; - - BUFFER *wb = w->response.data; - buffer_flush(wb); - wb->content_type = CT_APPLICATION_JSON; - buffer_no_cacheable(wb); - - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - host_functions2json(host, wb); - buffer_json_finalize(wb); - - return HTTP_RESP_OK; -} - -void web_client_source2buffer(struct web_client *w, BUFFER *source) { - if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_CLOUD)) - buffer_sprintf(source, "method=NC"); - else if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_BEARER)) - buffer_sprintf(source, "method=api-bearer"); - else - buffer_sprintf(source, "method=api"); - - if(web_client_flag_check(w, WEB_CLIENT_FLAG_AUTH_GOD)) - buffer_strcat(source, ",role=god"); - else - buffer_sprintf(source, ",role=%s", http_id2user_role(w->user_role)); - - buffer_sprintf(source, ",permissions="HTTP_ACCESS_FORMAT, (HTTP_ACCESS_FORMAT_CAST)w->access); - - if(w->auth.client_name[0]) - buffer_sprintf(source, ",user=%s", w->auth.client_name); - - if(!uuid_is_null(w->auth.cloud_account_id)) { - char uuid_str[UUID_COMPACT_STR_LEN]; - uuid_unparse_lower_compact(w->auth.cloud_account_id, uuid_str); - buffer_sprintf(source, ",account=%s", uuid_str); - } - - if(w->client_ip[0]) - buffer_sprintf(source, ",ip=%s", w->client_ip); - - if(w->forwarded_for) - buffer_sprintf(source, ",forwarded_for=%s", w->forwarded_for); -} - -static int web_client_api_request_v1_config(RRDHOST *host, struct web_client *w, char *url __maybe_unused) { - char *action = "tree"; - char *path = "/"; - char *id = NULL; - char *add_name = NULL; - int timeout = 120; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "action")) - action = value; - else if(!strcmp(name, "path")) - path = value; - else if(!strcmp(name, "id")) - id = value; - else if(!strcmp(name, "name")) - add_name = value; - else if(!strcmp(name, "timeout")) { - timeout = (int)strtol(value, NULL, 10); - if(timeout < 10) - timeout = 10; - } - } - - char transaction[UUID_COMPACT_STR_LEN]; - uuid_unparse_lower_compact(w->transaction, transaction); - - size_t len = strlen(action) + (id ? strlen(id) : 0) + strlen(path) + (add_name ? strlen(add_name) : 0) + 100; - - char cmd[len]; - if(strcmp(action, "tree") == 0) - snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " tree '%s' '%s'", path, id?id:""); - else { - DYNCFG_CMDS c = dyncfg_cmds2id(action); - if(!id || !*id || !dyncfg_is_valid_id(id)) { - rrd_call_function_error(w->response.data, "invalid id given", HTTP_RESP_BAD_REQUEST); - return HTTP_RESP_BAD_REQUEST; - } - - if(c == DYNCFG_CMD_NONE) { - rrd_call_function_error(w->response.data, "invalid action given", HTTP_RESP_BAD_REQUEST); - return HTTP_RESP_BAD_REQUEST; - } - - if(c == DYNCFG_CMD_ADD || c == DYNCFG_CMD_USERCONFIG || c == DYNCFG_CMD_TEST) { - if(c == DYNCFG_CMD_TEST && (!add_name || !*add_name)) { - // backwards compatibility for TEST without a name - char *colon = strrchr(id, ':'); - if(colon) { - *colon = '\0'; - add_name = ++colon; - } - else - add_name = "test"; - } - - if(!add_name || !*add_name || !dyncfg_is_valid_id(add_name)) { - rrd_call_function_error(w->response.data, "invalid name given", HTTP_RESP_BAD_REQUEST); - return HTTP_RESP_BAD_REQUEST; - } - snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s %s", id, dyncfg_id2cmd_one(c), add_name); - } - else - snprintfz(cmd, sizeof(cmd), PLUGINSD_FUNCTION_CONFIG " %s %s", id, dyncfg_id2cmd_one(c)); - } - - CLEAN_BUFFER *source = buffer_create(100, NULL); - web_client_source2buffer(w, source); - - buffer_flush(w->response.data); - int code = rrd_function_run(host, w->response.data, timeout, w->access, cmd, - true, transaction, - NULL, NULL, - web_client_progress_functions_update, w, - web_client_interrupt_callback, w, - w->payload, buffer_tostring(source)); - - return code; -} - -#ifndef ENABLE_DBENGINE -int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url __maybe_unused) { - return HTTP_RESP_NOT_FOUND; -} -#else -static void web_client_api_v1_dbengine_stats_for_tier(BUFFER *wb, size_t tier) { - RRDENG_SIZE_STATS stats = rrdeng_size_statistics(multidb_ctx[tier]); - - buffer_sprintf(wb, - "\n\t\t\"default_granularity_secs\":%zu" - ",\n\t\t\"sizeof_datafile\":%zu" - ",\n\t\t\"sizeof_page_in_cache\":%zu" - ",\n\t\t\"sizeof_point_data\":%zu" - ",\n\t\t\"sizeof_page_data\":%zu" - ",\n\t\t\"pages_per_extent\":%zu" - ",\n\t\t\"datafiles\":%zu" - ",\n\t\t\"extents\":%zu" - ",\n\t\t\"extents_pages\":%zu" - ",\n\t\t\"points\":%zu" - ",\n\t\t\"metrics\":%zu" - ",\n\t\t\"metrics_pages\":%zu" - ",\n\t\t\"extents_compressed_bytes\":%zu" - ",\n\t\t\"pages_uncompressed_bytes\":%zu" - ",\n\t\t\"pages_duration_secs\":%lld" - ",\n\t\t\"single_point_pages\":%zu" - ",\n\t\t\"first_t\":%ld" - ",\n\t\t\"last_t\":%ld" - ",\n\t\t\"database_retention_secs\":%lld" - ",\n\t\t\"average_compression_savings\":%0.2f" - ",\n\t\t\"average_point_duration_secs\":%0.2f" - ",\n\t\t\"average_metric_retention_secs\":%0.2f" - ",\n\t\t\"ephemeral_metrics_per_day_percent\":%0.2f" - ",\n\t\t\"average_page_size_bytes\":%0.2f" - ",\n\t\t\"estimated_concurrently_collected_metrics\":%zu" - ",\n\t\t\"currently_collected_metrics\":%zu" - ",\n\t\t\"disk_space\":%zu" - ",\n\t\t\"max_disk_space\":%zu" - , stats.default_granularity_secs - , stats.sizeof_datafile - , stats.sizeof_page_in_cache - , stats.sizeof_point_data - , stats.sizeof_page_data - , stats.pages_per_extent - , stats.datafiles - , stats.extents - , stats.extents_pages - , stats.points - , stats.metrics - , stats.metrics_pages - , stats.extents_compressed_bytes - , stats.pages_uncompressed_bytes - , (long long)stats.pages_duration_secs - , stats.single_point_pages - , stats.first_time_s - , stats.last_time_s - , (long long)stats.database_retention_secs - , stats.average_compression_savings - , stats.average_point_duration_secs - , stats.average_metric_retention_secs - , stats.ephemeral_metrics_per_day_percent - , stats.average_page_size_bytes - , stats.estimated_concurrently_collected_metrics - , stats.currently_collected_metrics - , stats.disk_space - , stats.max_disk_space - ); -} -int web_client_api_request_v1_dbengine_stats(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { - if (!netdata_ready) - return HTTP_RESP_SERVICE_UNAVAILABLE; - - BUFFER *wb = w->response.data; - buffer_flush(wb); - - if(!dbengine_enabled) { - buffer_strcat(wb, "dbengine is not enabled"); - return HTTP_RESP_NOT_FOUND; - } - - wb->content_type = CT_APPLICATION_JSON; - buffer_no_cacheable(wb); - buffer_strcat(wb, "{"); - for(size_t tier = 0; tier < storage_tiers ;tier++) { - buffer_sprintf(wb, "%s\n\t\"tier%zu\": {", tier?",":"", tier); - web_client_api_v1_dbengine_stats_for_tier(wb, tier); - buffer_strcat(wb, "\n\t}"); - } - buffer_strcat(wb, "\n}"); - - return HTTP_RESP_OK; -} -#endif - -#define HLT_MGM "manage/health" -int web_client_api_request_v1_mgmt(RRDHOST *host, struct web_client *w, char *url) { - const char *haystack = buffer_tostring(w->url_path_decoded); - char *needle; - - buffer_flush(w->response.data); - - if ((needle = strstr(haystack, HLT_MGM)) == NULL) { - buffer_strcat(w->response.data, "Invalid management request. Curently only 'health' is supported."); - return HTTP_RESP_NOT_FOUND; - } - needle += strlen(HLT_MGM); - if (*needle != '\0') { - buffer_strcat(w->response.data, "Invalid management request. Currently only 'health' is supported."); - return HTTP_RESP_NOT_FOUND; - } - return web_client_api_request_v1_mgmt_health(host, w, url); -} +#include "v1/api_v1_calls.h" +#include "v2/api_v2_calls.h" +#include "v3/api_v3_calls.h" static struct web_api_command api_commands_v1[] = { // time-series data APIs { .api = "data", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_data, + .callback = api_v1_data, .allow_subpaths = 0 }, +#if defined(ENABLE_API_V1) { .api = "weights", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_weights, + .callback = api_v1_weights, .allow_subpaths = 0 }, { // deprecated - do not use anymore - use "weights" .api = "metric_correlations", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_metric_correlations, + .callback = api_v1_metric_correlations, .allow_subpaths = 0 }, +#endif { - // exporting API - .api = "allmetrics", + .api = "badge.svg", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_BADGES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_allmetrics, + .callback = api_v1_badge, .allow_subpaths = 0 }, { - // badges can be fetched with both dashboard and badge ACL - .api = "badge.svg", + // exporting API + .api = "allmetrics", .hash = 0, - .acl = HTTP_ACL_BADGES, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_badge, + .callback = api_v1_allmetrics, .allow_subpaths = 0 }, // alerts APIs +#if defined(ENABLE_API_V1) { .api = "alarms", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_alarms, + .callback = api_v1_alarms, .allow_subpaths = 0 }, { .api = "alarms_values", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_alarms_values, + .callback = api_v1_alarms_values, .allow_subpaths = 0 }, { .api = "alarm_log", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_alarm_log, + .callback = api_v1_alarm_log, .allow_subpaths = 0 }, { .api = "alarm_variables", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_alarm_variables, + .callback = api_v1_alarm_variables, .allow_subpaths = 0 }, { .api = "variable", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_variable, + .callback = api_v1_variable, .allow_subpaths = 0 }, { .api = "alarm_count", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_alarm_count, + .callback = api_v1_alarm_count, .allow_subpaths = 0 }, +#endif // functions APIs - they check permissions per function call { .api = "function", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_FUNCTIONS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_function, + .callback = api_v1_function, .allow_subpaths = 0 }, + +#if defined(ENABLE_API_V1) { .api = "functions", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_FUNCTIONS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_functions, + .callback = api_v1_functions, .allow_subpaths = 0 }, +#endif // time-series metadata APIs +#if defined(ENABLE_API_V1) { .api = "chart", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_chart, + .callback = api_v1_chart, .allow_subpaths = 0 }, +#endif { .api = "charts", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_charts, + .callback = api_v1_charts, .allow_subpaths = 0 }, { .api = "context", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_context, + .callback = api_v1_context, .allow_subpaths = 0 }, { .api = "contexts", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_contexts, + .callback = api_v1_contexts, .allow_subpaths = 0 }, // registry APIs +#if defined(ENABLE_API_V1) { // registry checks the ACL by itself, so we allow everything .api = "registry", .hash = 0, .acl = HTTP_ACL_NONE, // it manages acl by itself .access = HTTP_ACCESS_NONE, // it manages access by itself - .callback = web_client_api_request_v1_registry, + .callback = api_v1_registry, .allow_subpaths = 0 }, +#endif // agent information APIs +#if defined(ENABLE_API_V1) { .api = "info", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_info, + .callback = api_v1_info, .allow_subpaths = 0 }, { .api = "aclk", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_aclk_state, + .callback = api_v1_aclk, .allow_subpaths = 0 }, { // deprecated - use /api/v2/info .api = "dbengine_stats", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_dbengine_stats, + .callback = api_v1_dbengine_stats, .allow_subpaths = 0 }, - - // dyncfg APIs { - .api = "config", + .api = "ml_info", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_config, + .callback = api_v1_ml_info, .allow_subpaths = 0 }, - -#if defined(ENABLE_ML) { - .api = "ml_info", - .hash = 0, - .acl = HTTP_ACL_DASHBOARD, - .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v1_ml_info, - .allow_subpaths = 0 + .api = "manage", + .hash = 0, + .acl = HTTP_ACL_MANAGEMENT, + .access = HTTP_ACCESS_NONE, // it manages access by itself + .callback = api_v1_manage, + .allow_subpaths = 1 }, #endif + // dyncfg APIs { - // deprecated - .api = "manage", + .api = "config", .hash = 0, - .acl = HTTP_ACL_MANAGEMENT, - .access = HTTP_ACCESS_NONE, // it manages access by itself - .callback = web_client_api_request_v1_mgmt, - .allow_subpaths = 1 + .acl = HTTP_ACL_DYNCFG, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_config, + .allow_subpaths = 0 }, { diff --git a/src/web/api/web_api_v1.h b/src/web/api/web_api_v1.h index cf0efbd1..c102ac75 100644 --- a/src/web/api/web_api_v1.h +++ b/src/web/api/web_api_v1.h @@ -5,43 +5,7 @@ #include "web_api.h" -struct web_client; - -CONTEXTS_V2_OPTIONS web_client_api_request_v2_context_options(char *o); -CONTEXTS_V2_ALERT_STATUS web_client_api_request_v2_alert_status(char *o); -void web_client_api_request_v2_contexts_options_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_OPTIONS options); -void web_client_api_request_v2_contexts_alerts_status_to_buffer_json_array(BUFFER *wb, const char *key, CONTEXTS_V2_ALERT_STATUS options); - -RRDR_OPTIONS rrdr_options_parse(char *o); -RRDR_OPTIONS rrdr_options_parse_one(const char *o); - -void rrdr_options_to_buffer(BUFFER *wb, RRDR_OPTIONS options); -void rrdr_options_to_buffer_json_array(BUFFER *wb, const char *key, RRDR_OPTIONS options); -void web_client_api_request_v1_data_options_to_string(char *buf, size_t size, RRDR_OPTIONS options); - -uint32_t web_client_api_request_v1_data_format(char *name); -uint32_t web_client_api_request_v1_data_google_format(char *name); - -int web_client_api_request_v1_alarms(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_alarms_values(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_alarm_log(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_single_chart(RRDHOST *host, struct web_client *w, char *url, void callback(RRDSET *st, BUFFER *buf)); -int web_client_api_request_v1_alarm_variables(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_alarm_count(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_charts(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_chart(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_registry(RRDHOST *host, struct web_client *w, char *url); -int web_client_api_request_v1_info(RRDHOST *host, struct web_client *w, char *url); int web_client_api_request_v1(RRDHOST *host, struct web_client *w, char *url_path_endpoint); -int web_client_api_request_v1_info_fill_buffer(RRDHOST *host, BUFFER *wb); - -void web_client_api_v1_init(void); -void web_client_api_v1_management_init(void); - -void host_labels2json(RRDHOST *host, BUFFER *wb, const char *key); -void web_client_api_request_v1_info_summary_alarm_statuses(RRDHOST *host, BUFFER *wb, const char *key); - -void web_client_source2buffer(struct web_client *w, BUFFER *source); extern char *api_secret; diff --git a/src/web/api/web_api_v2.c b/src/web/api/web_api_v2.c index c62ed9ed..f8e52a94 100644 --- a/src/web/api/web_api_v2.c +++ b/src/web/api/web_api_v2.c @@ -1,604 +1,27 @@ // SPDX-License-Identifier: GPL-3.0-or-later #include "web_api_v2.h" -#include "../rtc/webrtc.h" - -static bool verify_agent_uuids(const char *machine_guid, const char *node_id, const char *claim_id) { - if(!machine_guid || !node_id || !claim_id) - return false; - - if(strcmp(machine_guid, localhost->machine_guid) != 0) - return false; - - char *agent_claim_id = get_agent_claimid(); - - bool not_verified = (!agent_claim_id || strcmp(claim_id, agent_claim_id) != 0); - freez(agent_claim_id); - - if(not_verified || !localhost->node_id) - return false; - - char buf[UUID_STR_LEN]; - uuid_unparse_lower(*localhost->node_id, buf); - - if(strcmp(node_id, buf) != 0) - return false; - - return true; -} - -int api_v2_bearer_protection(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url) { - char *machine_guid = NULL; - char *claim_id = NULL; - char *node_id = NULL; - bool protection = netdata_is_protected_by_bearer; - - while (url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) continue; - if (!value || !*value) continue; - - if(!strcmp(name, "bearer_protection")) { - if(!strcmp(value, "on") || !strcmp(value, "true") || !strcmp(value, "yes")) - protection = true; - else - protection = false; - } - else if(!strcmp(name, "machine_guid")) - machine_guid = value; - else if(!strcmp(name, "claim_id")) - claim_id = value; - else if(!strcmp(name, "node_id")) - node_id = value; - } - - if(!verify_agent_uuids(machine_guid, node_id, claim_id)) { - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); - return HTTP_RESP_BAD_REQUEST; - } - - netdata_is_protected_by_bearer = protection; - - BUFFER *wb = w->response.data; - buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); - buffer_json_finalize(wb); - - return HTTP_RESP_OK; -} - -int api_v2_bearer_token(RRDHOST *host __maybe_unused, struct web_client *w __maybe_unused, char *url __maybe_unused) { - char *machine_guid = NULL; - char *claim_id = NULL; - char *node_id = NULL; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if (!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if (!name || !*name) continue; - if (!value || !*value) continue; - - if(!strcmp(name, "machine_guid")) - machine_guid = value; - else if(!strcmp(name, "claim_id")) - claim_id = value; - else if(!strcmp(name, "node_id")) - node_id = value; - } - - if(!verify_agent_uuids(machine_guid, node_id, claim_id)) { - buffer_flush(w->response.data); - buffer_strcat(w->response.data, "The request is missing or not matching local UUIDs"); - return HTTP_RESP_BAD_REQUEST; - } - - nd_uuid_t uuid; - time_t expires_s = bearer_create_token(&uuid, w); - - BUFFER *wb = w->response.data; - buffer_flush(wb); - buffer_json_initialize(wb, "\"", "\"", 0, true, BUFFER_JSON_OPTIONS_DEFAULT); - buffer_json_member_add_string(wb, "mg", localhost->machine_guid); - buffer_json_member_add_boolean(wb, "bearer_protection", netdata_is_protected_by_bearer); - buffer_json_member_add_uuid(wb, "token", &uuid); - buffer_json_member_add_time_t(wb, "expiration", expires_s); - buffer_json_finalize(wb); - - return HTTP_RESP_OK; -} - -static int web_client_api_request_v2_contexts_internal(RRDHOST *host __maybe_unused, struct web_client *w, char *url, CONTEXTS_V2_MODE mode) { - struct api_v2_contexts_request req = { 0 }; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "scope_nodes")) - req.scope_nodes = value; - else if(!strcmp(name, "nodes")) - req.nodes = value; - else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "scope_contexts")) - req.scope_contexts = value; - else if((mode & (CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_SEARCH | CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) && !strcmp(name, "contexts")) - req.contexts = value; - else if((mode & CONTEXTS_V2_SEARCH) && !strcmp(name, "q")) - req.q = value; - else if(!strcmp(name, "options")) - req.options = web_client_api_request_v2_context_options(value); - else if(!strcmp(name, "after")) - req.after = str2l(value); - else if(!strcmp(name, "before")) - req.before = str2l(value); - else if(!strcmp(name, "timeout")) - req.timeout_ms = str2l(value); - else if(mode & (CONTEXTS_V2_ALERTS | CONTEXTS_V2_ALERT_TRANSITIONS)) { - if (!strcmp(name, "alert")) - req.alerts.alert = value; - else if (!strcmp(name, "transition")) - req.alerts.transition = value; - else if(mode & CONTEXTS_V2_ALERTS) { - if (!strcmp(name, "status")) - req.alerts.status = web_client_api_request_v2_alert_status(value); - } - else if(mode & CONTEXTS_V2_ALERT_TRANSITIONS) { - if (!strcmp(name, "last")) - req.alerts.last = strtoul(value, NULL, 0); - else if(!strcmp(name, "context")) - req.contexts = value; - else if (!strcmp(name, "anchor_gi")) { - req.alerts.global_id_anchor = str2ull(value, NULL); - } - else { - for(int i = 0; i < ATF_TOTAL_ENTRIES ;i++) { - if(!strcmp(name, alert_transition_facets[i].query_param)) - req.alerts.facets[i] = value; - } - } - } - } - } - - if ((mode & CONTEXTS_V2_ALERT_TRANSITIONS) && !req.alerts.last) - req.alerts.last = 1; - - buffer_flush(w->response.data); - buffer_no_cacheable(w->response.data); - return rrdcontext_to_json_v2(w->response.data, &req, mode); -} - -static int web_client_api_request_v2_alert_transitions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERT_TRANSITIONS | CONTEXTS_V2_NODES); -} - -static int web_client_api_request_v2_alerts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_ALERTS | CONTEXTS_V2_NODES); -} - -static int web_client_api_request_v2_functions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_FUNCTIONS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); -} - -static int web_client_api_request_v2_versions(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_VERSIONS); -} - -static int web_client_api_request_v2_q(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_SEARCH | CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); -} - -static int web_client_api_request_v2_contexts(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_CONTEXTS | CONTEXTS_V2_NODES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_VERSIONS); -} - -static int web_client_api_request_v2_nodes(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_NODES | CONTEXTS_V2_NODES_INFO); -} - -static int web_client_api_request_v2_info(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_AGENTS | CONTEXTS_V2_AGENTS_INFO); -} - -static int web_client_api_request_v2_node_instances(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_v2_contexts_internal(host, w, url, CONTEXTS_V2_NODES | CONTEXTS_V2_NODE_INSTANCES | CONTEXTS_V2_AGENTS | CONTEXTS_V2_AGENTS_INFO | CONTEXTS_V2_VERSIONS); -} - -static int web_client_api_request_v2_weights(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return web_client_api_request_weights(host, w, url, WEIGHTS_METHOD_VALUE, WEIGHTS_FORMAT_MULTINODE, 2); -} - -static int web_client_api_request_v2_claim(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - return api_v2_claim(w, url); -} - -static int web_client_api_request_v2_alert_config(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - const char *config = NULL; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "config")) - config = value; - } - - buffer_flush(w->response.data); - - if(!config) { - w->response.data->content_type = CT_TEXT_PLAIN; - buffer_strcat(w->response.data, "A config hash ID is required. Add ?config=UUID query param"); - return HTTP_RESP_BAD_REQUEST; - } - - return contexts_v2_alert_config_to_json(w, config); -} - - -#define GROUP_BY_KEY_MAX_LENGTH 30 -static struct { - char group_by[GROUP_BY_KEY_MAX_LENGTH + 1]; - char aggregation[GROUP_BY_KEY_MAX_LENGTH + 1]; - char group_by_label[GROUP_BY_KEY_MAX_LENGTH + 1]; -} group_by_keys[MAX_QUERY_GROUP_BY_PASSES]; - -__attribute__((constructor)) void initialize_group_by_keys(void) { - for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { - snprintfz(group_by_keys[g].group_by, GROUP_BY_KEY_MAX_LENGTH, "group_by[%zu]", g); - snprintfz(group_by_keys[g].aggregation, GROUP_BY_KEY_MAX_LENGTH, "aggregation[%zu]", g); - snprintfz(group_by_keys[g].group_by_label, GROUP_BY_KEY_MAX_LENGTH, "group_by_label[%zu]", g); - } -} - -static int web_client_api_request_v2_data(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - usec_t received_ut = now_monotonic_usec(); - - int ret = HTTP_RESP_BAD_REQUEST; - - buffer_flush(w->response.data); - - char *google_version = "0.6", - *google_reqId = "0", - *google_sig = "0", - *google_out = "json", - *responseHandler = NULL, - *outFileName = NULL; - - time_t last_timestamp_in_data = 0, google_timestamp = 0; - - char *scope_nodes = NULL; - char *scope_contexts = NULL; - char *nodes = NULL; - char *contexts = NULL; - char *instances = NULL; - char *dimensions = NULL; - char *before_str = NULL; - char *after_str = NULL; - char *resampling_time_str = NULL; - char *points_str = NULL; - char *timeout_str = NULL; - char *labels = NULL; - char *alerts = NULL; - char *time_group_options = NULL; - char *tier_str = NULL; - size_t tier = 0; - RRDR_TIME_GROUPING time_group = RRDR_GROUPING_AVERAGE; - DATASOURCE_FORMAT format = DATASOURCE_JSON2; - RRDR_OPTIONS options = RRDR_OPTION_VIRTUAL_POINTS | RRDR_OPTION_JSON_WRAP | RRDR_OPTION_RETURN_JWAR; - - struct group_by_pass group_by[MAX_QUERY_GROUP_BY_PASSES] = { - { - .group_by = RRDR_GROUP_BY_DIMENSION, - .group_by_label = NULL, - .aggregation = RRDR_GROUP_BY_FUNCTION_AVERAGE, - }, - }; - - size_t group_by_idx = 0, group_by_label_idx = 0, aggregation_idx = 0; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "scope_nodes")) scope_nodes = value; - else if(!strcmp(name, "scope_contexts")) scope_contexts = value; - else if(!strcmp(name, "nodes")) nodes = value; - else if(!strcmp(name, "contexts")) contexts = value; - else if(!strcmp(name, "instances")) instances = value; - else if(!strcmp(name, "dimensions")) dimensions = value; - else if(!strcmp(name, "labels")) labels = value; - else if(!strcmp(name, "alerts")) alerts = value; - else if(!strcmp(name, "after")) after_str = value; - else if(!strcmp(name, "before")) before_str = value; - else if(!strcmp(name, "points")) points_str = value; - else if(!strcmp(name, "timeout")) timeout_str = value; - else if(!strcmp(name, "group_by")) { - group_by[group_by_idx++].group_by = group_by_parse(value); - if(group_by_idx >= MAX_QUERY_GROUP_BY_PASSES) - group_by_idx = MAX_QUERY_GROUP_BY_PASSES - 1; - } - else if(!strcmp(name, "group_by_label")) { - group_by[group_by_label_idx++].group_by_label = value; - if(group_by_label_idx >= MAX_QUERY_GROUP_BY_PASSES) - group_by_label_idx = MAX_QUERY_GROUP_BY_PASSES - 1; - } - else if(!strcmp(name, "aggregation")) { - group_by[aggregation_idx++].aggregation = group_by_aggregate_function_parse(value); - if(aggregation_idx >= MAX_QUERY_GROUP_BY_PASSES) - aggregation_idx = MAX_QUERY_GROUP_BY_PASSES - 1; - } - else if(!strcmp(name, "format")) format = web_client_api_request_v1_data_format(value); - else if(!strcmp(name, "options")) options |= rrdr_options_parse(value); - else if(!strcmp(name, "time_group")) time_group = time_grouping_parse(value, RRDR_GROUPING_AVERAGE); - else if(!strcmp(name, "time_group_options")) time_group_options = value; - else if(!strcmp(name, "time_resampling")) resampling_time_str = value; - else if(!strcmp(name, "tier")) tier_str = value; - else if(!strcmp(name, "callback")) responseHandler = value; - else if(!strcmp(name, "filename")) outFileName = value; - else if(!strcmp(name, "tqx")) { - // parse Google Visualization API options - // https://developers.google.com/chart/interactive/docs/dev/implementing_data_source - char *tqx_name, *tqx_value; - - while(value) { - tqx_value = strsep_skip_consecutive_separators(&value, ";"); - if(!tqx_value || !*tqx_value) continue; - - tqx_name = strsep_skip_consecutive_separators(&tqx_value, ":"); - if(!tqx_name || !*tqx_name) continue; - if(!tqx_value || !*tqx_value) continue; - - if(!strcmp(tqx_name, "version")) - google_version = tqx_value; - else if(!strcmp(tqx_name, "reqId")) - google_reqId = tqx_value; - else if(!strcmp(tqx_name, "sig")) { - google_sig = tqx_value; - google_timestamp = strtoul(google_sig, NULL, 0); - } - else if(!strcmp(tqx_name, "out")) { - google_out = tqx_value; - format = web_client_api_request_v1_data_google_format(google_out); - } - else if(!strcmp(tqx_name, "responseHandler")) - responseHandler = tqx_value; - else if(!strcmp(tqx_name, "outFileName")) - outFileName = tqx_value; - } - } - else { - for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { - if(!strcmp(name, group_by_keys[g].group_by)) - group_by[g].group_by = group_by_parse(value); - else if(!strcmp(name, group_by_keys[g].group_by_label)) - group_by[g].group_by_label = value; - else if(!strcmp(name, group_by_keys[g].aggregation)) - group_by[g].aggregation = group_by_aggregate_function_parse(value); - } - } - } - - // validate the google parameters given - fix_google_param(google_out); - fix_google_param(google_sig); - fix_google_param(google_reqId); - fix_google_param(google_version); - fix_google_param(responseHandler); - fix_google_param(outFileName); - - for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { - if (group_by[g].group_by_label && *group_by[g].group_by_label) - group_by[g].group_by |= RRDR_GROUP_BY_LABEL; - } - - if(group_by[0].group_by == RRDR_GROUP_BY_NONE) - group_by[0].group_by = RRDR_GROUP_BY_DIMENSION; - - for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) { - if ((group_by[g].group_by & ~(RRDR_GROUP_BY_DIMENSION)) || (options & RRDR_OPTION_PERCENTAGE)) { - options |= RRDR_OPTION_ABSOLUTE; - break; - } - } - - if(options & RRDR_OPTION_DEBUG) - options &= ~RRDR_OPTION_MINIFY; - - if(tier_str && *tier_str) { - tier = str2ul(tier_str); - if(tier < storage_tiers) - options |= RRDR_OPTION_SELECTED_TIER; - else - tier = 0; - } - - time_t before = (before_str && *before_str)?str2l(before_str):0; - time_t after = (after_str && *after_str) ?str2l(after_str):-600; - size_t points = (points_str && *points_str)?str2u(points_str):0; - int timeout = (timeout_str && *timeout_str)?str2i(timeout_str): 0; - time_t resampling_time = (resampling_time_str && *resampling_time_str) ? str2l(resampling_time_str) : 0; - - QUERY_TARGET_REQUEST qtr = { - .version = 2, - .scope_nodes = scope_nodes, - .scope_contexts = scope_contexts, - .after = after, - .before = before, - .host = NULL, - .st = NULL, - .nodes = nodes, - .contexts = contexts, - .instances = instances, - .dimensions = dimensions, - .alerts = alerts, - .timeout_ms = timeout, - .points = points, - .format = format, - .options = options, - .time_group_method = time_group, - .time_group_options = time_group_options, - .resampling_time = resampling_time, - .tier = tier, - .chart_label_key = NULL, - .labels = labels, - .query_source = QUERY_SOURCE_API_DATA, - .priority = STORAGE_PRIORITY_NORMAL, - .received_ut = received_ut, - - .interrupt_callback = web_client_interrupt_callback, - .interrupt_callback_data = w, - - .transaction = &w->transaction, - }; - - for(size_t g = 0; g < MAX_QUERY_GROUP_BY_PASSES ;g++) - qtr.group_by[g] = group_by[g]; - - QUERY_TARGET *qt = query_target_create(&qtr); - ONEWAYALLOC *owa = NULL; - - if(!qt) { - buffer_sprintf(w->response.data, "Failed to prepare the query."); - ret = HTTP_RESP_INTERNAL_SERVER_ERROR; - goto cleanup; - } - - web_client_timeout_checkpoint_set(w, timeout); - if(web_client_timeout_checkpoint_and_check(w, NULL)) { - ret = w->response.code; - goto cleanup; - } - - if(outFileName && *outFileName) { - buffer_sprintf(w->response.header, "Content-Disposition: attachment; filename=\"%s\"\r\n", outFileName); - netdata_log_debug(D_WEB_CLIENT, "%llu: generating outfilename header: '%s'", w->id, outFileName); - } - - if(format == DATASOURCE_DATATABLE_JSONP) { - if(responseHandler == NULL) - responseHandler = "google.visualization.Query.setResponse"; - - netdata_log_debug(D_WEB_CLIENT_ACCESS, "%llu: GOOGLE JSON/JSONP: version = '%s', reqId = '%s', sig = '%s', out = '%s', responseHandler = '%s', outFileName = '%s'", - w->id, google_version, google_reqId, google_sig, google_out, responseHandler, outFileName - ); - - buffer_sprintf( - w->response.data, - "%s({version:'%s',reqId:'%s',status:'ok',sig:'%"PRId64"',table:", - responseHandler, - google_version, - google_reqId, - (int64_t)now_realtime_sec()); - } - else if(format == DATASOURCE_JSONP) { - if(responseHandler == NULL) - responseHandler = "callback"; - - buffer_strcat(w->response.data, responseHandler); - buffer_strcat(w->response.data, "("); - } - - owa = onewayalloc_create(0); - ret = data_query_execute(owa, w->response.data, qt, &last_timestamp_in_data); - - if(format == DATASOURCE_DATATABLE_JSONP) { - if(google_timestamp < last_timestamp_in_data) - buffer_strcat(w->response.data, "});"); - - else { - // the client already has the latest data - buffer_flush(w->response.data); - buffer_sprintf(w->response.data, - "%s({version:'%s',reqId:'%s',status:'error',errors:[{reason:'not_modified',message:'Data not modified'}]});", - responseHandler, google_version, google_reqId); - } - } - else if(format == DATASOURCE_JSONP) - buffer_strcat(w->response.data, ");"); - - if(qt->internal.relative) - buffer_no_cacheable(w->response.data); - else - buffer_cacheable(w->response.data); - -cleanup: - query_target_release(qt); - onewayalloc_destroy(owa); - return ret; -} - -static int web_client_api_request_v2_webrtc(RRDHOST *host __maybe_unused, struct web_client *w, char *url __maybe_unused) { - return webrtc_new_connection(buffer_tostring(w->payload), w->response.data); -} - -static int web_client_api_request_v2_progress(RRDHOST *host __maybe_unused, struct web_client *w, char *url) { - char *transaction = NULL; - - while(url) { - char *value = strsep_skip_consecutive_separators(&url, "&"); - if(!value || !*value) continue; - - char *name = strsep_skip_consecutive_separators(&value, "="); - if(!name || !*name) continue; - if(!value || !*value) continue; - - // name and value are now the parameters - // they are not null and not empty - - if(!strcmp(name, "transaction")) transaction = value; - } - - nd_uuid_t tr; - uuid_parse_flexi(transaction, tr); - - rrd_function_call_progresser(&tr); - - return web_api_v2_report_progress(&tr, w->response.data); -} +#include "v1/api_v1_calls.h" +#include "v2/api_v2_calls.h" +#include "v3/api_v3_calls.h" static struct web_api_command api_commands_v2[] = { +#if defined(ENABLE_API_v2) // time-series multi-node multi-instance data APIs { .api = "data", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_data, + .callback = api_v2_data, .allow_subpaths = 0 }, { .api = "weights", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_weights, + .callback = api_v2_weights, .allow_subpaths = 0 }, @@ -606,18 +29,18 @@ static struct web_api_command api_commands_v2[] = { { .api = "contexts", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_contexts, + .callback = api_v2_contexts, .allow_subpaths = 0 }, { // full text search .api = "q", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_METRICS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_q, + .callback = api_v2_q, .allow_subpaths = 0 }, @@ -625,25 +48,25 @@ static struct web_api_command api_commands_v2[] = { { .api = "alerts", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_alerts, + .callback = api_v2_alerts, .allow_subpaths = 0 }, { .api = "alert_transitions", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_alert_transitions, + .callback = api_v2_alert_transitions, .allow_subpaths = 0 }, { .api = "alert_config", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_ALERTS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_alert_config, + .callback = api_v2_alert_config, .allow_subpaths = 0 }, @@ -651,41 +74,41 @@ static struct web_api_command api_commands_v2[] = { { .api = "info", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NOCHECK, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_info, + .callback = api_v2_info, .allow_subpaths = 0 }, { .api = "nodes", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_nodes, + .callback = api_v2_nodes, .allow_subpaths = 0 }, { .api = "node_instances", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_node_instances, + .callback = api_v2_node_instances, .allow_subpaths = 0 }, { .api = "versions", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NODES, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_versions, + .callback = api_v2_versions, .allow_subpaths = 0 }, { .api = "progress", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NOCHECK, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_progress, + .callback = api_v2_progress, .allow_subpaths = 0 }, @@ -693,9 +116,9 @@ static struct web_api_command api_commands_v2[] = { { .api = "functions", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_FUNCTIONS, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_functions, + .callback = api_v2_functions, .allow_subpaths = 0 }, @@ -705,7 +128,7 @@ static struct web_api_command api_commands_v2[] = { .hash = 0, .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, - .callback = web_client_api_request_v2_webrtc, + .callback = api_v2_webrtc, .allow_subpaths = 0 }, @@ -715,7 +138,7 @@ static struct web_api_command api_commands_v2[] = { .hash = 0, .acl = HTTP_ACL_NOCHECK, .access = HTTP_ACCESS_NONE, - .callback = web_client_api_request_v2_claim, + .callback = api_v2_claim, .allow_subpaths = 0 }, { @@ -731,17 +154,18 @@ static struct web_api_command api_commands_v2[] = { .hash = 0, .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, - .callback = api_v2_bearer_token, + .callback = api_v2_bearer_get_token, .allow_subpaths = 0 }, +#endif // Netdata branding APIs { .api = "ilove.svg", .hash = 0, - .acl = HTTP_ACL_DASHBOARD, + .acl = HTTP_ACL_NOCHECK, .access = HTTP_ACCESS_ANONYMOUS_DATA, - .callback = web_client_api_request_v2_ilove, + .callback = api_v2_ilove, .allow_subpaths = 0 }, diff --git a/src/web/api/web_api_v3.c b/src/web/api/web_api_v3.c new file mode 100644 index 00000000..a1092471 --- /dev/null +++ b/src/web/api/web_api_v3.c @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "web_api_v3.h" +#include "v1/api_v1_calls.h" +#include "v2/api_v2_calls.h" +#include "v3/api_v3_calls.h" + +static struct web_api_command api_commands_v3[] = { + // time-series multi-node multi-instance data APIs + { + .api = "data", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_data, + .allow_subpaths = 0 + }, + // badges + { + .api = "badge.svg", + .hash = 0, + .acl = HTTP_ACL_BADGES, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_badge, + .allow_subpaths = 0 + }, + // scoring engine + { + .api = "weights", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_weights, + .allow_subpaths = 0 + }, + // exporting API + { + .api = "allmetrics", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_allmetrics, + .allow_subpaths = 0 + }, + + // time-series multi-node multi-instance metadata APIs + { + .api = "context", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_context, + .allow_subpaths = 0 + }, + { + .api = "contexts", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_contexts, + .allow_subpaths = 0 + }, + + // fulltext search + { + .api = "q", + .hash = 0, + .acl = HTTP_ACL_METRICS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_q, + .allow_subpaths = 0 + }, + + // multi-node multi-instance alerts APIs + { + .api = "alerts", + .hash = 0, + .acl = HTTP_ACL_ALERTS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_alerts, + .allow_subpaths = 0 + }, + { + .api = "alert_transitions", + .hash = 0, + .acl = HTTP_ACL_ALERTS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_alert_transitions, + .allow_subpaths = 0 + }, + { + .api = "alert_config", + .hash = 0, + .acl = HTTP_ACL_ALERTS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_alert_config, + .allow_subpaths = 0 + }, + { + .api = "variable", + .hash = 0, + .acl = HTTP_ACL_ALERTS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_variable, + .allow_subpaths = 0 + }, + + // agent information APIs + { + .api = "info", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_NONE, + .callback = api_v2_info, + .allow_subpaths = 0 + }, + { + .api = "nodes", + .hash = 0, + .acl = HTTP_ACL_NODES, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_nodes, + .allow_subpaths = 0 + }, + { + .api = "node_instances", + .hash = 0, + .acl = HTTP_ACL_NODES, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_node_instances, + .allow_subpaths = 0 + }, + { + .api = "versions", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_versions, + .allow_subpaths = 0 + }, + { + .api = "progress", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_progress, + .allow_subpaths = 0 + }, + + // functions APIs + { + .api = "function", + .hash = 0, + .acl = HTTP_ACL_FUNCTIONS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_function, + .allow_subpaths = 0 + }, + { + .api = "functions", + .hash = 0, + .acl = HTTP_ACL_FUNCTIONS, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v2_functions, + .allow_subpaths = 0 + }, + + // dyncfg APIs + { + .api = "config", + .hash = 0, + .acl = HTTP_ACL_DYNCFG, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v1_config, + .allow_subpaths = 0 + }, + + // settings APIs + { + .api = "settings", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_ANONYMOUS_DATA, + .callback = api_v3_settings, + .allow_subpaths = 0 + }, + + // WebRTC APIs + { + .api = "rtc_offer", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, + .callback = api_v2_webrtc, + .allow_subpaths = 0 + }, + + // management APIs + { + .api = "claim", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_NONE, + .callback = api_v3_claim, + .allow_subpaths = 0 + }, + { + .api = "bearer_protection", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_VIEW_AGENT_CONFIG | HTTP_ACCESS_EDIT_AGENT_CONFIG, + .callback = api_v2_bearer_protection, + .allow_subpaths = 0 + }, + { + .api = "bearer_get_token", + .hash = 0, + .acl = HTTP_ACL_ACLK | ACL_DEV_OPEN_ACCESS, + .access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE, + .callback = api_v2_bearer_get_token, + .allow_subpaths = 0 + }, + { + .api = "me", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_NONE, + .callback = api_v3_me, + .allow_subpaths = 0 + }, + + // Netdata branding APIs + { + .api = "ilove.svg", + .hash = 0, + .acl = HTTP_ACL_NOCHECK, + .access = HTTP_ACCESS_NONE, + .callback = api_v2_ilove, + .allow_subpaths = 0 + }, + + {// terminator + .api = NULL, + .hash = 0, + .acl = HTTP_ACL_NONE, + .access = HTTP_ACCESS_NONE, + .callback = NULL, + .allow_subpaths = 0 + }, +}; + +inline int web_client_api_request_v3(RRDHOST *host, struct web_client *w, char *url_path_endpoint) { + static int initialized = 0; + + if(unlikely(initialized == 0)) { + initialized = 1; + + for(int i = 0; api_commands_v3[i].api ; i++) + api_commands_v3[i].hash = simple_hash(api_commands_v3[i].api); + } + + return web_client_api_request_vX(host, w, url_path_endpoint, api_commands_v3); +} diff --git a/src/web/api/web_api_v3.h b/src/web/api/web_api_v3.h new file mode 100644 index 00000000..32fa4cd1 --- /dev/null +++ b/src/web/api/web_api_v3.h @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +#ifndef NETDATA_WEB_API_V3_H +#define NETDATA_WEB_API_V3_H + +#include "web_api.h" + +struct web_client; + +int web_client_api_request_v3(RRDHOST *host, struct web_client *w, char *url_path_endpoint); + +#endif //NETDATA_WEB_API_V3_H |