diff options
Diffstat (limited to 'lib/northbound_confd.c')
-rw-r--r-- | lib/northbound_confd.c | 1494 |
1 files changed, 1494 insertions, 0 deletions
diff --git a/lib/northbound_confd.c b/lib/northbound_confd.c new file mode 100644 index 0000000..34406a1 --- /dev/null +++ b/lib/northbound_confd.c @@ -0,0 +1,1494 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * Copyright (C) 2018 NetDEF, Inc. + * Renato Westphal + */ + +#include <zebra.h> + +#include "log.h" +#include "lib_errors.h" +#include "command.h" +#include "debug.h" +#include "libfrr.h" +#include "lib/version.h" +#include "northbound.h" + +#include <confd_lib.h> +#include <confd_cdb.h> +#include <confd_dp.h> +#include <confd_maapi.h> + +DEFINE_MTYPE_STATIC(LIB, CONFD, "ConfD module"); + +static struct debug nb_dbg_client_confd = {0, "Northbound client: ConfD"}; + +static struct event_loop *master; +static struct sockaddr confd_addr; +static int cdb_sub_sock, dp_ctl_sock, dp_worker_sock; +static struct event *t_cdb_sub, *t_dp_ctl, *t_dp_worker; +static struct confd_daemon_ctx *dctx; +static struct confd_notification_ctx *live_ctx; +static bool confd_connected; +static struct list *confd_spoints; +static struct nb_transaction *transaction; + +static void frr_confd_finish_cdb(void); +static void frr_confd_finish_dp(void); +static int frr_confd_finish(void); + +#define flog_err_confd(funcname) \ + flog_err(EC_LIB_LIBCONFD, "%s: %s() failed: %s (%d): %s", __func__, \ + (funcname), confd_strerror(confd_errno), confd_errno, \ + confd_lasterr()) + + +/* ------------ Utils ------------ */ + +/* Get XPath string from ConfD hashed keypath. */ +static void frr_confd_get_xpath(const confd_hkeypath_t *kp, char *xpath, + size_t len) +{ + char *p; + + confd_xpath_pp_kpath(xpath, len, 0, kp); + + /* + * Replace double quotes by single quotes (the format accepted by the + * northbound API). + */ + p = xpath; + while ((p = strchr(p, '"')) != NULL) + *p++ = '\''; +} + +/* Convert ConfD binary value to a string. */ +static int frr_confd_val2str(const char *xpath, const confd_value_t *value, + char *string, size_t string_size) +{ + struct confd_cs_node *csp; + + csp = confd_cs_node_cd(NULL, xpath); + if (!csp) { + flog_err_confd("confd_cs_node_cd"); + return -1; + } + if (confd_val2str(csp->info.type, value, string, string_size) + == CONFD_ERR) { + flog_err_confd("confd_val2str"); + return -1; + } + + return 0; +} + +/* Obtain list entry from ConfD hashed keypath. */ +static int frr_confd_hkeypath_get_list_entry(const confd_hkeypath_t *kp, + struct nb_node *nb_node, + const void **list_entry) +{ + struct nb_node *nb_node_list; + int parent_lists = 0; + int curr_list = 0; + + *list_entry = NULL; + + /* + * Count the number of YANG lists in the path, disconsidering the + * last element. + */ + nb_node_list = nb_node; + while (nb_node_list->parent_list) { + nb_node_list = nb_node_list->parent_list; + parent_lists++; + } + if (nb_node->snode->nodetype != LYS_LIST && parent_lists == 0) + return 0; + + /* Start from the beginning and move down the tree. */ + for (int i = kp->len; i >= 0; i--) { + struct yang_list_keys keys; + + /* Not a YANG list. */ + if (kp->v[i][0].type != C_BUF) + continue; + + /* Obtain list keys. */ + memset(&keys, 0, sizeof(keys)); + for (int j = 0; kp->v[i][j].type != C_NOEXISTS; j++) { + strlcpy(keys.key[keys.num], + (char *)kp->v[i][j].val.buf.ptr, + sizeof(keys.key[keys.num])); + keys.num++; + } + + /* Obtain northbound node associated to the YANG list. */ + nb_node_list = nb_node; + for (int j = curr_list; j < parent_lists; j++) + nb_node_list = nb_node_list->parent_list; + + /* Obtain list entry. */ + if (!CHECK_FLAG(nb_node_list->flags, F_NB_NODE_KEYLESS_LIST)) { + *list_entry = nb_callback_lookup_entry( + nb_node, *list_entry, &keys); + if (*list_entry == NULL) + return -1; + } else { + unsigned long ptr_ulong; + + /* Retrieve list entry from pseudo-key (string). */ + if (sscanf(keys.key[0], "%lu", &ptr_ulong) != 1) + return -1; + *list_entry = (const void *)ptr_ulong; + } + + curr_list++; + } + + return 0; +} + +/* Fill the current date and time into a confd_datetime structure. */ +static void getdatetime(struct confd_datetime *datetime) +{ + struct tm tm; + struct timeval tv; + + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + + memset(datetime, 0, sizeof(*datetime)); + datetime->year = 1900 + tm.tm_year; + datetime->month = tm.tm_mon + 1; + datetime->day = tm.tm_mday; + datetime->sec = tm.tm_sec; + datetime->micro = tv.tv_usec; + datetime->timezone = 0; + datetime->timezone_minutes = 0; + datetime->hour = tm.tm_hour; + datetime->min = tm.tm_min; +} + +/* ------------ CDB code ------------ */ + +struct cdb_iter_args { + struct nb_config *candidate; + bool error; +}; + +static enum cdb_iter_ret +frr_confd_cdb_diff_iter(confd_hkeypath_t *kp, enum cdb_iter_op cdb_op, + confd_value_t *oldv, confd_value_t *newv, void *args) +{ + char xpath[XPATH_MAXLEN]; + struct nb_node *nb_node; + enum nb_operation nb_op; + struct cdb_iter_args *iter_args = args; + char value_str[YANG_VALUE_MAXLEN]; + struct yang_data *data; + char *sb1, *sb2; + int ret; + + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + + /* + * HACK: obtain value of leaf-list elements from the XPath due to + * a bug in the ConfD API. + */ + value_str[0] = '\0'; + sb1 = strrchr(xpath, '['); + sb2 = strrchr(xpath, ']'); + if (sb1 && sb2 && !strchr(sb1, '=')) { + *sb2 = '\0'; + strlcpy(value_str, sb1 + 1, sizeof(value_str)); + *sb1 = '\0'; + } + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + iter_args->error = true; + return ITER_STOP; + } + + /* Map operation values. */ + switch (cdb_op) { + case MOP_CREATED: + nb_op = NB_OP_CREATE; + break; + case MOP_DELETED: + nb_op = NB_OP_DESTROY; + break; + case MOP_VALUE_SET: + if (nb_operation_is_valid(NB_OP_MODIFY, nb_node->snode)) + nb_op = NB_OP_MODIFY; + else + /* Ignore list keys modifications. */ + return ITER_RECURSE; + break; + case MOP_MOVED_AFTER: + nb_op = NB_OP_MOVE; + break; + case MOP_MODIFIED: + /* We're not interested on this. */ + return ITER_RECURSE; + default: + flog_err(EC_LIB_DEVELOPMENT, + "%s: unexpected operation %u [xpath %s]", __func__, + cdb_op, xpath); + iter_args->error = true; + return ITER_STOP; + } + + /* Convert ConfD value to a string. */ + if (nb_node->snode->nodetype != LYS_LEAFLIST && newv + && frr_confd_val2str(nb_node->xpath, newv, value_str, + sizeof(value_str)) + != 0) { + flog_err(EC_LIB_CONFD_DATA_CONVERT, + "%s: failed to convert ConfD value to a string", + __func__); + iter_args->error = true; + return ITER_STOP; + } + + /* Edit the candidate configuration. */ + data = yang_data_new(xpath, value_str); + ret = nb_candidate_edit(iter_args->candidate, nb_node, nb_op, xpath, + NULL, data); + yang_data_free(data); + if (ret != NB_OK) { + flog_warn( + EC_LIB_NB_CANDIDATE_EDIT_ERROR, + "%s: failed to edit candidate configuration: operation [%s] xpath [%s]", + __func__, nb_operation_name(nb_op), xpath); + iter_args->error = true; + return ITER_STOP; + } + + return ITER_RECURSE; +} + +static void frr_confd_cdb_read_cb_prepare(int fd, int *subp, int reslen) +{ + struct nb_context context = {}; + struct nb_config *candidate; + struct cdb_iter_args iter_args; + char errmsg[BUFSIZ] = {0}; + int ret; + + candidate = nb_config_dup(running_config); + + /* Iterate over all configuration changes. */ + iter_args.candidate = candidate; + iter_args.error = false; + for (int i = 0; i < reslen; i++) { + if (cdb_diff_iterate(fd, subp[i], frr_confd_cdb_diff_iter, + ITER_WANT_PREV, &iter_args) + != CONFD_OK) { + flog_err_confd("cdb_diff_iterate"); + } + } + free(subp); + + if (iter_args.error) { + nb_config_free(candidate); + + if (cdb_sub_abort_trans( + cdb_sub_sock, CONFD_ERRCODE_APPLICATION_INTERNAL, 0, + 0, "Couldn't apply configuration changes") + != CONFD_OK) { + flog_err_confd("cdb_sub_abort_trans"); + return; + } + return; + } + + /* + * Validate the configuration changes and allocate all resources + * required to apply them. + */ + transaction = NULL; + context.client = NB_CLIENT_CONFD; + ret = nb_candidate_commit_prepare(context, candidate, NULL, + &transaction, false, false, errmsg, + sizeof(errmsg)); + if (ret != NB_OK && ret != NB_ERR_NO_CHANGES) { + enum confd_errcode errcode; + + switch (ret) { + case NB_ERR_LOCKED: + errcode = CONFD_ERRCODE_IN_USE; + break; + case NB_ERR_RESOURCE: + errcode = CONFD_ERRCODE_RESOURCE_DENIED; + break; + default: + errcode = CONFD_ERRCODE_APPLICATION; + break; + } + + /* Reject the configuration changes. */ + if (cdb_sub_abort_trans(cdb_sub_sock, errcode, 0, 0, "%s", + errmsg) + != CONFD_OK) { + flog_err_confd("cdb_sub_abort_trans"); + return; + } + } else { + /* Acknowledge the notification. */ + if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) + != CONFD_OK) { + flog_err_confd("cdb_sync_subscription_socket"); + return; + } + + /* No configuration changes. */ + if (!transaction) + nb_config_free(candidate); + } +} + +static void frr_confd_cdb_read_cb_commit(int fd, int *subp, int reslen) +{ + /* + * No need to process the configuration changes again as we're already + * keeping track of them in the "transaction" variable. + */ + free(subp); + + /* Apply the transaction. */ + if (transaction) { + struct nb_config *candidate = transaction->config; + char errmsg[BUFSIZ] = {0}; + + nb_candidate_commit_apply(transaction, true, NULL, errmsg, + sizeof(errmsg)); + nb_config_free(candidate); + } + + /* Acknowledge the notification. */ + if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) { + flog_err_confd("cdb_sync_subscription_socket"); + return; + } +} + +static int frr_confd_cdb_read_cb_abort(int fd, int *subp, int reslen) +{ + /* + * No need to process the configuration changes again as we're already + * keeping track of them in the "transaction" variable. + */ + free(subp); + + /* Abort the transaction. */ + if (transaction) { + struct nb_config *candidate = transaction->config; + char errmsg[BUFSIZ] = {0}; + + nb_candidate_commit_abort(transaction, errmsg, sizeof(errmsg)); + nb_config_free(candidate); + } + + /* Acknowledge the notification. */ + if (cdb_sync_subscription_socket(fd, CDB_DONE_PRIORITY) != CONFD_OK) { + flog_err_confd("cdb_sync_subscription_socket"); + return -1; + } + + return 0; +} + +static void frr_confd_cdb_read_cb(struct event *thread) +{ + int fd = EVENT_FD(thread); + enum cdb_sub_notification cdb_ev; + int flags; + int *subp = NULL; + int reslen = 0; + + event_add_read(master, frr_confd_cdb_read_cb, NULL, fd, &t_cdb_sub); + + if (cdb_read_subscription_socket2(fd, &cdb_ev, &flags, &subp, &reslen) + != CONFD_OK) { + flog_err_confd("cdb_read_subscription_socket2"); + return; + } + + switch (cdb_ev) { + case CDB_SUB_PREPARE: + frr_confd_cdb_read_cb_prepare(fd, subp, reslen); + break; + case CDB_SUB_COMMIT: + frr_confd_cdb_read_cb_commit(fd, subp, reslen); + break; + case CDB_SUB_ABORT: + frr_confd_cdb_read_cb_abort(fd, subp, reslen); + break; + default: + flog_err_confd("unknown CDB event"); + break; + } +} + +/* Trigger CDB subscriptions to read the startup configuration. */ +static void *thread_cdb_trigger_subscriptions(void *data) +{ + int sock; + int *sub_points = NULL, len = 0; + struct listnode *node; + int *spoint; + int i = 0; + + /* Create CDB data socket. */ + sock = socket(PF_INET, SOCK_STREAM, 0); + if (sock < 0) { + flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s", + __func__, safe_strerror(errno)); + return NULL; + } + + if (cdb_connect(sock, CDB_DATA_SOCKET, &confd_addr, + sizeof(struct sockaddr_in)) + != CONFD_OK) { + flog_err_confd("cdb_connect"); + return NULL; + } + + /* + * Fill array containing the subscription point of all loaded YANG + * modules. + */ + len = listcount(confd_spoints); + sub_points = XCALLOC(MTYPE_CONFD, len * sizeof(int)); + for (ALL_LIST_ELEMENTS_RO(confd_spoints, node, spoint)) + sub_points[i++] = *spoint; + + if (cdb_trigger_subscriptions(sock, sub_points, len) != CONFD_OK) { + flog_err_confd("cdb_trigger_subscriptions"); + return NULL; + } + + /* Cleanup and exit thread. */ + XFREE(MTYPE_CONFD, sub_points); + cdb_close(sock); + + return NULL; +} + +static int frr_confd_subscribe(const struct lysc_node *snode, void *arg) +{ + struct yang_module *module = arg; + struct nb_node *nb_node; + int *spoint; + int ret; + + switch (snode->nodetype) { + case LYS_CONTAINER: + case LYS_LEAF: + case LYS_LEAFLIST: + case LYS_LIST: + break; + default: + return YANG_ITER_CONTINUE; + } + + if (CHECK_FLAG(snode->flags, LYS_CONFIG_R)) + return YANG_ITER_CONTINUE; + + nb_node = snode->priv; + if (!nb_node) + return YANG_ITER_CONTINUE; + + DEBUGD(&nb_dbg_client_confd, "%s: subscribing to '%s'", __func__, + nb_node->xpath); + + spoint = XMALLOC(MTYPE_CONFD, sizeof(*spoint)); + ret = cdb_subscribe2(cdb_sub_sock, CDB_SUB_RUNNING_TWOPHASE, + CDB_SUB_WANT_ABORT_ON_ABORT, 3, spoint, + module->confd_hash, nb_node->xpath); + if (ret != CONFD_OK) { + flog_err_confd("cdb_subscribe2"); + XFREE(MTYPE_CONFD, spoint); + return YANG_ITER_CONTINUE; + } + + listnode_add(confd_spoints, spoint); + return YANG_ITER_CONTINUE; +} + +static int frr_confd_init_cdb(void) +{ + struct yang_module *module; + pthread_t cdb_trigger_thread; + + /* Create CDB subscription socket. */ + cdb_sub_sock = socket(PF_INET, SOCK_STREAM, 0); + if (cdb_sub_sock < 0) { + flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s", + __func__, safe_strerror(errno)); + return -1; + } + + if (cdb_connect(cdb_sub_sock, CDB_SUBSCRIPTION_SOCKET, &confd_addr, + sizeof(struct sockaddr_in)) + != CONFD_OK) { + flog_err_confd("cdb_connect"); + goto error; + } + + /* Subscribe to all loaded YANG data modules. */ + confd_spoints = list_new(); + RB_FOREACH (module, yang_modules, &yang_modules) { + module->confd_hash = confd_str2hash(module->info->ns); + if (module->confd_hash == 0) { + flog_err( + EC_LIB_LIBCONFD, + "%s: failed to find hash value for namespace %s", + __func__, module->info->ns); + goto error; + } + + /* + * The CDB API doesn't provide a mechanism to subscribe to an + * entire YANG module. So we have to find the top level + * nodes ourselves and subscribe to their paths. + */ + yang_snodes_iterate(module->info, frr_confd_subscribe, 0, + module); + } + + if (cdb_subscribe_done(cdb_sub_sock) != CONFD_OK) { + flog_err_confd("cdb_subscribe_done"); + goto error; + } + + /* Create short lived pthread to trigger the CDB subscriptions. */ + if (pthread_create(&cdb_trigger_thread, NULL, + thread_cdb_trigger_subscriptions, NULL)) { + flog_err(EC_LIB_SYSTEM_CALL, "%s: error creating pthread: %s", + __func__, safe_strerror(errno)); + goto error; + } + pthread_detach(cdb_trigger_thread); + + event_add_read(master, frr_confd_cdb_read_cb, NULL, cdb_sub_sock, + &t_cdb_sub); + + return 0; + +error: + frr_confd_finish_cdb(); + + return -1; +} + +static void frr_confd_finish_cdb(void) +{ + if (cdb_sub_sock > 0) { + EVENT_OFF(t_cdb_sub); + cdb_close(cdb_sub_sock); + } +} + +/* ------------ DP code ------------ */ + +static int frr_confd_transaction_init(struct confd_trans_ctx *tctx) +{ + confd_trans_set_fd(tctx, dp_worker_sock); + + return CONFD_OK; +} + +#define CONFD_MAX_CHILD_NODES 32 + +static int frr_confd_data_get_elem(struct confd_trans_ctx *tctx, + confd_hkeypath_t *kp) +{ + struct nb_node *nb_node; + char xpath[XPATH_MAXLEN]; + struct yang_data *data; + confd_value_t v; + const void *list_entry = NULL; + + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + confd_data_reply_not_found(tctx); + return CONFD_OK; + } + + if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) { + confd_data_reply_not_found(tctx); + return CONFD_OK; + } + + data = nb_callback_get_elem(nb_node, xpath, list_entry); + if (data) { + if (data->value) { + CONFD_SET_STR(&v, data->value); + confd_data_reply_value(tctx, &v); + } else + confd_data_reply_found(tctx); + yang_data_free(data); + } else + confd_data_reply_not_found(tctx); + + return CONFD_OK; +} + +static int frr_confd_data_get_next(struct confd_trans_ctx *tctx, + confd_hkeypath_t *kp, long next) +{ + struct nb_node *nb_node; + char xpath[XPATH_MAXLEN]; + struct yang_data *data; + const void *parent_list_entry, *nb_next; + confd_value_t v[LIST_MAXKEYS]; + + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + confd_data_reply_next_key(tctx, NULL, -1, -1); + return CONFD_OK; + } + + if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry) + != 0) { + /* List entry doesn't exist anymore. */ + confd_data_reply_next_key(tctx, NULL, -1, -1); + return CONFD_OK; + } + + nb_next = nb_callback_get_next(nb_node, parent_list_entry, + (next == -1) ? NULL : (void *)next); + if (!nb_next) { + /* End of the list or leaf-list. */ + confd_data_reply_next_key(tctx, NULL, -1, -1); + return CONFD_OK; + } + + switch (nb_node->snode->nodetype) { + case LYS_LIST: + if (!CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) { + struct yang_list_keys keys; + + memset(&keys, 0, sizeof(keys)); + if (nb_callback_get_keys(nb_node, nb_next, &keys) + != NB_OK) { + flog_warn(EC_LIB_NB_CB_STATE, + "%s: failed to get list keys", + __func__); + confd_data_reply_next_key(tctx, NULL, -1, -1); + return CONFD_OK; + } + + /* Feed keys to ConfD. */ + for (size_t i = 0; i < keys.num; i++) + CONFD_SET_STR(&v[i], keys.key[i]); + confd_data_reply_next_key(tctx, v, keys.num, + (long)nb_next); + } else { + char pointer_str[32]; + + /* + * ConfD 6.6 user guide, chapter 6.11 (Operational data + * lists without keys): + * "To support this without having completely separate + * APIs, we use a "pseudo" key in the ConfD APIs for + * this type of list. This key is not part of the data + * model, and completely hidden in the northbound agent + * interfaces, but is used with e.g. the get_next() and + * get_elem() callbacks as if it were a normal key. This + * "pseudo" key is always a single signed 64-bit + * integer, i.e. the confd_value_t type is C_INT64. The + * values can be chosen arbitrarily by the application, + * as long as a key value returned by get_next() can be + * used to get the data for the corresponding list entry + * with get_elem() or get_object() as usual. It could + * e.g. be an index into an array that holds the data, + * or even a memory address in integer form". + * + * Since we're using the CONFD_DAEMON_FLAG_STRINGSONLY + * option, we must convert our pseudo-key (a void + * pointer) to a string before sending it to confd. + */ + snprintf(pointer_str, sizeof(pointer_str), "%lu", + (unsigned long)nb_next); + CONFD_SET_STR(&v[0], pointer_str); + confd_data_reply_next_key(tctx, v, 1, (long)nb_next); + } + break; + case LYS_LEAFLIST: + data = nb_callback_get_elem(nb_node, xpath, nb_next); + if (data) { + if (data->value) { + CONFD_SET_STR(&v[0], data->value); + confd_data_reply_next_key(tctx, v, 1, + (long)nb_next); + } + yang_data_free(data); + } else + confd_data_reply_next_key(tctx, NULL, -1, -1); + break; + default: + break; + } + + return CONFD_OK; +} + +/* + * Optional callback - implemented for performance reasons. + */ +static int frr_confd_data_get_object(struct confd_trans_ctx *tctx, + confd_hkeypath_t *kp) +{ + struct nb_node *nb_node; + const struct lysc_node *child; + char xpath[XPATH_MAXLEN]; + char xpath_child[XPATH_MAXLEN * 2]; + struct list *elements; + struct yang_data *data; + const void *list_entry; + confd_value_t values[CONFD_MAX_CHILD_NODES]; + size_t nvalues = 0; + + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + confd_data_reply_not_found(tctx); + return CONFD_ERR; + } + + if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &list_entry) != 0) { + confd_data_reply_not_found(tctx); + return CONFD_OK; + } + + elements = yang_data_list_new(); + + /* Loop through list child nodes. */ + LY_LIST_FOR (lysc_node_child(nb_node->snode), child) { + struct nb_node *nb_node_child = child->priv; + confd_value_t *v; + + if (nvalues > CONFD_MAX_CHILD_NODES) + break; + + v = &values[nvalues++]; + + /* Non-presence containers, lists and leaf-lists. */ + if (!nb_node_child->cbs.get_elem) { + CONFD_SET_NOEXISTS(v); + continue; + } + + snprintf(xpath_child, sizeof(xpath_child), "%s/%s", xpath, + child->name); + data = nb_callback_get_elem(nb_node_child, xpath_child, + list_entry); + if (data) { + if (data->value) + CONFD_SET_STR(v, data->value); + else { + /* Presence containers and empty leafs. */ + CONFD_SET_XMLTAG( + v, nb_node_child->confd_hash, + confd_str2hash(nb_node_child->snode + ->module->ns)); + } + listnode_add(elements, data); + } else + CONFD_SET_NOEXISTS(v); + } + + confd_data_reply_value_array(tctx, values, nvalues); + + /* Release memory. */ + list_delete(&elements); + + return CONFD_OK; +} + +/* + * Optional callback - implemented for performance reasons. + */ +static int frr_confd_data_get_next_object(struct confd_trans_ctx *tctx, + confd_hkeypath_t *kp, long next) +{ + char xpath[XPATH_MAXLEN]; + struct nb_node *nb_node; + struct list *elements; + const void *parent_list_entry; + const void *nb_next; +#define CONFD_OBJECTS_PER_TIME 100 + struct confd_next_object objects[CONFD_OBJECTS_PER_TIME + 1]; + char pseudo_keys[CONFD_OBJECTS_PER_TIME][32]; + int nobjects = 0; + + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + confd_data_reply_next_object_array(tctx, NULL, 0, 0); + return CONFD_OK; + } + + if (frr_confd_hkeypath_get_list_entry(kp, nb_node, &parent_list_entry) + != 0) { + confd_data_reply_next_object_array(tctx, NULL, 0, 0); + return CONFD_OK; + } + + elements = yang_data_list_new(); + nb_next = (next == -1) ? NULL : (void *)next; + + memset(objects, 0, sizeof(objects)); + for (int j = 0; j < CONFD_OBJECTS_PER_TIME; j++) { + struct confd_next_object *object; + const struct lysc_node *child; + struct yang_data *data; + size_t nvalues = 0; + + object = &objects[j]; + + nb_next = nb_callback_get_next(nb_node, parent_list_entry, + nb_next); + if (!nb_next) + /* End of the list. */ + break; + + object->next = (long)nb_next; + + /* Leaf-lists require special handling. */ + if (nb_node->snode->nodetype == LYS_LEAFLIST) { + object->v = XMALLOC(MTYPE_CONFD, sizeof(confd_value_t)); + data = nb_callback_get_elem(nb_node, xpath, nb_next); + assert(data && data->value); + CONFD_SET_STR(object->v, data->value); + nvalues++; + listnode_add(elements, data); + goto next; + } + + object->v = + XMALLOC(MTYPE_CONFD, + CONFD_MAX_CHILD_NODES * sizeof(confd_value_t)); + + /* + * ConfD 6.6 user guide, chapter 6.11 (Operational data lists + * without keys): + * "In the response to the get_next_object() callback, the data + * provider is expected to provide the key values along with the + * other leafs in an array that is populated according to the + * data model. This must be done also for this type of list, + * even though the key isn't actually in the data model. The + * "pseudo" key must always be the first element in the array". + */ + if (CHECK_FLAG(nb_node->flags, F_NB_NODE_KEYLESS_LIST)) { + confd_value_t *v; + + snprintf(pseudo_keys[j], sizeof(pseudo_keys[j]), "%lu", + (unsigned long)nb_next); + + v = &object->v[nvalues++]; + CONFD_SET_STR(v, pseudo_keys[j]); + } + + /* Loop through list child nodes. */ + LY_LIST_FOR (lysc_node_child(nb_node->snode), child) { + struct nb_node *nb_node_child = child->priv; + char xpath_child[XPATH_MAXLEN * 2]; + confd_value_t *v; + + if (nvalues > CONFD_MAX_CHILD_NODES) + break; + + v = &object->v[nvalues++]; + + /* Non-presence containers, lists and leaf-lists. */ + if (!nb_node_child->cbs.get_elem) { + CONFD_SET_NOEXISTS(v); + continue; + } + + snprintf(xpath_child, sizeof(xpath_child), "%s/%s", + xpath, child->name); + data = nb_callback_get_elem(nb_node_child, xpath_child, + nb_next); + if (data) { + if (data->value) + CONFD_SET_STR(v, data->value); + else { + /* + * Presence containers and empty leafs. + */ + CONFD_SET_XMLTAG( + v, nb_node_child->confd_hash, + confd_str2hash( + nb_node_child->snode + ->module->ns)); + } + listnode_add(elements, data); + } else + CONFD_SET_NOEXISTS(v); + } + next: + object->n = nvalues; + nobjects++; + } + + if (nobjects == 0) { + confd_data_reply_next_object_array(tctx, NULL, 0, 0); + list_delete(&elements); + return CONFD_OK; + } + + /* Detect end of the list. */ + if (!nb_next) { + nobjects++; + objects[nobjects].v = NULL; + } + + /* Reply to ConfD. */ + confd_data_reply_next_object_arrays(tctx, objects, nobjects, 0); + if (!nb_next) + nobjects--; + + /* Release memory. */ + list_delete(&elements); + for (int j = 0; j < nobjects; j++) { + struct confd_next_object *object; + + object = &objects[j]; + XFREE(MTYPE_CONFD, object->v); + } + + return CONFD_OK; +} + +static int frr_confd_notification_send(const char *xpath, + struct list *arguments) +{ + struct nb_node *nb_node; + struct yang_module *module; + struct confd_datetime now; + confd_tag_value_t *values; + int nvalues; + int i = 0; + struct yang_data *data; + struct listnode *node; + int ret; + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + return -1; + } + module = yang_module_find(nb_node->snode->module->name); + assert(module); + + nvalues = 2; + if (arguments) + nvalues += listcount(arguments); + + values = XMALLOC(MTYPE_CONFD, nvalues * sizeof(*values)); + + CONFD_SET_TAG_XMLBEGIN(&values[i++], nb_node->confd_hash, + module->confd_hash); + for (ALL_LIST_ELEMENTS_RO(arguments, node, data)) { + struct nb_node *nb_node_arg; + + nb_node_arg = nb_node_find(data->xpath); + if (!nb_node_arg) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, + data->xpath); + XFREE(MTYPE_CONFD, values); + return NB_ERR; + } + + CONFD_SET_TAG_STR(&values[i++], nb_node_arg->confd_hash, + data->value); + } + CONFD_SET_TAG_XMLEND(&values[i++], nb_node->confd_hash, + module->confd_hash); + + getdatetime(&now); + ret = confd_notification_send(live_ctx, &now, values, nvalues); + + /* Release memory. */ + XFREE(MTYPE_CONFD, values); + + /* Map ConfD return code to northbound return code. */ + switch (ret) { + case CONFD_OK: + return NB_OK; + default: + return NB_ERR; + } +} + +static int frr_confd_action_init(struct confd_user_info *uinfo) +{ + confd_action_set_fd(uinfo, dp_worker_sock); + + return CONFD_OK; +} + +static int frr_confd_action_execute(struct confd_user_info *uinfo, + struct xml_tag *name, confd_hkeypath_t *kp, + confd_tag_value_t *params, int nparams) +{ + char xpath[XPATH_MAXLEN]; + struct nb_node *nb_node; + struct list *input; + struct list *output; + struct yang_data *data; + confd_tag_value_t *reply; + int ret = CONFD_OK; + char errmsg[BUFSIZ] = {0}; + + /* Getting the XPath is tricky. */ + if (kp) { + /* This is a YANG RPC. */ + frr_confd_get_xpath(kp, xpath, sizeof(xpath)); + strlcat(xpath, "/", sizeof(xpath)); + strlcat(xpath, confd_hash2str(name->tag), sizeof(xpath)); + } else { + /* This is a YANG action. */ + snprintf(xpath, sizeof(xpath), "/%s:%s", + confd_ns2prefix(name->ns), confd_hash2str(name->tag)); + } + + nb_node = nb_node_find(xpath); + if (!nb_node) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, xpath); + return CONFD_ERR; + } + + input = yang_data_list_new(); + output = yang_data_list_new(); + + /* Process input nodes. */ + for (int i = 0; i < nparams; i++) { + char xpath_input[XPATH_MAXLEN * 2]; + char value_str[YANG_VALUE_MAXLEN]; + + snprintf(xpath_input, sizeof(xpath_input), "%s/%s", xpath, + confd_hash2str(params[i].tag.tag)); + + if (frr_confd_val2str(xpath_input, ¶ms[i].v, value_str, + sizeof(value_str)) + != 0) { + flog_err( + EC_LIB_CONFD_DATA_CONVERT, + "%s: failed to convert ConfD value to a string", + __func__); + ret = CONFD_ERR; + goto exit; + } + + data = yang_data_new(xpath_input, value_str); + listnode_add(input, data); + } + + /* Execute callback registered for this XPath. */ + if (nb_callback_rpc(nb_node, xpath, input, output, errmsg, + sizeof(errmsg)) + != NB_OK) { + flog_warn(EC_LIB_NB_CB_RPC, "%s: rpc callback failed: %s", + __func__, xpath); + ret = CONFD_ERR; + goto exit; + } + + /* Process output nodes. */ + if (listcount(output) > 0) { + struct listnode *node; + int i = 0; + + reply = XMALLOC(MTYPE_CONFD, + listcount(output) * sizeof(*reply)); + + for (ALL_LIST_ELEMENTS_RO(output, node, data)) { + struct nb_node *nb_node_output; + int hash; + + nb_node_output = nb_node_find(data->xpath); + if (!nb_node_output) { + flog_warn(EC_LIB_YANG_UNKNOWN_DATA_PATH, + "%s: unknown data path: %s", __func__, + data->xpath); + goto exit; + } + + hash = confd_str2hash(nb_node_output->snode->name); + CONFD_SET_TAG_STR(&reply[i++], hash, data->value); + } + confd_action_reply_values(uinfo, reply, listcount(output)); + XFREE(MTYPE_CONFD, reply); + } + +exit: + /* Release memory. */ + list_delete(&input); + list_delete(&output); + + return ret; +} + + +static int frr_confd_dp_read(struct confd_daemon_ctx *dctx, int fd) +{ + int ret; + + ret = confd_fd_ready(dctx, fd); + if (ret == CONFD_EOF) { + flog_err_confd("confd_fd_ready"); + frr_confd_finish(); + return -1; + } else if (ret == CONFD_ERR && confd_errno != CONFD_ERR_EXTERNAL) { + flog_err_confd("confd_fd_ready"); + frr_confd_finish(); + return -1; + } + + return 0; +} + +static void frr_confd_dp_ctl_read(struct event *thread) +{ + struct confd_daemon_ctx *dctx = EVENT_ARG(thread); + int fd = EVENT_FD(thread); + + event_add_read(master, frr_confd_dp_ctl_read, dctx, fd, &t_dp_ctl); + + frr_confd_dp_read(dctx, fd); +} + +static void frr_confd_dp_worker_read(struct event *thread) +{ + struct confd_daemon_ctx *dctx = EVENT_ARG(thread); + int fd = EVENT_FD(thread); + + event_add_read(master, frr_confd_dp_worker_read, dctx, fd, + &t_dp_worker); + + frr_confd_dp_read(dctx, fd); +} + +static int frr_confd_subscribe_state(const struct lysc_node *snode, void *arg) +{ + struct nb_node *nb_node = snode->priv; + struct confd_data_cbs *data_cbs = arg; + + if (!nb_node || !CHECK_FLAG(snode->flags, LYS_CONFIG_R)) + return YANG_ITER_CONTINUE; + /* We only need to subscribe to the root of the state subtrees. */ + if (snode->parent && CHECK_FLAG(snode->parent->flags, LYS_CONFIG_R)) + return YANG_ITER_CONTINUE; + + DEBUGD(&nb_dbg_client_confd, + "%s: providing data to '%s' (callpoint %s)", __func__, + nb_node->xpath, snode->name); + + strlcpy(data_cbs->callpoint, snode->name, sizeof(data_cbs->callpoint)); + if (confd_register_data_cb(dctx, data_cbs) != CONFD_OK) + flog_err_confd("confd_register_data_cb"); + + return YANG_ITER_CONTINUE; +} + +static int frr_confd_init_dp(const char *program_name) +{ + struct confd_trans_cbs trans_cbs; + struct confd_data_cbs data_cbs; + struct confd_notification_stream_cbs ncbs; + struct confd_action_cbs acbs; + + /* Initialize daemon context. */ + dctx = confd_init_daemon(program_name); + if (!dctx) { + flog_err_confd("confd_init_daemon"); + goto error; + } + + /* + * Inform we want to receive YANG values as raw strings, and that we + * want to provide only strings in the reply functions, regardless of + * the YANG type. + */ + confd_set_daemon_flags(dctx, CONFD_DAEMON_FLAG_STRINGSONLY); + + /* Establish a control socket. */ + dp_ctl_sock = socket(PF_INET, SOCK_STREAM, 0); + if (dp_ctl_sock < 0) { + flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s", + __func__, safe_strerror(errno)); + goto error; + } + + if (confd_connect(dctx, dp_ctl_sock, CONTROL_SOCKET, &confd_addr, + sizeof(struct sockaddr_in)) + != CONFD_OK) { + flog_err_confd("confd_connect"); + goto error; + } + + /* + * Establish a worker socket (only one since this plugin runs on a + * single thread). + */ + dp_worker_sock = socket(PF_INET, SOCK_STREAM, 0); + if (dp_worker_sock < 0) { + flog_err(EC_LIB_SOCKET, "%s: failed to create socket: %s", + __func__, safe_strerror(errno)); + goto error; + } + if (confd_connect(dctx, dp_worker_sock, WORKER_SOCKET, &confd_addr, + sizeof(struct sockaddr_in)) + != CONFD_OK) { + flog_err_confd("confd_connect"); + goto error; + } + + /* Register transaction callback functions. */ + memset(&trans_cbs, 0, sizeof(trans_cbs)); + trans_cbs.init = frr_confd_transaction_init; + confd_register_trans_cb(dctx, &trans_cbs); + + /* Register our read/write callbacks. */ + memset(&data_cbs, 0, sizeof(data_cbs)); + data_cbs.get_elem = frr_confd_data_get_elem; + data_cbs.exists_optional = frr_confd_data_get_elem; + data_cbs.get_next = frr_confd_data_get_next; + data_cbs.get_object = frr_confd_data_get_object; + data_cbs.get_next_object = frr_confd_data_get_next_object; + + /* + * Iterate over all loaded YANG modules and subscribe to the paths + * referent to state data. + */ + yang_snodes_iterate(NULL, frr_confd_subscribe_state, 0, &data_cbs); + + /* Register notification stream. */ + memset(&ncbs, 0, sizeof(ncbs)); + ncbs.fd = dp_worker_sock; + /* + * RFC 5277 - Section 3.2.3: + * A NETCONF server implementation supporting the notification + * capability MUST support the "NETCONF" notification event + * stream. This stream contains all NETCONF XML event notifications + * supported by the NETCONF server. + */ + strlcpy(ncbs.streamname, "NETCONF", sizeof(ncbs.streamname)); + if (confd_register_notification_stream(dctx, &ncbs, &live_ctx) + != CONFD_OK) { + flog_err_confd("confd_register_notification_stream"); + goto error; + } + + /* Register the action handler callback. */ + memset(&acbs, 0, sizeof(acbs)); + strlcpy(acbs.actionpoint, "actionpoint", sizeof(acbs.actionpoint)); + acbs.init = frr_confd_action_init; + acbs.action = frr_confd_action_execute; + if (confd_register_action_cbs(dctx, &acbs) != CONFD_OK) { + flog_err_confd("confd_register_action_cbs"); + goto error; + } + + /* Notify we registered all callbacks we wanted. */ + if (confd_register_done(dctx) != CONFD_OK) { + flog_err_confd("confd_register_done"); + goto error; + } + + event_add_read(master, frr_confd_dp_ctl_read, dctx, dp_ctl_sock, + &t_dp_ctl); + event_add_read(master, frr_confd_dp_worker_read, dctx, dp_worker_sock, + &t_dp_worker); + + return 0; + +error: + frr_confd_finish_dp(); + + return -1; +} + +static void frr_confd_finish_dp(void) +{ + if (dp_worker_sock > 0) { + EVENT_OFF(t_dp_worker); + close(dp_worker_sock); + } + if (dp_ctl_sock > 0) { + EVENT_OFF(t_dp_ctl); + close(dp_ctl_sock); + } + if (dctx != NULL) + confd_release_daemon(dctx); +} + +/* ------------ CLI ------------ */ + +DEFUN (debug_nb_confd, + debug_nb_confd_cmd, + "[no] debug northbound client confd", + NO_STR + DEBUG_STR + "Northbound debugging\n" + "Client\n" + "ConfD\n") +{ + uint32_t mode = DEBUG_NODE2MODE(vty->node); + bool no = strmatch(argv[0]->text, "no"); + + DEBUG_MODE_SET(&nb_dbg_client_confd, mode, !no); + + return CMD_SUCCESS; +} + +static int frr_confd_debug_config_write(struct vty *vty) +{ + if (DEBUG_MODE_CHECK(&nb_dbg_client_confd, DEBUG_MODE_CONF)) + vty_out(vty, "debug northbound client confd\n"); + + return 0; +} + +static int frr_confd_debug_set_all(uint32_t flags, bool set) +{ + DEBUG_FLAGS_SET(&nb_dbg_client_confd, flags, set); + + /* If all modes have been turned off, don't preserve options. */ + if (!DEBUG_MODE_CHECK(&nb_dbg_client_confd, DEBUG_MODE_ALL)) + DEBUG_CLEAR(&nb_dbg_client_confd); + + return 0; +} + +static void frr_confd_cli_init(void) +{ + hook_register(nb_client_debug_config_write, + frr_confd_debug_config_write); + hook_register(nb_client_debug_set_all, frr_confd_debug_set_all); + + install_element(ENABLE_NODE, &debug_nb_confd_cmd); + install_element(CONFIG_NODE, &debug_nb_confd_cmd); +} + +/* ------------ Main ------------ */ + +static int frr_confd_calculate_snode_hash(const struct lysc_node *snode, + void *arg) +{ + struct nb_node *nb_node = snode->priv; + + if (nb_node) + nb_node->confd_hash = confd_str2hash(snode->name); + + return YANG_ITER_CONTINUE; +} + +static int frr_confd_init(const char *program_name) +{ + struct sockaddr_in *confd_addr4 = (struct sockaddr_in *)&confd_addr; + int debuglevel = CONFD_SILENT; + int ret = -1; + + /* Initialize ConfD library. */ + confd_init(program_name, stderr, debuglevel); + + confd_addr4->sin_family = AF_INET; + confd_addr4->sin_addr.s_addr = inet_addr("127.0.0.1"); + confd_addr4->sin_port = htons(CONFD_PORT); + if (confd_load_schemas(&confd_addr, sizeof(struct sockaddr_in)) + != CONFD_OK) { + flog_err_confd("confd_load_schemas"); + return -1; + } + + ret = frr_confd_init_cdb(); + if (ret != 0) + goto error; + + ret = frr_confd_init_dp(program_name); + if (ret != 0) { + frr_confd_finish_cdb(); + goto error; + } + + yang_snodes_iterate(NULL, frr_confd_calculate_snode_hash, 0, NULL); + + hook_register(nb_notification_send, frr_confd_notification_send); + + confd_connected = true; + return 0; + +error: + confd_free_schemas(); + + return ret; +} + +static int frr_confd_finish(void) +{ + if (!confd_connected) + return 0; + + frr_confd_finish_cdb(); + frr_confd_finish_dp(); + + confd_free_schemas(); + + confd_connected = false; + + return 0; +} + +static int frr_confd_module_late_init(struct event_loop *tm) +{ + master = tm; + + if (frr_confd_init(frr_get_progname()) < 0) { + flog_err(EC_LIB_CONFD_INIT, + "failed to initialize the ConfD module"); + return -1; + } + + hook_register(frr_fini, frr_confd_finish); + frr_confd_cli_init(); + + return 0; +} + +static int frr_confd_module_init(void) +{ + hook_register(frr_late_init, frr_confd_module_late_init); + + return 0; +} + +FRR_MODULE_SETUP(.name = "frr_confd", .version = FRR_VERSION, + .description = "FRR ConfD integration module", + .init = frr_confd_module_init, +); |