| 1 | // SPDX-License-Identifier: GPL-3.0-or-later |
| 2 | |
| 3 | #include "dyncfg-internals.h" |
| 4 | #include "dyncfg.h" |
| 5 | |
| 6 | struct dyncfg_globals dyncfg_globals = { 0 }; |
| 7 | |
| 8 | RRDHOST *dyncfg_rrdhost_by_uuid(ND_UUID *uuid) { |
| 9 | char uuid_str[UUID_STR_LEN]; |
| 10 | uuid_unparse_lower(uuid->uuid, uuid_str); |
| 11 | |
| 12 | RRDHOST *host = rrdhost_find_by_guid(uuid_str); |
| 13 | if(!host) |
| 14 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: cannot find host with UUID '%s'", uuid_str); |
| 15 | |
| 16 | return host; |
| 17 | } |
| 18 | |
| 19 | RRDHOST *dyncfg_rrdhost(DYNCFG *df) { |
| 20 | return dyncfg_rrdhost_by_uuid(&df->host_uuid); |
| 21 | } |
| 22 | |
| 23 | void dyncfg_cleanup(DYNCFG *v) { |
| 24 | string_freez(v->dyncfg.source); |
| 25 | v->dyncfg.source = NULL; |
| 26 | |
| 27 | buffer_free(v->dyncfg.payload); |
| 28 | v->dyncfg.payload = NULL; |
| 29 | |
| 30 | string_freez(v->path); |
| 31 | v->path = NULL; |
| 32 | |
| 33 | string_freez(v->current.source); |
| 34 | v->current.source = NULL; |
| 35 | |
| 36 | string_freez(v->function); |
| 37 | v->function = NULL; |
| 38 | |
| 39 | string_freez(v->template); |
| 40 | v->template = NULL; |
| 41 | } |
| 42 | |
| 43 | static void dyncfg_normalize(DYNCFG *df) { |
| 44 | usec_t now_ut = now_realtime_usec(); |
| 45 | |
| 46 | if(!df->current.created_ut) |
| 47 | df->current.created_ut = now_ut; |
| 48 | |
| 49 | if(!df->current.modified_ut) |
| 50 | df->current.modified_ut = now_ut; |
| 51 | } |
| 52 | |
| 53 | static void dyncfg_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 54 | DYNCFG *df = value; |
| 55 | dyncfg_cleanup(df); |
| 56 | } |
| 57 | |
| 58 | static void dyncfg_insert_cb(const DICTIONARY_ITEM *item, void *value, void *data __maybe_unused) { |
| 59 | DYNCFG *df = value; |
| 60 | dyncfg_normalize(df); |
| 61 | |
| 62 | const char *id = dictionary_acquired_item_name(item); |
| 63 | size_t buf_size = strlen(id) + 20; |
| 64 | CLEAN_CHAR_P *buf = mallocz(buf_size); |
| 65 | snprintfz(buf, buf_size, PLUGINSD_FUNCTION_CONFIG " %s", id); |
| 66 | df->function = string_strdupz(buf); |
| 67 | |
| 68 | if(df->type == DYNCFG_TYPE_JOB && !df->template) { |
| 69 | const char *last_colon = strrchr(id, ':'); |
| 70 | if(last_colon) |
| 71 | df->template = string_strndupz(id, last_colon - id); |
| 72 | else |
| 73 | nd_log(NDLS_DAEMON, NDLP_WARNING, |
| 74 | "DYNCFG: id '%s' is a job, but does not contain a colon to find the template", id); |
| 75 | } |
| 76 | } |
| 77 | |
| 78 | static void dyncfg_react_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) { |
| 79 | DYNCFG *df = value; (void)df; |
| 80 | ; |
| 81 | } |
| 82 | |
| 83 | static bool dyncfg_conflict_cb(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data) { |
| 84 | bool *overwrite_cb_ptr = data; |
| 85 | bool overwrite_cb = (overwrite_cb_ptr && *overwrite_cb_ptr); |
| 86 | |
| 87 | DYNCFG *v = old_value; |
| 88 | DYNCFG *nv = new_value; |
| 89 | |
| 90 | size_t changes = 0; |
| 91 | |
| 92 | dyncfg_normalize(nv); |
| 93 | |
| 94 | if(!UUIDeq(v->host_uuid, nv->host_uuid)) { |
| 95 | char u1[UUID_STR_LEN], u2[UUID_STR_LEN]; |
| 96 | nd_uuid_unparse_lower(v->host_uuid.uuid, u1); |
| 97 | nd_uuid_unparse_lower(nv->host_uuid.uuid, u2); |
| 98 | |
| 99 | nd_log(NDLS_DAEMON, NDLP_NOTICE, |
| 100 | "DYNCFG: configuration '%s' changed host id from '%s' to '%s'", |
| 101 | dictionary_acquired_item_name(item), u1, u2); |
| 102 | |
| 103 | SWAP(v->host_uuid, nv->host_uuid); |
| 104 | changes++; |
| 105 | } |
| 106 | |
| 107 | if(v->path != nv->path) { |
| 108 | nd_log(NDLS_DAEMON, NDLP_NOTICE, |
| 109 | "DYNCFG: configuration '%s' changed path from '%s' to '%s'", |
| 110 | dictionary_acquired_item_name(item), string2str(v->path), string2str(nv->path)); |
| 111 | |
| 112 | SWAP(v->path, nv->path); |
| 113 | changes++; |
| 114 | } |
| 115 | |
| 116 | if(v->cmds != nv->cmds) { |
| 117 | SWAP(v->cmds, nv->cmds); |
| 118 | changes++; |
| 119 | } |
| 120 | |
| 121 | if(v->type != nv->type) { |
| 122 | SWAP(v->type, nv->type); |
| 123 | changes++; |
| 124 | } |
| 125 | |
| 126 | if(v->view_access != nv->view_access) { |
| 127 | SWAP(v->view_access, nv->view_access); |
| 128 | changes++; |
| 129 | } |
| 130 | |
| 131 | if(v->edit_access != nv->edit_access) { |
| 132 | SWAP(v->edit_access, nv->edit_access); |
| 133 | changes++; |
| 134 | } |
| 135 | |
| 136 | if(v->current.status != nv->current.status) { |
| 137 | SWAP(v->current.status, nv->current.status); |
| 138 | changes++; |
| 139 | } |
| 140 | |
| 141 | if (v->current.source_type != nv->current.source_type) { |
| 142 | SWAP(v->current.source_type, nv->current.source_type); |
| 143 | changes++; |
| 144 | } |
| 145 | |
| 146 | if (v->current.source != nv->current.source) { |
| 147 | SWAP(v->current.source, nv->current.source); |
| 148 | changes++; |
| 149 | } |
| 150 | |
| 151 | if(nv->current.created_ut < v->current.created_ut) { |
| 152 | SWAP(v->current.created_ut, nv->current.created_ut); |
| 153 | changes++; |
| 154 | } |
| 155 | |
| 156 | if(nv->current.modified_ut > v->current.modified_ut) { |
| 157 | SWAP(v->current.modified_ut, nv->current.modified_ut); |
| 158 | changes++; |
| 159 | } |
| 160 | |
| 161 | if(!v->execute_cb || (overwrite_cb && nv->execute_cb && (v->execute_cb != nv->execute_cb || v->execute_cb_data != nv->execute_cb_data))) { |
| 162 | v->sync = nv->sync, |
| 163 | v->execute_cb = nv->execute_cb; |
| 164 | v->execute_cb_data = nv->execute_cb_data; |
| 165 | changes++; |
| 166 | } |
| 167 | |
| 168 | dyncfg_cleanup(nv); |
| 169 | |
| 170 | return changes > 0; |
| 171 | } |
| 172 | |
| 173 | // ---------------------------------------------------------------------------- |
| 174 | |
| 175 | void dyncfg_init_low_level(bool load_saved) { |
| 176 | if(!dyncfg_globals.nodes) { |
| 177 | dyncfg_globals.nodes = dictionary_create_advanced(DICT_OPTION_FIXED_SIZE | DICT_OPTION_DONT_OVERWRITE_VALUE, &dictionary_stats_category_dyncfg, sizeof(DYNCFG)); |
| 178 | dictionary_register_insert_callback(dyncfg_globals.nodes, dyncfg_insert_cb, NULL); |
| 179 | dictionary_register_react_callback(dyncfg_globals.nodes, dyncfg_react_cb, NULL); |
| 180 | dictionary_register_conflict_callback(dyncfg_globals.nodes, dyncfg_conflict_cb, NULL); |
| 181 | dictionary_register_delete_callback(dyncfg_globals.nodes, dyncfg_delete_cb, NULL); |
| 182 | |
| 183 | char path[PATH_MAX]; |
| 184 | snprintfz(path, sizeof(path), "%s/%s", netdata_configured_varlib_dir, "config"); |
| 185 | |
| 186 | if(mkdir(path, 0755) == -1) { |
| 187 | if(errno != EEXIST) |
| 188 | nd_log(NDLS_DAEMON, NDLP_CRIT, "DYNCFG: failed to create dynamic configuration directory '%s'", path); |
| 189 | } |
| 190 | |
| 191 | dyncfg_globals.dir = strdupz(path); |
| 192 | |
| 193 | if(load_saved) |
| 194 | dyncfg_load_all(); |
| 195 | } |
| 196 | } |
| 197 | |
| 198 | void dyncfg_shutdown_low_level(void) { |
| 199 | if(dyncfg_globals.nodes) { |
| 200 | dictionary_destroy(dyncfg_globals.nodes); |
| 201 | dyncfg_globals.nodes = NULL; |
| 202 | } |
| 203 | |
| 204 | freez((void *)dyncfg_globals.dir); |
| 205 | dyncfg_globals.dir = NULL; |
| 206 | } |
| 207 | |
| 208 | // ---------------------------------------------------------------------------- |
| 209 | |
| 210 | const DICTIONARY_ITEM *dyncfg_add_internal(RRDHOST *host, const char *id, const char *path, |
| 211 | DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, |
| 212 | const char *source, DYNCFG_CMDS cmds, |
| 213 | usec_t created_ut, usec_t modified_ut, |
| 214 | bool sync, HTTP_ACCESS view_access, HTTP_ACCESS edit_access, |
| 215 | rrd_function_execute_cb_t execute_cb, void *execute_cb_data, |
| 216 | bool overwrite_cb) { |
| 217 | DYNCFG tmp = { |
| 218 | .host_uuid = host->host_id, |
| 219 | .path = string_strdupz(path), |
| 220 | .cmds = cmds, |
| 221 | .type = type, |
| 222 | .view_access = view_access, |
| 223 | .edit_access = edit_access, |
| 224 | .current = { |
| 225 | .status = status, |
| 226 | .source_type = source_type, |
| 227 | .source = string_strdupz(source), |
| 228 | .created_ut = created_ut, |
| 229 | .modified_ut = modified_ut, |
| 230 | }, |
| 231 | .sync = sync, |
| 232 | .dyncfg = { 0 }, |
| 233 | .execute_cb = execute_cb, |
| 234 | .execute_cb_data = execute_cb_data, |
| 235 | }; |
| 236 | |
| 237 | return dictionary_set_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1, &tmp, sizeof(tmp), &overwrite_cb); |
| 238 | } |
| 239 | |
| 240 | static void dyncfg_send_updates(const char *id) { |
| 241 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1); |
| 242 | if(!item) { |
| 243 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: asked to update plugin for configuration '%s', but it is not found.", id); |
| 244 | return; |
| 245 | } |
| 246 | |
| 247 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 248 | |
| 249 | if(df->type == DYNCFG_TYPE_SINGLE || df->type == DYNCFG_TYPE_JOB) { |
| 250 | if (df->cmds & DYNCFG_CMD_UPDATE && df->dyncfg.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && df->dyncfg.payload && buffer_strlen(df->dyncfg.payload)) |
| 251 | dyncfg_echo_update(item, df, id); |
| 252 | } |
| 253 | else if(df->type == DYNCFG_TYPE_TEMPLATE && (df->cmds & DYNCFG_CMD_ADD)) { |
| 254 | STRING *template = string_strdupz(id); |
| 255 | |
| 256 | size_t len = strlen(id); |
| 257 | DYNCFG *df_job; |
| 258 | dfe_start_reentrant(dyncfg_globals.nodes, df_job) { |
| 259 | const char *id_template = df_job_dfe.name; |
| 260 | if(df_job->type == DYNCFG_TYPE_JOB && // it is a job |
| 261 | df_job->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && // it is dynamically configured |
| 262 | df_job->template == template && // it has the same template name |
| 263 | strncmp(id_template, id, len) == 0 && // the template name matches (redundant) |
| 264 | id_template[len] == ':' && // immediately after the template there is ':' |
| 265 | id_template[len + 1]) { // and there is something else after the ':' |
| 266 | dyncfg_echo_add(item, df_job_dfe.item, df, df_job, id, &id_template[len + 1]); |
| 267 | } |
| 268 | } |
| 269 | dfe_done(df_job); |
| 270 | |
| 271 | string_freez(template); |
| 272 | } |
| 273 | |
| 274 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 275 | } |
| 276 | |
| 277 | bool dyncfg_is_user_disabled(const char *id) { |
| 278 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id); |
| 279 | if(!item) |
| 280 | return false; |
| 281 | |
| 282 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 283 | bool ret = df->dyncfg.user_disabled; |
| 284 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 285 | return ret; |
| 286 | } |
| 287 | |
| 288 | bool dyncfg_job_has_registered_template(const char *id) { |
| 289 | CLEAN_CHAR_P *buf = strdupz(id); |
| 290 | char *colon = strrchr(buf, ':'); |
| 291 | if(!colon) |
| 292 | return false; |
| 293 | |
| 294 | *colon = '\0'; |
| 295 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, buf); |
| 296 | if(!item) |
| 297 | return false; |
| 298 | |
| 299 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 300 | bool ret = df->type == DYNCFG_TYPE_TEMPLATE; |
| 301 | |
| 302 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 303 | return ret; |
| 304 | } |
| 305 | |
| 306 | DYNCFG_CMDS dyncfg_sanitize_cmds(DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, DYNCFG_CMDS cmds) { |
| 307 | // all configurations support schema |
| 308 | cmds |= DYNCFG_CMD_SCHEMA; |
| 309 | |
| 310 | // if there is either enable or disable, both are supported |
| 311 | if(cmds & (DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE)) |
| 312 | cmds |= DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE; |
| 313 | |
| 314 | // add |
| 315 | if(type == DYNCFG_TYPE_TEMPLATE) { |
| 316 | // templates must always support "add" |
| 317 | cmds |= DYNCFG_CMD_ADD; |
| 318 | } |
| 319 | else { |
| 320 | // only templates can have "add" |
| 321 | cmds &= ~DYNCFG_CMD_ADD; |
| 322 | } |
| 323 | |
| 324 | // remove |
| 325 | if(source_type == DYNCFG_SOURCE_TYPE_DYNCFG && type == DYNCFG_TYPE_JOB) { |
| 326 | // dyncfg jobs must always be removable |
| 327 | cmds |= DYNCFG_CMD_REMOVE; |
| 328 | } |
| 329 | else { |
| 330 | // remove is only available for dyncfg jobs |
| 331 | cmds &= ~DYNCFG_CMD_REMOVE; |
| 332 | } |
| 333 | |
| 334 | // data |
| 335 | if(type == DYNCFG_TYPE_TEMPLATE) { |
| 336 | // templates do not have data |
| 337 | cmds &= ~(DYNCFG_CMD_GET | DYNCFG_CMD_UPDATE); |
| 338 | } |
| 339 | |
| 340 | return cmds; |
| 341 | } |
| 342 | |
| 343 | bool dyncfg_add_low_level(RRDHOST *host, const char *id, const char *path, |
| 344 | DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, const char *source, |
| 345 | DYNCFG_CMDS cmds, usec_t created_ut, usec_t modified_ut, bool sync, |
| 346 | HTTP_ACCESS view_access, HTTP_ACCESS edit_access, |
| 347 | rrd_function_execute_cb_t execute_cb, void *execute_cb_data) { |
| 348 | |
| 349 | if(view_access == HTTP_ACCESS_NONE) |
| 350 | view_access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_VIEW_AGENT_CONFIG; |
| 351 | |
| 352 | if(edit_access == HTTP_ACCESS_NONE) |
| 353 | edit_access = HTTP_ACCESS_SIGNED_ID | HTTP_ACCESS_SAME_SPACE | HTTP_ACCESS_EDIT_AGENT_CONFIG | HTTP_ACCESS_COMMERCIAL_SPACE; |
| 354 | |
| 355 | if(!dyncfg_is_valid_id(id)) { |
| 356 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id); |
| 357 | return false; |
| 358 | } |
| 359 | |
| 360 | if(type == DYNCFG_TYPE_JOB && !dyncfg_job_has_registered_template(id)) { |
| 361 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: job id '%s' does not have a registered template. Ignoring dynamic configuration for it.", id); |
| 362 | return false; |
| 363 | } |
| 364 | |
| 365 | DYNCFG_CMDS old_cmds = cmds; |
| 366 | cmds = dyncfg_sanitize_cmds(type, source_type, cmds); |
| 367 | |
| 368 | if(cmds != old_cmds) { |
| 369 | CLEAN_BUFFER *t = buffer_create(1024, NULL); |
| 370 | buffer_sprintf(t, "DYNCFG: id '%s' was declared with cmds: ", id); |
| 371 | dyncfg_cmds2buffer(old_cmds, t); |
| 372 | buffer_strcat(t, ", but they have sanitized to: "); |
| 373 | dyncfg_cmds2buffer(cmds, t); |
| 374 | nd_log(NDLS_DAEMON, NDLP_NOTICE, "%s", buffer_tostring(t)); |
| 375 | } |
| 376 | |
| 377 | const DICTIONARY_ITEM *item = dyncfg_add_internal(host, id, path, status, type, source_type, source, cmds, |
| 378 | created_ut, modified_ut, sync, view_access, edit_access, |
| 379 | execute_cb, execute_cb_data, true); |
| 380 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 381 | |
| 382 | // if(df->source_type == DYNCFG_SOURCE_TYPE_DYNCFG && !df->saves) |
| 383 | // nd_log(NDLS_DAEMON, NDLP_WARNING, "DYNCFG: configuration '%s' is created with source type dyncfg, but we don't have a saved configuration for it", id); |
| 384 | |
| 385 | rrd_collector_started(); |
| 386 | rrd_function_add( |
| 387 | host, |
| 388 | NULL, |
| 389 | string2str(df->function), |
| 390 | 120, |
| 391 | 1000, |
| 392 | DYNCFG_FUNCTIONS_VERSION, |
| 393 | "Dynamic configuration", |
| 394 | "config", |
| 395 | (view_access & edit_access), |
| 396 | sync, |
| 397 | dyncfg_function_intercept_cb, |
| 398 | NULL); |
| 399 | |
| 400 | if(df->type != DYNCFG_TYPE_TEMPLATE && (df->cmds & (DYNCFG_CMD_ENABLE|DYNCFG_CMD_DISABLE))) { |
| 401 | DYNCFG_CMDS status_to_send_to_plugin = |
| 402 | (df->dyncfg.user_disabled || df->current.status == DYNCFG_STATUS_DISABLED) ? DYNCFG_CMD_DISABLE : DYNCFG_CMD_ENABLE; |
| 403 | |
| 404 | if (status_to_send_to_plugin == DYNCFG_CMD_ENABLE && dyncfg_is_user_disabled(string2str(df->template))) |
| 405 | status_to_send_to_plugin = DYNCFG_CMD_DISABLE; |
| 406 | |
| 407 | dyncfg_echo(item, df, id, status_to_send_to_plugin); |
| 408 | } |
| 409 | |
| 410 | if(!(df->current.source_type == DYNCFG_SOURCE_TYPE_DYNCFG && df->type == DYNCFG_TYPE_JOB)) |
| 411 | dyncfg_send_updates(id); |
| 412 | |
| 413 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 414 | |
| 415 | return true; |
| 416 | } |
| 417 | |
| 418 | void dyncfg_del_low_level(RRDHOST *host, const char *id) { |
| 419 | if(!dyncfg_is_valid_id(id)) { |
| 420 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id); |
| 421 | return; |
| 422 | } |
| 423 | |
| 424 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id); |
| 425 | if(item) { |
| 426 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 427 | rrd_function_del(host, NULL, string2str(df->function)); |
| 428 | |
| 429 | bool garbage_collect = false; |
| 430 | if(df->dyncfg.saves == 0) { |
| 431 | dictionary_del(dyncfg_globals.nodes, id); |
| 432 | garbage_collect = true; |
| 433 | } |
| 434 | |
| 435 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 436 | |
| 437 | if(garbage_collect) |
| 438 | dictionary_garbage_collect(dyncfg_globals.nodes); |
| 439 | } |
| 440 | } |
| 441 | |
| 442 | void dyncfg_status_low_level(RRDHOST *host __maybe_unused, const char *id, DYNCFG_STATUS status) { |
| 443 | if(!dyncfg_is_valid_id(id)) { |
| 444 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id); |
| 445 | return; |
| 446 | } |
| 447 | |
| 448 | if(status == DYNCFG_STATUS_NONE) { |
| 449 | nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: status provided to id '%s' is invalid. Ignoring it.", id); |
| 450 | return; |
| 451 | } |
| 452 | |
| 453 | const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id); |
| 454 | if(item) { |
| 455 | DYNCFG *df = dictionary_acquired_item_value(item); |
| 456 | df->current.status = status; |
| 457 | dictionary_acquired_item_release(dyncfg_globals.nodes, item); |
| 458 | } |
| 459 | } |
| 460 | |
| 461 | // ---------------------------------------------------------------------------- |
| 462 | |
| 463 | void dyncfg_add_streaming(BUFFER *wb) { |
| 464 | // when sending config functions to parents, we send only 1 function called 'config'; |
| 465 | // the parent will send the command to the child, and the child will validate it; |
| 466 | // this way the parent does not need to receive removals of config functions; |
| 467 | |
| 468 | buffer_sprintf(wb |
| 469 | , PLUGINSD_KEYWORD_FUNCTION " GLOBAL " PLUGINSD_FUNCTION_CONFIG " %d \"%s\" \"%s\" "HTTP_ACCESS_FORMAT" %d\n" |
| 470 | , 120 |
| 471 | , "Dynamic configuration" |
| 472 | , "config" |
| 473 | , (unsigned)HTTP_ACCESS_ANONYMOUS_DATA |
| 474 | , 1000 |
| 475 | ); |
| 476 | } |
| 477 | |
| 478 | bool dyncfg_available_for_rrdhost(RRDHOST *host) { |
| 479 | if(host == localhost) |
| 480 | return true; |
| 481 | |
| 482 | return rrd_function_available(host, PLUGINSD_FUNCTION_CONFIG); |
| 483 | } |
| 484 | |
| 485 | // ---------------------------------------------------------------------------- |