dyncfg.c 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405
  1. // SPDX-License-Identifier: GPL-3.0-or-later
  2. #include "dyncfg-internals.h"
  3. #include "dyncfg.h"
  4. struct dyncfg_globals dyncfg_globals = { 0 };
  5. void dyncfg_cleanup(DYNCFG *v) {
  6. buffer_free(v->payload);
  7. v->payload = NULL;
  8. string_freez(v->path);
  9. v->path = NULL;
  10. string_freez(v->source);
  11. v->source = NULL;
  12. string_freez(v->function);
  13. v->function = NULL;
  14. string_freez(v->template);
  15. v->template = NULL;
  16. }
  17. static void dyncfg_normalize(DYNCFG *df) {
  18. usec_t now_ut = now_realtime_usec();
  19. if(!df->created_ut)
  20. df->created_ut = now_ut;
  21. if(!df->modified_ut)
  22. df->modified_ut = now_ut;
  23. }
  24. static void dyncfg_delete_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) {
  25. DYNCFG *df = value;
  26. dyncfg_cleanup(df);
  27. }
  28. static void dyncfg_insert_cb(const DICTIONARY_ITEM *item, void *value, void *data __maybe_unused) {
  29. DYNCFG *df = value;
  30. dyncfg_normalize(df);
  31. const char *id = dictionary_acquired_item_name(item);
  32. char buf[strlen(id) + 20];
  33. snprintfz(buf, sizeof(buf), PLUGINSD_FUNCTION_CONFIG " %s", id);
  34. df->function = string_strdupz(buf);
  35. if(df->type == DYNCFG_TYPE_JOB && !df->template) {
  36. const char *last_colon = strrchr(id, ':');
  37. if(last_colon)
  38. df->template = string_strndupz(id, last_colon - id);
  39. else
  40. nd_log(NDLS_DAEMON, NDLP_WARNING,
  41. "DYNCFG: id '%s' is a job, but does not contain a colon to find the template", id);
  42. }
  43. }
  44. static void dyncfg_react_cb(const DICTIONARY_ITEM *item __maybe_unused, void *value, void *data __maybe_unused) {
  45. DYNCFG *df = value; (void)df;
  46. ;
  47. }
  48. static bool dyncfg_conflict_cb(const DICTIONARY_ITEM *item __maybe_unused, void *old_value, void *new_value, void *data __maybe_unused) {
  49. DYNCFG *v = old_value;
  50. DYNCFG *nv = new_value;
  51. size_t changes = 0;
  52. dyncfg_normalize(nv);
  53. if(v->host != nv->host) {
  54. SWAP(v->host, nv->host);
  55. changes++;
  56. }
  57. if(v->path != nv->path) {
  58. SWAP(v->path, nv->path);
  59. changes++;
  60. }
  61. if(v->status != nv->status) {
  62. SWAP(v->status, nv->status);
  63. changes++;
  64. }
  65. if(v->type != nv->type) {
  66. SWAP(v->type, nv->type);
  67. changes++;
  68. }
  69. if(v->source_type != nv->source_type) {
  70. SWAP(v->source_type, nv->source_type);
  71. changes++;
  72. }
  73. if(v->cmds != nv->cmds) {
  74. SWAP(v->cmds, nv->cmds);
  75. changes++;
  76. }
  77. if(v->source != nv->source) {
  78. SWAP(v->source, nv->source);
  79. changes++;
  80. }
  81. if(nv->created_ut < v->created_ut) {
  82. SWAP(v->created_ut, nv->created_ut);
  83. changes++;
  84. }
  85. if(nv->modified_ut > v->modified_ut) {
  86. SWAP(v->modified_ut, nv->modified_ut);
  87. changes++;
  88. }
  89. if(v->sync != nv->sync) {
  90. SWAP(v->sync, nv->sync);
  91. changes++;
  92. }
  93. if(nv->payload) {
  94. SWAP(v->payload, nv->payload);
  95. changes++;
  96. }
  97. if(!v->execute_cb || (nv->overwrite_cb && nv->execute_cb && (v->execute_cb != nv->execute_cb || v->execute_cb_data != nv->execute_cb_data))) {
  98. v->execute_cb = nv->execute_cb;
  99. v->execute_cb_data = nv->execute_cb_data;
  100. changes++;
  101. }
  102. dyncfg_cleanup(nv);
  103. return changes > 0;
  104. }
  105. // ----------------------------------------------------------------------------
  106. void dyncfg_init_low_level(bool load_saved) {
  107. if(!dyncfg_globals.nodes) {
  108. dyncfg_globals.nodes = dictionary_create_advanced(DICT_OPTION_FIXED_SIZE | DICT_OPTION_DONT_OVERWRITE_VALUE, NULL, sizeof(DYNCFG));
  109. dictionary_register_insert_callback(dyncfg_globals.nodes, dyncfg_insert_cb, NULL);
  110. dictionary_register_react_callback(dyncfg_globals.nodes, dyncfg_react_cb, NULL);
  111. dictionary_register_conflict_callback(dyncfg_globals.nodes, dyncfg_conflict_cb, NULL);
  112. dictionary_register_delete_callback(dyncfg_globals.nodes, dyncfg_delete_cb, NULL);
  113. char path[PATH_MAX];
  114. snprintfz(path, sizeof(path), "%s/%s", netdata_configured_varlib_dir, "config");
  115. if(mkdir(path, 0755) == -1) {
  116. if(errno != EEXIST)
  117. nd_log(NDLS_DAEMON, NDLP_CRIT, "DYNCFG: failed to create dynamic configuration directory '%s'", path);
  118. }
  119. dyncfg_globals.dir = strdupz(path);
  120. if(load_saved)
  121. dyncfg_load_all();
  122. }
  123. }
  124. // ----------------------------------------------------------------------------
  125. const DICTIONARY_ITEM *dyncfg_add_internal(RRDHOST *host, const char *id, const char *path, DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, const char *source, DYNCFG_CMDS cmds, usec_t created_ut, usec_t modified_ut, bool sync, rrd_function_execute_cb_t execute_cb, void *execute_cb_data, bool overwrite_cb) {
  126. DYNCFG tmp = {
  127. .host = host,
  128. .path = string_strdupz(path),
  129. .status = status,
  130. .type = type,
  131. .cmds = cmds,
  132. .source_type = source_type,
  133. .source = string_strdupz(source),
  134. .created_ut = created_ut,
  135. .modified_ut = modified_ut,
  136. .sync = sync,
  137. .user_disabled = false,
  138. .restart_required = false,
  139. .payload = NULL,
  140. .execute_cb = execute_cb,
  141. .execute_cb_data = execute_cb_data,
  142. .overwrite_cb = overwrite_cb,
  143. };
  144. uuid_copy(tmp.host_uuid, host->host_uuid);
  145. return dictionary_set_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1, &tmp, sizeof(tmp), NULL);
  146. }
  147. static void dyncfg_send_updates(const char *id) {
  148. const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item_advanced(dyncfg_globals.nodes, id, -1);
  149. if(!item) {
  150. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: asked to update plugin for configuration '%s', but it is not found.", id);
  151. return;
  152. }
  153. DYNCFG *df = dictionary_acquired_item_value(item);
  154. if(df->type == DYNCFG_TYPE_SINGLE || df->type == DYNCFG_TYPE_JOB) {
  155. if (df->cmds & DYNCFG_CMD_UPDATE)
  156. dyncfg_echo_update(item, df, id);
  157. }
  158. else if(df->type == DYNCFG_TYPE_TEMPLATE && (df->cmds & DYNCFG_CMD_ADD)) {
  159. STRING *template = string_strdupz(id);
  160. size_t len = strlen(id);
  161. DYNCFG *tf;
  162. dfe_start_reentrant(dyncfg_globals.nodes, tf) {
  163. const char *t_id = tf_dfe.name;
  164. if(tf->type == DYNCFG_TYPE_JOB && tf->template == template && strncmp(t_id, id, len) == 0 && t_id[len] == ':' && t_id[len + 1]) {
  165. dyncfg_echo_add(item, df, id, &t_id[len + 1]);
  166. }
  167. }
  168. dfe_done(tf);
  169. string_freez(template);
  170. }
  171. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  172. }
  173. bool dyncfg_is_user_disabled(const char *id) {
  174. const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
  175. if(!item)
  176. return false;
  177. DYNCFG *df = dictionary_acquired_item_value(item);
  178. bool ret = df->user_disabled;
  179. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  180. return ret;
  181. }
  182. bool dyncfg_job_has_registered_template(const char *id) {
  183. char buf[strlen(id) + 1];
  184. memcpy(buf, id, sizeof(buf));
  185. char *colon = strrchr(buf, ':');
  186. if(!colon)
  187. return false;
  188. *colon = '\0';
  189. const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, buf);
  190. if(!item)
  191. return false;
  192. DYNCFG *df = dictionary_acquired_item_value(item);
  193. bool ret = df->type == DYNCFG_TYPE_TEMPLATE;
  194. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  195. return ret;
  196. }
  197. bool dyncfg_add_low_level(RRDHOST *host, const char *id, const char *path, DYNCFG_STATUS status, DYNCFG_TYPE type, DYNCFG_SOURCE_TYPE source_type, const char *source, DYNCFG_CMDS cmds, usec_t created_ut, usec_t modified_ut, bool sync, rrd_function_execute_cb_t execute_cb, void *execute_cb_data) {
  198. if(!dyncfg_is_valid_id(id)) {
  199. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
  200. return false;
  201. }
  202. if(type == DYNCFG_TYPE_JOB && !dyncfg_job_has_registered_template(id)) {
  203. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: job id '%s' does not have a registered template. Ignoring dynamic configuration for it.", id);
  204. return false;
  205. }
  206. DYNCFG_CMDS old_cmds = cmds;
  207. // all configurations support schema
  208. cmds |= DYNCFG_CMD_SCHEMA;
  209. // if there is either enable or disable, both are supported
  210. if(cmds & (DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE))
  211. cmds |= DYNCFG_CMD_ENABLE | DYNCFG_CMD_DISABLE;
  212. // add
  213. if(type == DYNCFG_TYPE_TEMPLATE) {
  214. // templates must always support "add"
  215. cmds |= DYNCFG_CMD_ADD;
  216. }
  217. else {
  218. // only templates can have "add"
  219. cmds &= ~DYNCFG_CMD_ADD;
  220. }
  221. // remove
  222. if(source_type == DYNCFG_SOURCE_TYPE_DYNCFG && type == DYNCFG_TYPE_JOB) {
  223. // remove is only available for dyncfg jobs
  224. cmds |= DYNCFG_CMD_REMOVE;
  225. }
  226. else {
  227. // remove is only available for dyncfg jobs
  228. cmds &= ~DYNCFG_CMD_REMOVE;
  229. }
  230. // data
  231. if(type == DYNCFG_TYPE_TEMPLATE) {
  232. // templates do not have data
  233. cmds &= ~(DYNCFG_CMD_GET | DYNCFG_CMD_UPDATE | DYNCFG_CMD_TEST);
  234. }
  235. if(cmds != old_cmds) {
  236. CLEAN_BUFFER *t = buffer_create(1024, NULL);
  237. buffer_sprintf(t, "DYNCFG: id '%s' was declared with cmds: ", id);
  238. dyncfg_cmds2buffer(old_cmds, t);
  239. buffer_strcat(t, ", but they have sanitized to: ");
  240. dyncfg_cmds2buffer(cmds, t);
  241. nd_log(NDLS_DAEMON, NDLP_NOTICE, "%s", buffer_tostring(t));
  242. }
  243. const DICTIONARY_ITEM *item = dyncfg_add_internal(host, id, path, status, type, source_type, source, cmds, created_ut, modified_ut, sync, execute_cb, execute_cb_data, true);
  244. DYNCFG *df = dictionary_acquired_item_value(item);
  245. // if(df->source_type == DYNCFG_SOURCE_TYPE_DYNCFG && !df->saves)
  246. // 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);
  247. rrd_collector_started();
  248. rrd_function_add(
  249. host,
  250. NULL,
  251. string2str(df->function),
  252. 120,
  253. 1000,
  254. "Dynamic configuration",
  255. "config",
  256. HTTP_ACCESS_ADMIN,
  257. sync,
  258. dyncfg_function_intercept_cb,
  259. NULL);
  260. DYNCFG_CMDS status_to_send_to_plugin = df->user_disabled ? DYNCFG_CMD_DISABLE : DYNCFG_CMD_ENABLE;
  261. if(status_to_send_to_plugin == DYNCFG_CMD_ENABLE && dyncfg_is_user_disabled(string2str(df->template)))
  262. status_to_send_to_plugin = DYNCFG_CMD_DISABLE;
  263. dyncfg_echo(item, df, id, status_to_send_to_plugin);
  264. dyncfg_send_updates(id);
  265. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  266. return true;
  267. }
  268. void dyncfg_del_low_level(RRDHOST *host, const char *id) {
  269. if(!dyncfg_is_valid_id(id)) {
  270. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
  271. return;
  272. }
  273. const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
  274. if(item) {
  275. DYNCFG *df = dictionary_acquired_item_value(item);
  276. rrd_function_del(host, NULL, string2str(df->function));
  277. bool garbage_collect = false;
  278. if(df->saves == 0) {
  279. dictionary_del(dyncfg_globals.nodes, id);
  280. garbage_collect = true;
  281. }
  282. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  283. if(garbage_collect)
  284. dictionary_garbage_collect(dyncfg_globals.nodes);
  285. }
  286. }
  287. void dyncfg_status_low_level(RRDHOST *host __maybe_unused, const char *id, DYNCFG_STATUS status) {
  288. if(!dyncfg_is_valid_id(id)) {
  289. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: id '%s' is invalid. Ignoring dynamic configuration for it.", id);
  290. return;
  291. }
  292. if(status == DYNCFG_STATUS_NONE) {
  293. nd_log(NDLS_DAEMON, NDLP_ERR, "DYNCFG: status provided to id '%s' is invalid. Ignoring it.", id);
  294. return;
  295. }
  296. const DICTIONARY_ITEM *item = dictionary_get_and_acquire_item(dyncfg_globals.nodes, id);
  297. if(item) {
  298. DYNCFG *df = dictionary_acquired_item_value(item);
  299. df->status = status;
  300. dictionary_acquired_item_release(dyncfg_globals.nodes, item);
  301. }
  302. }
  303. // ----------------------------------------------------------------------------
  304. void dyncfg_add_streaming(BUFFER *wb) {
  305. // when sending config functions to parents, we send only 1 function called 'config';
  306. // the parent will send the command to the child, and the child will validate it;
  307. // this way the parent does not need to receive removals of config functions;
  308. buffer_sprintf(wb
  309. , PLUGINSD_KEYWORD_FUNCTION " GLOBAL " PLUGINSD_FUNCTION_CONFIG " %d \"%s\" \"%s\" \"%s\" %d\n"
  310. , 120
  311. , "Dynamic configuration"
  312. , "config"
  313. , http_id2access(HTTP_ACCESS_ADMIN)
  314. , 1000
  315. );
  316. }
  317. bool dyncfg_available_for_rrdhost(RRDHOST *host) {
  318. if(host == localhost || rrdhost_option_check(host, RRDHOST_OPTION_VIRTUAL_HOST))
  319. return true;
  320. return rrd_function_available(host, PLUGINSD_FUNCTION_CONFIG);
  321. }
  322. // ----------------------------------------------------------------------------