]> git.proxmox.com Git - mirror_frr.git/blob - lib/northbound_confd.c
Merge pull request #3502 from donaldsharp/socket_to_me_baby
[mirror_frr.git] / lib / northbound_confd.c
1 /*
2 * Copyright (C) 2018 NetDEF, Inc.
3 * Renato Westphal
4 *
5 * This program is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License as published by the Free
7 * Software Foundation; either version 2 of the License, or (at your option)
8 * any later version.
9 *
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
12 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
13 * more details.
14 *
15 * You should have received a copy of the GNU General Public License along
16 * with this program; see the file COPYING; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 */
19
20 #include <zebra.h>
21
22 #include "log.h"
23 #include "lib_errors.h"
24 #include "command.h"
25 #include "libfrr.h"
26 #include "version.h"
27 #include "northbound.h"
28
29 #include <confd_lib.h>
30 #include <confd_cdb.h>
31 #include <confd_dp.h>
32 #include <confd_maapi.h>
33
34 DEFINE_MTYPE_STATIC(LIB, CONFD, "ConfD module")
35
36 static struct thread_master *master;
37 static struct sockaddr confd_addr;
38 static int cdb_sub_sock, dp_ctl_sock, dp_worker_sock;
39 static struct thread *t_cdb_sub, *t_dp_ctl, *t_dp_worker;
40 static struct confd_daemon_ctx *dctx;
41 static struct confd_notification_ctx *live_ctx;
42 static bool confd_connected;
43 static struct list *confd_spoints;
44 static struct nb_transaction *transaction;
45
46 static void frr_confd_finish_cdb(void);
47 static void frr_confd_finish_dp(void);
48 static int frr_confd_finish(void);
49
50 #define flog_err_confd(funcname) \
51 flog_err(EC_LIB_LIBCONFD, "%s: %s() failed: %s (%d): %s", __func__, \
52 (funcname), confd_strerror(confd_errno), confd_errno, \
53 confd_lasterr())
54
55
56 /* ------------ Utils ------------ */
57
58 /* Get XPath string from ConfD hashed keypath. */
59 static void frr_confd_get_xpath(const confd_hkeypath_t *kp, char *xpath,
60 size_t len)
61 {
62 char *p;
63
64 confd_xpath_pp_kpath(xpath, len, 0, kp);
65
66 /*
67 * Replace double quotes by single quotes (the format accepted by the
68 * northbound API).
69 */
70 p = xpath;
71 while ((p = strchr(p, '"')) != NULL)
72 *p++ = '\'';
73 }
74
75 /* Convert ConfD binary value to a string. */
76 static int frr_confd_val2str(const char *xpath, const confd_value_t *value,
77 char *string, size_t string_size)
78 {
79 struct confd_cs_node *csp;
80
81 csp = confd_cs_node_cd(NULL, xpath);
82 if (!csp) {
83 flog_err_confd("confd_cs_node_cd");
84 return -1;
85 }
86 if (confd_val2str(csp->info.type, value, string, string_size)
87 == CONFD_ERR) {
88 flog_err_confd("confd_val2str");
89 return -1;
90 }
91
92 return 0;
93 }
94
95 /* Obtain list entry from ConfD hashed keypath. */
96 static int frr_confd_hkeypath_get_list_entry(const confd_hkeypath_t *kp,
97 struct nb_node *nb_node,
98 const void **list_entry)
99 {
100 struct nb_node *nb_node_list;
101 int parent_lists = 0;
102 int curr_list = 0;
103
104 *list_entry = NULL;
105
106 /*
107 * Count the number of YANG lists in the path, disconsidering the
108 * last element.
109 */
110 nb_node_list = nb_node;
111 while (nb_node_list->parent_list) {
112 nb_node_list = nb_node_list->parent_list;
113 parent_lists++;
114 }
115 if (nb_node->snode->nodetype != LYS_LIST && parent_lists == 0)
116 return 0;
117
118 /* Start from the beginning and move down the tree. */
119 for (int i = kp->len; i >= 0; i--) {
120 struct yang_list_keys keys;
121
122 /* Not a YANG list. */
123 if (kp->v[i][0].type != C_BUF)
124 continue;
125
126 /* Obtain list keys. */
127 memset(&keys, 0, sizeof(keys));
128 for (int j = 0; kp->v[i][j].type != C_NOEXISTS; j++) {
129 strlcpy(keys.key[keys.num],
130 (char *)kp->v[i][j].val.buf.ptr,
131 sizeof(keys.key[keys.num]));
132 keys.num++;
133 }
134
135 /* Obtain northbound node associated to the YANG list. */
136 nb_node_list = nb_node;
137 for (int j = curr_list; j < parent_lists; j++)
138 nb_node_list = nb_node_list->parent_list;
139
140 /* Obtain list entry. */
141 if (!CHECK_FLAG(nb_node_list->flags, F_NB_NODE_KEYLESS_LIST)) {
142 *list_entry = nb_node_list->cbs.lookup_entry(
143 *list_entry, &keys);
144 if (*list_entry == NULL)
145 return -1;
146 } else {
147 unsigned long ptr_ulong;
148
149 /* Retrieve list entry from pseudo-key (string). */
150 if (sscanf(keys.key[0], "%lu", &ptr_ulong) != 1)
151 return -1;
152 *list_entry = (const void *)ptr_ulong;
153 }
154
155 curr_list++;
156 }
157
158 return 0;
159 }
160
161 /* Fill the current date and time into a confd_datetime structure. */
162 static void getdatetime(struct confd_datetime *datetime)
163 {
164 struct tm tm;
165 struct timeval tv;
166
167 gettimeofday(&tv, NULL);
168 gmtime_r(&tv.tv_sec, &tm);
169
170 memset(datetime, 0, sizeof(*datetime));
171 datetime->year = 1900 + tm.tm_year;
172 datetime->month = tm.tm_mon + 1;
173 datetime->day = tm.tm_mday;
174 datetime->sec = tm.tm_sec;
175 datetime->micro = tv.tv_usec;
176 datetime->timezone = 0;
177 datetime->timezone_minutes = 0;
178 datetime->hour = tm.tm_hour;
179 datetime->min = tm.tm_min;
180 }
181
182 /* ------------ CDB code ------------ */
183
184 struct cdb_iter_args {
185 struct nb_config *candidate;
186 bool error;
187 };
188
189 static enum cdb_iter_ret
190 frr_confd_cdb_diff_iter(confd_hkeypath_t *kp, enum cdb_iter_op cdb_op,
191 confd_value_t *oldv, confd_value_t *newv, void *args)
192 {
193 char xpath[XPATH_MAXLEN];
194 struct nb_node *nb_node;
195 enum nb_operation nb_op;
196 struct cdb_iter_args *iter_args = args;
197 char value_str[YANG_VALUE_MAXLEN];
198 struct yang_data *data;
199 char *sb1, *sb2;
200 int ret;
201
202 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
203
204 /*
205 * HACK: obtain value of leaf-list elements from the XPath due to
206 * a bug in the ConfD API.
207 */
208 value_str[0] = '\0';
209 sb1 = strrchr(xpath, '[');
210 sb2 = strrchr(xpath, ']');
211 if (sb1 && sb2 && !strchr(sb1, '=')) {
212 *sb2 = '\0';
213 strlcpy(value_str, sb1 + 1, sizeof(value_str));
214 *sb1 = '\0';
215 }
216
217 nb_node = nb_node_find(xpath);
218 if (!nb_node) {
219 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
220 "%s: unknown data path: %s", __func__, xpath);
221 iter_args->error = true;
222 return ITER_STOP;
223 }
224
225 /* Map operation values. */
226 switch (cdb_op) {
227 case MOP_CREATED:
228 nb_op = NB_OP_CREATE;
229 break;
230 case MOP_DELETED:
231 nb_op = NB_OP_DELETE;
232 break;
233 case MOP_VALUE_SET:
234 if (nb_operation_is_valid(NB_OP_MODIFY, nb_node->snode))
235 nb_op = NB_OP_MODIFY;
236 else
237 /* Ignore list keys modifications. */
238 return ITER_RECURSE;
239 break;
240 case MOP_MOVED_AFTER:
241 nb_op = NB_OP_MOVE;
242 break;
243 case MOP_MODIFIED:
244 /* We're not interested on this. */
245 return ITER_RECURSE;
246 default:
247 flog_err(EC_LIB_DEVELOPMENT,
248 "%s: unexpected operation %u [xpath %s]", __func__,
249 cdb_op, xpath);
250 iter_args->error = true;
251 return ITER_STOP;
252 }
253
254 /* Convert ConfD value to a string. */
255 if (nb_node->snode->nodetype != LYS_LEAFLIST && newv
256 && frr_confd_val2str(nb_node->xpath, newv, value_str,
257 sizeof(value_str))
258 != 0) {
259 flog_err(EC_LIB_CONFD_DATA_CONVERT,
260 "%s: failed to convert ConfD value to a string",
261 __func__);
262 iter_args->error = true;
263 return ITER_STOP;
264 }
265
266 /* Edit the candidate configuration. */
267 data = yang_data_new(xpath, value_str);
268 ret = nb_candidate_edit(iter_args->candidate, nb_node, nb_op, xpath,
269 NULL, data);
270 yang_data_free(data);
271 if (ret != NB_OK) {
272 flog_warn(
273 EC_LIB_NB_CANDIDATE_EDIT_ERROR,
274 "%s: failed to edit candidate configuration: operation [%s] xpath [%s]",
275 __func__, nb_operation_name(nb_op), xpath);
276 iter_args->error = true;
277 return ITER_STOP;
278 }
279
280 return ITER_RECURSE;
281 }
282
283 static int frr_confd_cdb_read_cb_prepare(int fd, int *subp, int reslen)
284 {
285 struct nb_config *candidate;
286 struct cdb_iter_args iter_args;
287 int ret;
288
289 candidate = nb_config_dup(running_config);
290
291 /* Iterate over all configuration changes. */
292 iter_args.candidate = candidate;
293 iter_args.error = false;
294 for (int i = 0; i < reslen; i++) {
295 if (cdb_diff_iterate(fd, subp[i], frr_confd_cdb_diff_iter,
296 ITER_WANT_PREV, &iter_args)
297 != CONFD_OK) {
298 flog_err_confd("cdb_diff_iterate");
299 }
300 }
301 free(subp);
302
303 if (iter_args.error) {
304 nb_config_free(candidate);
305
306 if (cdb_sub_abort_trans(
307 cdb_sub_sock, CONFD_ERRCODE_APPLICATION_INTERNAL, 0,
308 0, "Couldn't apply configuration changes")
309 != CONFD_OK) {
310 flog_err_confd("cdb_sub_abort_trans");
311 return -1;
312 }
313 return 0;
314 }
315
316 /*
317 * Validate the configuration changes and allocate all resources
318 * required to apply them.
319 */
320 transaction = NULL;
321 ret = nb_candidate_commit_prepare(candidate, NB_CLIENT_CONFD, NULL,
322 &transaction);
323 if (ret != NB_OK && ret != NB_ERR_NO_CHANGES) {
324 enum confd_errcode errcode;
325 const char *errmsg;
326
327 switch (ret) {
328 case NB_ERR_LOCKED:
329 errcode = CONFD_ERRCODE_IN_USE;
330 errmsg = "Configuration is locked by another process";
331 break;
332 case NB_ERR_RESOURCE:
333 errcode = CONFD_ERRCODE_RESOURCE_DENIED;
334 errmsg = "Failed do allocate resources";
335 break;
336 default:
337 errcode = CONFD_ERRCODE_INTERNAL;
338 errmsg = "Internal error";
339 break;
340 }
341
342 /* Reject the configuration changes. */
343 if (cdb_sub_abort_trans(cdb_sub_sock, errcode, 0, 0, "%s",
344 errmsg)
345 != CONFD_OK) {
346 flog_err_confd("cdb_sub_abort_trans");
347 return -1;
348 }
349 } else {
350 /* Acknowledge the notification. */
351 if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY)
352 != CONFD_OK) {
353 flog_err_confd("cdb_sync_subscription_socket");
354 return -1;
355 }
356
357 /* No configuration changes. */
358 if (!transaction)
359 nb_config_free(candidate);
360 }
361
362 return 0;
363 }
364
365 static int frr_confd_cdb_read_cb_commit(int fd, int *subp, int reslen)
366 {
367 /*
368 * No need to process the configuration changes again as we're already
369 * keeping track of them in the "transaction" variable.
370 */
371 free(subp);
372
373 /* Apply the transaction. */
374 if (transaction) {
375 struct nb_config *candidate = transaction->config;
376
377 nb_candidate_commit_apply(transaction, true, NULL);
378 nb_config_free(candidate);
379 }
380
381 /* Acknowledge the notification. */
382 if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) {
383 flog_err_confd("cdb_sync_subscription_socket");
384 return -1;
385 }
386
387 return 0;
388 }
389
390 static int frr_confd_cdb_read_cb_abort(int fd, int *subp, int reslen)
391 {
392 /*
393 * No need to process the configuration changes again as we're already
394 * keeping track of them in the "transaction" variable.
395 */
396 free(subp);
397
398 /* Abort the transaction. */
399 if (transaction) {
400 struct nb_config *candidate = transaction->config;
401
402 nb_candidate_commit_abort(transaction);
403 nb_config_free(candidate);
404 }
405
406 /* Acknowledge the notification. */
407 if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) {
408 flog_err_confd("cdb_sync_subscription_socket");
409 return -1;
410 }
411
412 return 0;
413 }
414
415 static int frr_confd_cdb_read_cb(struct thread *thread)
416 {
417 int fd = THREAD_FD(thread);
418 enum cdb_sub_notification cdb_ev;
419 int flags;
420 int *subp = NULL;
421 int reslen = 0;
422
423 thread = NULL;
424 thread_add_read(master, frr_confd_cdb_read_cb, NULL, fd, &thread);
425
426 if (cdb_read_subscription_socket2(fd, &cdb_ev, &flags, &subp, &reslen)
427 != CONFD_OK) {
428 flog_err_confd("cdb_read_subscription_socket2");
429 return -1;
430 }
431
432 switch (cdb_ev) {
433 case CDB_SUB_PREPARE:
434 return frr_confd_cdb_read_cb_prepare(fd, subp, reslen);
435 case CDB_SUB_COMMIT:
436 return frr_confd_cdb_read_cb_commit(fd, subp, reslen);
437 case CDB_SUB_ABORT:
438 return frr_confd_cdb_read_cb_abort(fd, subp, reslen);
439 default:
440 flog_err_confd("unknown CDB event");
441 return -1;
442 }
443 }
444
445 /* Trigger CDB subscriptions to read the startup configuration. */
446 static void *thread_cdb_trigger_subscriptions(void *data)
447 {
448 int sock;
449 int *sub_points = NULL, len = 0;
450 struct listnode *node;
451 int *spoint;
452 int i = 0;
453
454 /* Create CDB data socket. */
455 sock = socket(PF_INET, SOCK_STREAM, 0);
456 if (sock < 0) {
457 flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
458 __func__, safe_strerror(errno));
459 return NULL;
460 }
461
462 if (cdb_connect(sock, CDB_DATA_SOCKET, &confd_addr,
463 sizeof(struct sockaddr_in))
464 != CONFD_OK) {
465 flog_err_confd("cdb_connect");
466 return NULL;
467 }
468
469 /*
470 * Fill array containing the subscription point of all loaded YANG
471 * modules.
472 */
473 len = listcount(confd_spoints);
474 sub_points = XCALLOC(MTYPE_CONFD, len * sizeof(int));
475 for (ALL_LIST_ELEMENTS_RO(confd_spoints, node, spoint))
476 sub_points[i++] = *spoint;
477
478 if (cdb_trigger_subscriptions(sock, sub_points, len) != CONFD_OK) {
479 flog_err_confd("cdb_trigger_subscriptions");
480 return NULL;
481 }
482
483 /* Cleanup and exit thread. */
484 XFREE(MTYPE_CONFD, sub_points);
485 cdb_close(sock);
486
487 return NULL;
488 }
489
490 static int frr_confd_init_cdb(void)
491 {
492 struct yang_module *module;
493 pthread_t cdb_trigger_thread;
494
495 /* Create CDB subscription socket. */
496 cdb_sub_sock = socket(PF_INET, SOCK_STREAM, 0);
497 if (cdb_sub_sock < 0) {
498 flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
499 __func__, safe_strerror(errno));
500 return -1;
501 }
502
503 if (cdb_connect(cdb_sub_sock, CDB_SUBSCRIPTION_SOCKET, &confd_addr,
504 sizeof(struct sockaddr_in))
505 != CONFD_OK) {
506 flog_err_confd("cdb_connect");
507 goto error;
508 }
509
510 /* Subscribe to all loaded YANG data modules. */
511 confd_spoints = list_new();
512 RB_FOREACH (module, yang_modules, &yang_modules) {
513 struct lys_node *snode;
514
515 module->confd_hash = confd_str2hash(module->info->ns);
516 if (module->confd_hash == 0) {
517 flog_err(
518 EC_LIB_LIBCONFD,
519 "%s: failed to find hash value for namespace %s",
520 __func__, module->info->ns);
521 goto error;
522 }
523
524 /*
525 * The CDB API doesn't provide a mechanism to subscribe to an
526 * entire YANG module. So we have to find the top level
527 * nodes ourselves and subscribe to their paths.
528 */
529 LY_TREE_FOR (module->info->data, snode) {
530 struct nb_node *nb_node;
531 int *spoint;
532 int ret;
533
534 switch (snode->nodetype) {
535 case LYS_CONTAINER:
536 case LYS_LEAF:
537 case LYS_LEAFLIST:
538 case LYS_LIST:
539 break;
540 default:
541 continue;
542 }
543
544 if (CHECK_FLAG(snode->flags, LYS_CONFIG_R))
545 continue;
546
547 nb_node = snode->priv;
548 if (debug_northbound)
549 zlog_debug("%s: subscribing to '%s'", __func__,
550 nb_node->xpath);
551
552 spoint = XMALLOC(MTYPE_CONFD, sizeof(*spoint));
553 ret = cdb_subscribe2(
554 cdb_sub_sock, CDB_SUB_RUNNING_TWOPHASE,
555 CDB_SUB_WANT_ABORT_ON_ABORT, 3, spoint,
556 module->confd_hash, nb_node->xpath);
557 if (ret != CONFD_OK) {
558 flog_err_confd("cdb_subscribe2");
559 XFREE(MTYPE_CONFD, spoint);
560 }
561 listnode_add(confd_spoints, spoint);
562 }
563 }
564
565 if (cdb_subscribe_done(cdb_sub_sock) != CONFD_OK) {
566 flog_err_confd("cdb_subscribe_done");
567 goto error;
568 }
569
570 /* Create short lived pthread to trigger the CDB subscriptions. */
571 if (pthread_create(&cdb_trigger_thread, NULL,
572 thread_cdb_trigger_subscriptions, NULL)) {
573 flog_err(EC_LIB_SYSTEM_CALL, "%s: error creating pthread: %s",
574 __func__, safe_strerror(errno));
575 goto error;
576 }
577 pthread_detach(cdb_trigger_thread);
578
579 thread_add_read(master, frr_confd_cdb_read_cb, NULL, cdb_sub_sock,
580 &t_cdb_sub);
581
582 return 0;
583
584 error:
585 frr_confd_finish_cdb();
586
587 return -1;
588 }
589
590 static void frr_confd_finish_cdb(void)
591 {
592 if (cdb_sub_sock > 0) {
593 THREAD_OFF(t_cdb_sub);
594 cdb_close(cdb_sub_sock);
595 }
596 }
597
598 /* ------------ DP code ------------ */
599
600 static int frr_confd_transaction_init(struct confd_trans_ctx *tctx)
601 {
602 confd_trans_set_fd(tctx, dp_worker_sock);
603
604 return CONFD_OK;
605 }
606
607 #define CONFD_MAX_CHILD_NODES 32
608
609 static int frr_confd_data_get_elem(struct confd_trans_ctx *tctx,
610 confd_hkeypath_t *kp)
611 {
612 struct nb_node *nb_node;
613 char xpath[BUFSIZ];
614 struct yang_data *data;
615 confd_value_t v;
616 const void *list_entry = NULL;
617
618 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
619
620 nb_node = nb_node_find(xpath);
621 if (!nb_node) {
622 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
623 "%s: unknown data path: %s", __func__, xpath);
624 confd_data_reply_not_found(tctx);
625 return CONFD_OK;
626 }
627
628 if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) {
629 confd_data_reply_not_found(tctx);
630 return CONFD_OK;
631 }
632
633 data = nb_node->cbs.get_elem(xpath, list_entry);
634 if (data) {
635 if (data->value) {
636 CONFD_SET_STR(&v, data->value);
637 confd_data_reply_value(tctx, &v);
638 } else
639 confd_data_reply_found(tctx);
640 yang_data_free(data);
641 } else
642 confd_data_reply_not_found(tctx);
643
644 return CONFD_OK;
645 }
646
647 static int frr_confd_data_get_next(struct confd_trans_ctx *tctx,
648 confd_hkeypath_t *kp, long next)
649 {
650 struct nb_node *nb_node;
651 char xpath[BUFSIZ];
652 struct yang_data *data;
653 const void *parent_list_entry, *nb_next;
654 confd_value_t v[LIST_MAXKEYS];
655
656 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
657
658 nb_node = nb_node_find(xpath);
659 if (!nb_node) {
660 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
661 "%s: unknown data path: %s", __func__, xpath);
662 confd_data_reply_next_key(tctx, NULL, -1, -1);
663 return CONFD_OK;
664 }
665
666 if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry)
667 != 0) {
668 /* List entry doesn't exist anymore. */
669 confd_data_reply_next_key(tctx, NULL, -1, -1);
670 return CONFD_OK;
671 }
672
673 nb_next = nb_node->cbs.get_next(parent_list_entry,
674 (next == -1) ? NULL : (void *)next);
675 if (!nb_next) {
676 /* End of the list or leaf-list. */
677 confd_data_reply_next_key(tctx, NULL, -1, -1);
678 return CONFD_OK;
679 }
680
681 switch (nb_node->snode->nodetype) {
682 case LYS_LIST:
683 if (!CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) {
684 struct yang_list_keys keys;
685
686 memset(&keys, 0, sizeof(keys));
687 if (nb_node->cbs.get_keys(nb_next, &keys) != NB_OK) {
688 flog_warn(EC_LIB_NB_CB_STATE,
689 "%s: failed to get list keys",
690 __func__);
691 confd_data_reply_next_key(tctx, NULL, -1, -1);
692 return CONFD_OK;
693 }
694
695 /* Feed keys to ConfD. */
696 for (size_t i = 0; i < keys.num; i++)
697 CONFD_SET_STR(&v[i], keys.key[i]);
698 confd_data_reply_next_key(tctx, v, keys.num,
699 (long)nb_next);
700 } else {
701 char pointer_str[16];
702
703 /*
704 * ConfD 6.6 user guide, chapter 6.11 (Operational data
705 * lists without keys):
706 * "To support this without having completely separate
707 * APIs, we use a "pseudo" key in the ConfD APIs for
708 * this type of list. This key is not part of the data
709 * model, and completely hidden in the northbound agent
710 * interfaces, but is used with e.g. the get_next() and
711 * get_elem() callbacks as if it were a normal key. This
712 * "pseudo" key is always a single signed 64-bit
713 * integer, i.e. the confd_value_t type is C_INT64. The
714 * values can be chosen arbitrarily by the application,
715 * as long as a key value returned by get_next() can be
716 * used to get the data for the corresponding list entry
717 * with get_elem() or get_object() as usual. It could
718 * e.g. be an index into an array that holds the data,
719 * or even a memory address in integer form".
720 *
721 * Since we're using the CONFD_DAEMON_FLAG_STRINGSONLY
722 * option, we must convert our pseudo-key (a void
723 * pointer) to a string before sending it to confd.
724 */
725 snprintf(pointer_str, sizeof(pointer_str), "%lu",
726 (unsigned long)nb_next);
727 CONFD_SET_STR(&v[0], pointer_str);
728 confd_data_reply_next_key(tctx, v, 1, (long)nb_next);
729 }
730 break;
731 case LYS_LEAFLIST:
732 data = nb_node->cbs.get_elem(xpath, nb_next);
733 if (data) {
734 if (data->value) {
735 CONFD_SET_STR(&v[0], data->value);
736 confd_data_reply_next_key(tctx, v, 1,
737 (long)nb_next);
738 }
739 yang_data_free(data);
740 } else
741 confd_data_reply_next_key(tctx, NULL, -1, -1);
742 break;
743 default:
744 break;
745 }
746
747 return CONFD_OK;
748 }
749
750 /*
751 * Optional callback - implemented for performance reasons.
752 */
753 static int frr_confd_data_get_object(struct confd_trans_ctx *tctx,
754 confd_hkeypath_t *kp)
755 {
756 struct nb_node *nb_node;
757 const struct lys_node *child;
758 char xpath[BUFSIZ];
759 char xpath_child[XPATH_MAXLEN];
760 struct list *elements;
761 struct yang_data *data;
762 const void *list_entry;
763 confd_value_t values[CONFD_MAX_CHILD_NODES];
764 size_t nvalues = 0;
765
766 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
767
768 nb_node = nb_node_find(xpath);
769 if (!nb_node) {
770 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
771 "%s: unknown data path: %s", __func__, xpath);
772 confd_data_reply_not_found(tctx);
773 return CONFD_ERR;
774 }
775
776 if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) {
777 confd_data_reply_not_found(tctx);
778 return CONFD_OK;
779 }
780
781 elements = yang_data_list_new();
782
783 /* Loop through list child nodes. */
784 LY_TREE_FOR (nb_node->snode->child, child) {
785 struct nb_node *nb_node_child = child->priv;
786 confd_value_t *v;
787
788 if (nvalues > CONFD_MAX_CHILD_NODES)
789 break;
790
791 v = &values[nvalues++];
792
793 /* Non-presence containers, lists and leaf-lists. */
794 if (!nb_node_child->cbs.get_elem) {
795 CONFD_SET_NOEXISTS(v);
796 continue;
797 }
798
799 snprintf(xpath_child, sizeof(xpath_child), "%s/%s", xpath,
800 child->name);
801 data = nb_node_child->cbs.get_elem(xpath_child, list_entry);
802 if (data) {
803 if (data->value)
804 CONFD_SET_STR(v, data->value);
805 else {
806 /* Presence containers and empty leafs. */
807 CONFD_SET_XMLTAG(
808 v, nb_node_child->confd_hash,
809 confd_str2hash(nb_node_child->snode
810 ->module->ns));
811 }
812 listnode_add(elements, data);
813 } else
814 CONFD_SET_NOEXISTS(v);
815 }
816
817 confd_data_reply_value_array(tctx, values, nvalues);
818
819 /* Release memory. */
820 list_delete(&elements);
821
822 return CONFD_OK;
823 }
824
825 /*
826 * Optional callback - implemented for performance reasons.
827 */
828 static int frr_confd_data_get_next_object(struct confd_trans_ctx *tctx,
829 confd_hkeypath_t *kp, long next)
830 {
831 char xpath[BUFSIZ];
832 struct nb_node *nb_node;
833 struct list *elements;
834 const void *parent_list_entry;
835 const void *nb_next;
836 #define CONFD_OBJECTS_PER_TIME 100
837 struct confd_next_object objects[CONFD_OBJECTS_PER_TIME + 1];
838 char pseudo_keys[CONFD_OBJECTS_PER_TIME][16];
839 int nobjects = 0;
840
841 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
842
843 nb_node = nb_node_find(xpath);
844 if (!nb_node) {
845 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
846 "%s: unknown data path: %s", __func__, xpath);
847 confd_data_reply_next_object_array(tctx, NULL, 0, 0);
848 return CONFD_OK;
849 }
850
851 if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry)
852 != 0) {
853 confd_data_reply_next_object_array(tctx, NULL, 0, 0);
854 return CONFD_OK;
855 }
856
857 elements = yang_data_list_new();
858 nb_next = (next == -1) ? NULL : (void *)next;
859
860 memset(objects, 0, sizeof(objects));
861 for (int j = 0; j < CONFD_OBJECTS_PER_TIME; j++) {
862 struct confd_next_object *object;
863 struct lys_node *child;
864 struct yang_data *data;
865 size_t nvalues = 0;
866
867 object = &objects[j];
868
869 nb_next = nb_node->cbs.get_next(parent_list_entry, nb_next);
870 if (!nb_next)
871 /* End of the list. */
872 break;
873
874 object->next = (long)nb_next;
875
876 /* Leaf-lists require special handling. */
877 if (nb_node->snode->nodetype == LYS_LEAFLIST) {
878 object->v = XMALLOC(MTYPE_CONFD, sizeof(confd_value_t));
879 data = nb_node->cbs.get_elem(xpath, nb_next);
880 assert(data && data->value);
881 CONFD_SET_STR(object->v, data->value);
882 nvalues++;
883 listnode_add(elements, data);
884 goto next;
885 }
886
887 object->v =
888 XMALLOC(MTYPE_CONFD,
889 CONFD_MAX_CHILD_NODES * sizeof(confd_value_t));
890
891 /*
892 * ConfD 6.6 user guide, chapter 6.11 (Operational data lists
893 * without keys):
894 * "In the response to the get_next_object() callback, the data
895 * provider is expected to provide the key values along with the
896 * other leafs in an array that is populated according to the
897 * data model. This must be done also for this type of list,
898 * even though the key isn't actually in the data model. The
899 * "pseudo" key must always be the first element in the array".
900 */
901 if (CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) {
902 confd_value_t *v;
903
904 snprintf(pseudo_keys[j], sizeof(pseudo_keys[j]), "%lu",
905 (unsigned long)nb_next);
906
907 v = &object->v[nvalues++];
908 CONFD_SET_STR(v, pseudo_keys[j]);
909 }
910
911 /* Loop through list child nodes. */
912 LY_TREE_FOR (nb_node->snode->child, child) {
913 struct nb_node *nb_node_child = child->priv;
914 char xpath_child[XPATH_MAXLEN];
915 confd_value_t *v;
916
917 if (nvalues > CONFD_MAX_CHILD_NODES)
918 break;
919
920 v = &object->v[nvalues++];
921
922 /* Non-presence containers, lists and leaf-lists. */
923 if (!nb_node_child->cbs.get_elem) {
924 CONFD_SET_NOEXISTS(v);
925 continue;
926 }
927
928 snprintf(xpath_child, sizeof(xpath_child), "%s/%s",
929 xpath, child->name);
930 data = nb_node_child->cbs.get_elem(xpath_child,
931 nb_next);
932 if (data) {
933 if (data->value)
934 CONFD_SET_STR(v, data->value);
935 else {
936 /*
937 * Presence containers and empty leafs.
938 */
939 CONFD_SET_XMLTAG(
940 v, nb_node_child->confd_hash,
941 confd_str2hash(
942 nb_node_child->snode
943 ->module->ns));
944 }
945 listnode_add(elements, data);
946 } else
947 CONFD_SET_NOEXISTS(v);
948 }
949 next:
950 object->n = nvalues;
951 nobjects++;
952 }
953
954 if (nobjects == 0) {
955 confd_data_reply_next_object_array(tctx, NULL, 0, 0);
956 list_delete(&elements);
957 return CONFD_OK;
958 }
959
960 /* Detect end of the list. */
961 if (!nb_next) {
962 nobjects++;
963 objects[nobjects].v = NULL;
964 }
965
966 /* Reply to ConfD. */
967 confd_data_reply_next_object_arrays(tctx, objects, nobjects, 0);
968 if (!nb_next)
969 nobjects--;
970
971 /* Release memory. */
972 list_delete(&elements);
973 for (int j = 0; j < nobjects; j++) {
974 struct confd_next_object *object;
975
976 object = &objects[j];
977 XFREE(MTYPE_CONFD, object->v);
978 }
979
980 return CONFD_OK;
981 }
982
983 static int frr_confd_notification_send(const char *xpath,
984 struct list *arguments)
985 {
986 struct nb_node *nb_node;
987 struct yang_module *module;
988 struct confd_datetime now;
989 confd_tag_value_t *values;
990 int nvalues;
991 int i = 0;
992 struct yang_data *data;
993 struct listnode *node;
994 int ret;
995
996 nb_node = nb_node_find(xpath);
997 if (!nb_node) {
998 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
999 "%s: unknown data path: %s", __func__, xpath);
1000 return -1;
1001 }
1002 module = yang_module_find(nb_node->snode->module->name);
1003 assert(module);
1004
1005 nvalues = 2;
1006 if (arguments)
1007 nvalues += listcount(arguments);
1008
1009 values = XMALLOC(MTYPE_CONFD, nvalues * sizeof(*values));
1010
1011 CONFD_SET_TAG_XMLBEGIN(&values[i++], nb_node->confd_hash,
1012 module->confd_hash);
1013 for (ALL_LIST_ELEMENTS_RO(arguments, node, data)) {
1014 struct nb_node *nb_node_arg;
1015
1016 nb_node_arg = nb_node_find(data->xpath);
1017 if (!nb_node_arg) {
1018 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
1019 "%s: unknown data path: %s", __func__,
1020 data->xpath);
1021 XFREE(MTYPE_CONFD, values);
1022 return NB_ERR;
1023 }
1024
1025 CONFD_SET_TAG_STR(&values[i++], nb_node_arg->confd_hash,
1026 data->value);
1027 }
1028 CONFD_SET_TAG_XMLEND(&values[i++], nb_node->confd_hash,
1029 module->confd_hash);
1030
1031 getdatetime(&now);
1032 ret = confd_notification_send(live_ctx, &now, values, nvalues);
1033
1034 /* Release memory. */
1035 XFREE(MTYPE_CONFD, values);
1036
1037 /* Map ConfD return code to northbound return code. */
1038 switch (ret) {
1039 case CONFD_OK:
1040 return NB_OK;
1041 default:
1042 return NB_ERR;
1043 }
1044 }
1045
1046 static int frr_confd_action_init(struct confd_user_info *uinfo)
1047 {
1048 confd_action_set_fd(uinfo, dp_worker_sock);
1049
1050 return CONFD_OK;
1051 }
1052
1053 static int frr_confd_action_execute(struct confd_user_info *uinfo,
1054 struct xml_tag *name, confd_hkeypath_t *kp,
1055 confd_tag_value_t *params, int nparams)
1056 {
1057 char xpath[BUFSIZ];
1058 struct nb_node *nb_node;
1059 struct list *input;
1060 struct list *output;
1061 struct yang_data *data;
1062 confd_tag_value_t *reply;
1063 int ret = CONFD_OK;
1064
1065 /* Getting the XPath is tricky. */
1066 if (kp) {
1067 /* This is a YANG RPC. */
1068 frr_confd_get_xpath(kp, xpath, sizeof(xpath));
1069 strlcat(xpath, "/", sizeof(xpath));
1070 strlcat(xpath, confd_hash2str(name->tag), sizeof(xpath));
1071 } else {
1072 /* This is a YANG action. */
1073 snprintf(xpath, sizeof(xpath), "/%s:%s",
1074 confd_ns2prefix(name->ns), confd_hash2str(name->tag));
1075 }
1076
1077 nb_node = nb_node_find(xpath);
1078 if (!nb_node) {
1079 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
1080 "%s: unknown data path: %s", __func__, xpath);
1081 return CONFD_ERR;
1082 }
1083
1084 input = yang_data_list_new();
1085 output = yang_data_list_new();
1086
1087 /* Process input nodes. */
1088 for (int i = 0; i < nparams; i++) {
1089 char xpath_input[BUFSIZ];
1090 char value_str[YANG_VALUE_MAXLEN];
1091
1092 snprintf(xpath_input, sizeof(xpath_input), "%s/%s", xpath,
1093 confd_hash2str(params[i].tag.tag));
1094
1095 if (frr_confd_val2str(xpath_input, &params[i].v, value_str,
1096 sizeof(value_str))
1097 != 0) {
1098 flog_err(
1099 EC_LIB_CONFD_DATA_CONVERT,
1100 "%s: failed to convert ConfD value to a string",
1101 __func__);
1102 ret = CONFD_ERR;
1103 goto exit;
1104 }
1105
1106 data = yang_data_new(xpath_input, value_str);
1107 listnode_add(input, data);
1108 }
1109
1110 /* Execute callback registered for this XPath. */
1111 if (nb_node->cbs.rpc(xpath, input, output) != NB_OK) {
1112 flog_warn(EC_LIB_NB_CB_RPC, "%s: rpc callback failed: %s",
1113 __func__, xpath);
1114 ret = CONFD_ERR;
1115 goto exit;
1116 }
1117
1118 /* Process output nodes. */
1119 if (listcount(output) > 0) {
1120 struct listnode *node;
1121 int i = 0;
1122
1123 reply = XMALLOC(MTYPE_CONFD,
1124 listcount(output) * sizeof(*reply));
1125
1126 for (ALL_LIST_ELEMENTS_RO(output, node, data)) {
1127 struct nb_node *nb_node_output;
1128 int hash;
1129
1130 nb_node_output = nb_node_find(data->xpath);
1131 if (!nb_node_output) {
1132 flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH,
1133 "%s: unknown data path: %s", __func__,
1134 data->xpath);
1135 goto exit;
1136 }
1137
1138 hash = confd_str2hash(nb_node_output->snode->name);
1139 CONFD_SET_TAG_STR(&reply[i++], hash, data->value);
1140 }
1141 confd_action_reply_values(uinfo, reply, listcount(output));
1142 XFREE(MTYPE_CONFD, reply);
1143 }
1144
1145 exit:
1146 /* Release memory. */
1147 list_delete(&input);
1148 list_delete(&output);
1149
1150 return ret;
1151 }
1152
1153
1154 static int frr_confd_dp_read(struct thread *thread)
1155 {
1156 struct confd_daemon_ctx *dctx = THREAD_ARG(thread);
1157 int fd = THREAD_FD(thread);
1158 int ret;
1159
1160 thread = NULL;
1161 thread_add_read(master, frr_confd_dp_read, dctx, fd, &thread);
1162
1163 ret = confd_fd_ready(dctx, fd);
1164 if (ret == CONFD_EOF) {
1165 flog_err_confd("confd_fd_ready");
1166 frr_confd_finish();
1167 return -1;
1168 } else if (ret == CONFD_ERR && confd_errno != CONFD_ERR_EXTERNAL) {
1169 flog_err_confd("confd_fd_ready");
1170 frr_confd_finish();
1171 return -1;
1172 }
1173
1174 return 0;
1175 }
1176
1177 static int frr_confd_subscribe_state(const struct lys_node *snode, void *arg)
1178 {
1179 struct nb_node *nb_node = snode->priv;
1180 struct confd_data_cbs *data_cbs = arg;
1181
1182 if (!CHECK_FLAG(snode->flags, LYS_CONFIG_R))
1183 return YANG_ITER_CONTINUE;
1184 /* We only need to subscribe to the root of the state subtrees. */
1185 if (snode->parent && CHECK_FLAG(snode->parent->flags, LYS_CONFIG_R))
1186 return YANG_ITER_CONTINUE;
1187
1188 if (debug_northbound)
1189 zlog_debug("%s: providing data to '%s' (callpoint %s)",
1190 __func__, nb_node->xpath, snode->name);
1191
1192 strlcpy(data_cbs->callpoint, snode->name, sizeof(data_cbs->callpoint));
1193 if (confd_register_data_cb(dctx, data_cbs) != CONFD_OK)
1194 flog_err_confd("confd_register_data_cb");
1195
1196 return YANG_ITER_CONTINUE;
1197 }
1198
1199 static int frr_confd_init_dp(const char *program_name)
1200 {
1201 struct confd_trans_cbs trans_cbs;
1202 struct confd_data_cbs data_cbs;
1203 struct confd_notification_stream_cbs ncbs;
1204 struct confd_action_cbs acbs;
1205
1206 /* Initialize daemon context. */
1207 dctx = confd_init_daemon(program_name);
1208 if (!dctx) {
1209 flog_err_confd("confd_init_daemon");
1210 goto error;
1211 }
1212
1213 /*
1214 * Inform we want to receive YANG values as raw strings, and that we
1215 * want to provide only strings in the reply functions, regardless of
1216 * the YANG type.
1217 */
1218 confd_set_daemon_flags(dctx, CONFD_DAEMON_FLAG_STRINGSONLY);
1219
1220 /* Establish a control socket. */
1221 dp_ctl_sock = socket(PF_INET, SOCK_STREAM, 0);
1222 if (dp_ctl_sock < 0) {
1223 flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
1224 __func__, safe_strerror(errno));
1225 goto error;
1226 }
1227
1228 if (confd_connect(dctx, dp_ctl_sock, CONTROL_SOCKET, &confd_addr,
1229 sizeof(struct sockaddr_in))
1230 != CONFD_OK) {
1231 flog_err_confd("confd_connect");
1232 goto error;
1233 }
1234
1235 /*
1236 * Establish a worker socket (only one since this plugin runs on a
1237 * single thread).
1238 */
1239 dp_worker_sock = socket(PF_INET, SOCK_STREAM, 0);
1240 if (dp_worker_sock < 0) {
1241 flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s",
1242 __func__, safe_strerror(errno));
1243 goto error;
1244 }
1245 if (confd_connect(dctx, dp_worker_sock, WORKER_SOCKET, &confd_addr,
1246 sizeof(struct sockaddr_in))
1247 != CONFD_OK) {
1248 flog_err_confd("confd_connect");
1249 goto error;
1250 }
1251
1252 /* Register transaction callback functions. */
1253 memset(&trans_cbs, 0, sizeof(trans_cbs));
1254 trans_cbs.init = frr_confd_transaction_init;
1255 confd_register_trans_cb(dctx, &trans_cbs);
1256
1257 /* Register our read/write callbacks. */
1258 memset(&data_cbs, 0, sizeof(data_cbs));
1259 data_cbs.get_elem = frr_confd_data_get_elem;
1260 data_cbs.exists_optional = frr_confd_data_get_elem;
1261 data_cbs.get_next = frr_confd_data_get_next;
1262 data_cbs.get_object = frr_confd_data_get_object;
1263 data_cbs.get_next_object = frr_confd_data_get_next_object;
1264
1265 /*
1266 * Iterate over all loaded YANG modules and subscribe to the paths
1267 * referent to state data.
1268 */
1269 yang_snodes_iterate_all(frr_confd_subscribe_state, 0, &data_cbs);
1270
1271 /* Register notification stream. */
1272 memset(&ncbs, 0, sizeof(ncbs));
1273 ncbs.fd = dp_worker_sock;
1274 /*
1275 * RFC 5277 - Section 3.2.3:
1276 * A NETCONF server implementation supporting the notification
1277 * capability MUST support the "NETCONF" notification event
1278 * stream. This stream contains all NETCONF XML event notifications
1279 * supported by the NETCONF server.
1280 */
1281 strlcpy(ncbs.streamname, "NETCONF", sizeof(ncbs.streamname));
1282 if (confd_register_notification_stream(dctx, &ncbs, &live_ctx)
1283 != CONFD_OK) {
1284 flog_err_confd("confd_register_notification_stream");
1285 goto error;
1286 }
1287
1288 /* Register the action handler callback. */
1289 memset(&acbs, 0, sizeof(acbs));
1290 strlcpy(acbs.actionpoint, "actionpoint", sizeof(acbs.actionpoint));
1291 acbs.init = frr_confd_action_init;
1292 acbs.action = frr_confd_action_execute;
1293 if (confd_register_action_cbs(dctx, &acbs) != CONFD_OK) {
1294 flog_err_confd("confd_register_action_cbs");
1295 goto error;
1296 }
1297
1298 /* Notify we registered all callbacks we wanted. */
1299 if (confd_register_done(dctx) != CONFD_OK) {
1300 flog_err_confd("confd_register_done");
1301 goto error;
1302 }
1303
1304 thread_add_read(master, frr_confd_dp_read, dctx, dp_ctl_sock,
1305 &t_dp_ctl);
1306 thread_add_read(master, frr_confd_dp_read, dctx, dp_worker_sock,
1307 &t_dp_worker);
1308
1309 return 0;
1310
1311 error:
1312 frr_confd_finish_dp();
1313
1314 return -1;
1315 }
1316
1317 static void frr_confd_finish_dp(void)
1318 {
1319 if (dp_worker_sock > 0) {
1320 THREAD_OFF(t_dp_worker);
1321 close(dp_worker_sock);
1322 }
1323 if (dp_ctl_sock > 0) {
1324 THREAD_OFF(t_dp_ctl);
1325 close(dp_ctl_sock);
1326 }
1327 if (dctx != NULL)
1328 confd_release_daemon(dctx);
1329 }
1330
1331 /* ------------ Main ------------ */
1332
1333 static int frr_confd_calculate_snode_hash(const struct lys_node *snode,
1334 void *arg)
1335 {
1336 struct nb_node *nb_node = snode->priv;
1337
1338 nb_node->confd_hash = confd_str2hash(snode->name);
1339
1340 return YANG_ITER_CONTINUE;
1341 }
1342
1343 static int frr_confd_init(const char *program_name)
1344 {
1345 struct sockaddr_in *confd_addr4 = (struct sockaddr_in *)&confd_addr;
1346 int debuglevel = CONFD_SILENT;
1347 int ret = -1;
1348
1349 /* Initialize ConfD library. */
1350 confd_init(program_name, stderr, debuglevel);
1351
1352 confd_addr4->sin_family = AF_INET;
1353 confd_addr4->sin_addr.s_addr = inet_addr("127.0.0.1");
1354 confd_addr4->sin_port = htons(CONFD_PORT);
1355 if (confd_load_schemas(&confd_addr, sizeof(struct sockaddr_in))
1356 != CONFD_OK) {
1357 flog_err_confd("confd_load_schemas");
1358 return -1;
1359 }
1360
1361 ret = frr_confd_init_cdb();
1362 if (ret != 0)
1363 goto error;
1364
1365 ret = frr_confd_init_dp(program_name);
1366 if (ret != 0) {
1367 frr_confd_finish_cdb();
1368 goto error;
1369 }
1370
1371 yang_snodes_iterate_all(frr_confd_calculate_snode_hash, 0, NULL);
1372
1373 hook_register(nb_notification_send, frr_confd_notification_send);
1374
1375 confd_connected = true;
1376 return 0;
1377
1378 error:
1379 confd_free_schemas();
1380
1381 return ret;
1382 }
1383
1384 static int frr_confd_finish(void)
1385 {
1386 if (confd_connected == false)
1387 return 0;
1388
1389 frr_confd_finish_cdb();
1390 frr_confd_finish_dp();
1391
1392 confd_free_schemas();
1393
1394 confd_connected = false;
1395
1396 return 0;
1397 }
1398
1399 static int frr_confd_module_late_init(struct thread_master *tm)
1400 {
1401 master = tm;
1402
1403 if (frr_confd_init(frr_get_progname()) < 0) {
1404 flog_err(EC_LIB_CONFD_INIT,
1405 "failed to initialize the ConfD module");
1406 return -1;
1407 }
1408
1409 hook_register(frr_fini, frr_confd_finish);
1410
1411 return 0;
1412 }
1413
1414 static int frr_confd_module_init(void)
1415 {
1416 hook_register(frr_late_init, frr_confd_module_late_init);
1417
1418 return 0;
1419 }
1420
1421 FRR_MODULE_SETUP(.name = "frr_confd", .version = FRR_VERSION,
1422 .description = "FRR ConfD integration module",
1423 .init = frr_confd_module_init, )