diff options
47 files changed, 1293 insertions, 150 deletions
diff --git a/bgpd/bgp_route.c b/bgpd/bgp_route.c index 479a7dfed1..3bbbdee161 100644 --- a/bgpd/bgp_route.c +++ b/bgpd/bgp_route.c @@ -3211,21 +3211,6 @@ void bgp_best_selection(struct bgp *bgp, struct bgp_dest *dest, bgp_path_info_unset_flag(dest, look_thru, BGP_PATH_DMED_CHECK); - if (CHECK_FLAG(bgp->flags, BGP_FLAG_DETERMINISTIC_MED) && - (!CHECK_FLAG(look_thru->flags, - BGP_PATH_DMED_SELECTED))) { - bgp_path_info_unset_flag(dest, look_thru, - BGP_PATH_DMED_CHECK); - if (debug) - zlog_debug("%s: %pBD(%s) pi %s dmed", - __func__, dest, - bgp->name_pretty, - look_thru->peer->host); - - worse = look_thru; - continue; - } - reason = dest->reason; any_comparisons = true; if (bgp_path_info_cmp(bgp, first, look_thru, &paths_eq, diff --git a/bgpd/bgpd.c b/bgpd/bgpd.c index eab059abea..cf1ea78acf 100644 --- a/bgpd/bgpd.c +++ b/bgpd/bgpd.c @@ -3412,12 +3412,28 @@ static struct bgp *bgp_create(as_t *as, const char *name, afi_t afi; safi_t safi; - if (hidden) { + if (hidden) bgp = bgp_old; - goto peer_init; - } + else + bgp = XCALLOC(MTYPE_BGP, sizeof(struct bgp)); + + bgp->as = *as; + + if (bgp->as_pretty) + XFREE(MTYPE_BGP_NAME, bgp->as_pretty); + if (as_pretty) + bgp->as_pretty = XSTRDUP(MTYPE_BGP_NAME, as_pretty); + else + bgp->as_pretty = XSTRDUP(MTYPE_BGP_NAME, asn_asn2asplain(*as)); - bgp = XCALLOC(MTYPE_BGP, sizeof(struct bgp)); + if (asnotation != ASNOTATION_UNDEFINED) { + bgp->asnotation = asnotation; + SET_FLAG(bgp->config, BGP_CONFIG_ASNOTATION); + } else + asn_str2asn_notation(bgp->as_pretty, NULL, &bgp->asnotation); + + if (hidden) + goto peer_init; if (BGP_DEBUG(zebra, ZEBRA)) { if (inst_type == BGP_INSTANCE_TYPE_DEFAULT) @@ -3461,18 +3477,6 @@ static struct bgp *bgp_create(as_t *as, const char *name, bgp->peer = list_new(); peer_init: - bgp->as = *as; - if (as_pretty) - bgp->as_pretty = XSTRDUP(MTYPE_BGP_NAME, as_pretty); - else - bgp->as_pretty = XSTRDUP(MTYPE_BGP_NAME, asn_asn2asplain(*as)); - - if (asnotation != ASNOTATION_UNDEFINED) { - bgp->asnotation = asnotation; - SET_FLAG(bgp->config, BGP_CONFIG_ASNOTATION); - } else - asn_str2asn_notation(bgp->as_pretty, NULL, &bgp->asnotation); - bgp->peer->cmp = (int (*)(void *, void *))peer_cmp; bgp->peerhash = hash_create(peer_hash_key_make, peer_hash_same, "BGP Peer Hash"); @@ -3569,7 +3573,7 @@ peer_init: /* printable name we can use in debug messages */ if (inst_type == BGP_INSTANCE_TYPE_DEFAULT && !hidden) { bgp->name_pretty = XSTRDUP(MTYPE_BGP_NAME, "VRF default"); - } else { + } else if (!hidden) { const char *n; int len; @@ -3772,7 +3776,7 @@ int bgp_lookup_by_as_name_type(struct bgp **bgp_val, as_t *as, const char *as_pr /* Handle AS number change */ if (bgp->as != *as) { if (hidden || CHECK_FLAG(bgp->vrf_flags, BGP_VRF_AUTO)) { - if (force_config == false && hidden) { + if (hidden) { bgp_create(as, name, inst_type, as_pretty, asnotation, bgp, hidden); @@ -4265,12 +4269,11 @@ int bgp_delete(struct bgp *bgp) bgp_set_evpn(bgp_get_default()); } - if (bgp->process_queue) - work_queue_free_and_null(&bgp->process_queue); - - if (!IS_BGP_INSTANCE_HIDDEN(bgp)) + if (!IS_BGP_INSTANCE_HIDDEN(bgp)) { + if (bgp->process_queue) + work_queue_free_and_null(&bgp->process_queue); bgp_unlock(bgp); /* initial reference */ - else { + } else { for (afi = AFI_IP; afi < AFI_MAX; afi++) { enum vpn_policy_direction dir; diff --git a/doc/developer/mgmtd-dev.rst b/doc/developer/mgmtd-dev.rst index 6cbd617f8c..f113d1b521 100644 --- a/doc/developer/mgmtd-dev.rst +++ b/doc/developer/mgmtd-dev.rst @@ -39,17 +39,18 @@ Conversion Status Fully Converted To MGMTD """""""""""""""""""""""" +- lib/affinitymap - lib/distribute - lib/filter +- lib/if - lib/if_rmap +- lib/keychain - lib/routemap -- lib/affinitymap -- lib/if - lib/vrf - ripd - ripngd - staticd -- zebra (* - partial) +- zebra Converted To Northbound """"""""""""""""""""""" @@ -69,7 +70,6 @@ Unconverted - bgpd - ldpd - lib/event -- lib/keychain - lib/log_vty - lib/nexthop_group - lib/zlog_5424_cli diff --git a/lib/northbound_oper.c b/lib/northbound_oper.c index 6336db502a..b7815b001a 100644 --- a/lib/northbound_oper.c +++ b/lib/northbound_oper.c @@ -140,6 +140,11 @@ nb_op_create_yield_state(const char *xpath, struct yang_translator *translator, ys = XCALLOC(MTYPE_NB_YIELD_STATE, sizeof(*ys)); ys->xpath = darr_strdup_cap(xpath, (size_t)XPATH_MAXLEN); + /* remove trailing '/'s */ + while (darr_len(ys->xpath) > 1 && ys->xpath[darr_len(ys->xpath) - 2] == '/') { + darr_setlen(ys->xpath, darr_len(ys->xpath) - 1); + *darr_last(ys->xpath) = 0; + } ys->xpath_orig = darr_strdup(xpath); ys->translator = translator; ys->flags = flags; @@ -417,8 +422,7 @@ static enum nb_error nb_op_ys_finalize_node_info(struct nb_op_yield_state *ys, /* Assert that we are walking the rightmost branch */ assert(!inner->parent || inner == inner->parent->child->prev); - if (CHECK_FLAG(inner->schema->nodetype, - LYS_CASE | LYS_CHOICE | LYS_CONTAINER)) { + if (CHECK_FLAG(inner->schema->nodetype, LYS_CONTAINER)) { /* containers have only zero or one child on a branch of a tree */ inner = ((struct lyd_node_inner *)inner)->child; assert(!inner || inner->prev == inner); @@ -568,7 +572,8 @@ static enum nb_error nb_op_ys_init_node_infos(struct nb_op_yield_state *ys) inner = node; prevlen = 0; xplen = strlen(xpath); - darr_free(xpath); + darr_free(ys->xpath); + ys->xpath = xpath; for (i = len; i > 0; i--, inner = &inner->parent->node) { ni = &ys->node_infos[i - 1]; ni->inner = inner; @@ -897,8 +902,7 @@ static const struct lysc_node *nb_op_sib_first(struct nb_op_yield_state *ys, * base of the user query, return the next schema node from the query * string (schema_path). */ - if (last != NULL && - !CHECK_FLAG(last->schema->nodetype, LYS_CASE | LYS_CHOICE)) + if (last != NULL) assert(last->schema == parent); if (darr_lasti(ys->node_infos) < ys->query_base_level) return ys->schema_path[darr_lasti(ys->node_infos) + 1]; @@ -975,7 +979,8 @@ static enum nb_error __walk(struct nb_op_yield_state *ys, bool is_resume) if (!walk_stem_tip) return NB_ERR_NOT_FOUND; - if (ys->schema_path[0]->nodetype == LYS_CHOICE) { + if (ys->schema_path[0]->parent && + CHECK_FLAG(ys->schema_path[0]->parent->nodetype, LYS_CHOICE|LYS_CASE)) { flog_err(EC_LIB_NB_OPERATIONAL_DATA, "%s: unable to walk root level choice node from module: %s", __func__, ys->schema_path[0]->module->name); @@ -1082,8 +1087,12 @@ static enum nb_error __walk(struct nb_op_yield_state *ys, bool is_resume) LYS_LEAF | LYS_LEAFLIST | LYS_CONTAINER)) xpath_child = nb_op_get_child_path(ys->xpath, sib, xpath_child); - else if (CHECK_FLAG(sib->nodetype, LYS_CASE | LYS_CHOICE)) + else if (CHECK_FLAG(sib->nodetype, LYS_CASE | LYS_CHOICE)) { darr_in_strdup(xpath_child, ys->xpath); + len = darr_last(ys->node_infos)->xpath_len; + darr_setlen(xpath_child, len + 1); + xpath_child[len] = 0; + } nn = sib->priv; @@ -1703,22 +1712,22 @@ static enum nb_error nb_op_ys_init_schema_path(struct nb_op_yield_state *ys, * NOTE: appears to be a bug in nb_node linkage where parent can be NULL, * or I'm misunderstanding the code, in any case we use the libyang * linkage to walk which works fine. - * - * XXX: we don't actually support choice/case yet, they are container - * types in the libyang schema, but won't be in data so our length - * checking gets messed up. */ - for (sn = nblast->snode, count = 0; sn; count++, sn = sn->parent) + for (sn = nblast->snode, count = 0; sn; sn = sn->parent) { if (sn != nblast->snode) assert(CHECK_FLAG(sn->nodetype, - LYS_CONTAINER | LYS_LIST | - LYS_CHOICE | LYS_CASE)); + LYS_CONTAINER | LYS_LIST | LYS_CHOICE | LYS_CASE)); + if (!CHECK_FLAG(sn->nodetype, LYS_CHOICE | LYS_CASE)) + count++; + } /* create our arrays */ darr_append_n(ys->schema_path, count); darr_append_n(ys->query_tokens, count); darr_append_nz(ys->non_specific_predicate, count); - for (sn = nblast->snode; sn; sn = sn->parent) - ys->schema_path[--count] = sn; + for (sn = nblast->snode; sn; sn = sn->parent) { + if (!CHECK_FLAG(sn->nodetype, LYS_CHOICE | LYS_CASE)) + ys->schema_path[--count] = sn; + } /* * Now tokenize the query string and get pointers to each token @@ -1737,50 +1746,42 @@ static enum nb_error nb_op_ys_init_schema_path(struct nb_op_yield_state *ys, int nlen = strlen(name); int mnlen = 0; - /* - * Technically the query_token for choice/case should probably be pointing at - * the child (leaf) rather than the parent (container), however, - * we only use these for processing list nodes so KISS. - */ - if (CHECK_FLAG(ys->schema_path[i]->nodetype, - LYS_CASE | LYS_CHOICE)) { - ys->query_tokens[i] = ys->query_tokens[i - 1]; - continue; - } - + s2 = s; while (true) { - s2 = strstr(s, name); + /* skip past any module name prefix */ + s2 = strstr(s2, name); if (!s2) goto error; - if (s2[-1] == ':') { + if (s2 > s && s2[-1] == ':') { mnlen = strlen(modname) + 1; - if (ys->query_tokstr > s2 - mnlen || - strncmp(s2 - mnlen, modname, mnlen - 1)) - goto error; + if (s2 - s < mnlen || strncmp(s2 - mnlen, modname, mnlen - 1)) { + /* No match of module prefix, advance and try again */ + s2 += strlen(name); + continue; + } s2 -= mnlen; nlen += mnlen; } - s = s2; - if ((i == 0 || s[-1] == '/') && - (s[nlen] == 0 || s[nlen] == '[' || s[nlen] == '/')) + if ((i == 0 || s2[-1] == '/') && + (s2[nlen] == 0 || s2[nlen] == '[' || s2[nlen] == '/')) { + s = s2; break; - /* - * Advance past the incorrect match, must have been - * part of previous predicate. - */ - s += nlen; + } + /* No exact match at end, advance and try again */ + s2 += strlen(name); } /* NUL terminate previous token and save this one */ - if (i > 0) + if (i > 0) { + assert(s[-1] == '/'); s[-1] = 0; + } ys->query_tokens[i] = s; s += nlen; } - /* NOTE: need to subtract choice/case nodes when these are supported */ ys->query_base_level = darr_lasti(ys->schema_path); return NB_OK; diff --git a/ospfd/ospf_lsa.c b/ospfd/ospf_lsa.c index 1d5c0be5c4..15068ec820 100644 --- a/ospfd/ospf_lsa.c +++ b/ospfd/ospf_lsa.c @@ -87,16 +87,6 @@ bool ospf_check_dna_lsa(const struct ospf_lsa *lsa) : false); } -struct timeval int2tv(int a) -{ - struct timeval ret; - - ret.tv_sec = a; - ret.tv_usec = 0; - - return ret; -} - struct timeval msec2tv(int a) { struct timeval ret; diff --git a/ospfd/ospf_lsa.h b/ospfd/ospf_lsa.h index f58bbde07a..309d506ea6 100644 --- a/ospfd/ospf_lsa.h +++ b/ospfd/ospf_lsa.h @@ -224,7 +224,6 @@ enum lsid_status { LSID_AVAILABLE = 0, LSID_CHANGE, LSID_NOT_AVAILABLE }; /* Prototypes. */ /* XXX: Eek, time functions, similar are in lib/thread.c */ -extern struct timeval int2tv(int); extern struct timeval msec2tv(int a); extern int tv2msec(struct timeval tv); diff --git a/pimd/pim_autorp.c b/pimd/pim_autorp.c index dc077dbbd6..d3f3517efd 100644 --- a/pimd/pim_autorp.c +++ b/pimd/pim_autorp.c @@ -967,6 +967,7 @@ err: static bool pim_autorp_socket_enable(struct pim_autorp *autorp) { int fd; + struct interface *ifp; /* Return early if socket is already enabled */ if (autorp->sock != -1) @@ -980,6 +981,13 @@ static bool pim_autorp_socket_enable(struct pim_autorp *autorp) return false; } + if (vrf_bind(autorp->pim->vrf->vrf_id, fd, NULL)) { + zlog_warn("Could not bind autorp socket to vrf fd=%d: vrf_id=%d: errno=%d: %s", + fd, autorp->pim->vrf->vrf_id, errno, safe_strerror(errno)); + close(fd); + return false; + } + if (!pim_autorp_setup(fd)) { zlog_warn("Could not setup autorp socket fd=%d: errno=%d: %s", fd, errno, safe_strerror(errno)); @@ -990,6 +998,11 @@ static bool pim_autorp_socket_enable(struct pim_autorp *autorp) autorp->sock = fd; + /* Join autorp groups on all pim enabled interfaces in the VRF */ + FOR_ALL_INTERFACES (autorp->pim->vrf, ifp) { + pim_autorp_add_ifp(ifp); + } + if (PIM_DEBUG_AUTORP) zlog_debug("%s: AutoRP socket enabled (fd=%u)", __func__, fd); @@ -1002,6 +1015,7 @@ static bool pim_autorp_socket_disable(struct pim_autorp *autorp) if (autorp->sock == -1) return true; + /* No need to leave the autorp groups explicitly, they are left when the socket is closed */ if (close(autorp->sock)) { zlog_warn("Failure closing autorp socket: fd=%d errno=%d: %s", autorp->sock, errno, safe_strerror(errno)); @@ -1428,10 +1442,10 @@ void pim_autorp_add_ifp(struct interface *ifp) { /* Add a new interface for autorp * When autorp is enabled, we must join the autorp groups on all - * pim/multicast interfaces. When autorp first starts, if finds all - * current multicast interfaces and joins on them. If a new interface - * comes up or is configured for multicast after autorp is running, then - * this method will add it for autorp-> + * pim/multicast interfaces. When autorp becomes enabled, it finds all + * current pim enabled interfaces and joins the autorp groups on them. + * Any new interfaces added after autorp is enabled will use this function + * to join the autorp groups * This is called even when adding a new pim interface that is not yet * active, so make sure the check, it'll call in again once the interface is up. */ @@ -1441,7 +1455,8 @@ void pim_autorp_add_ifp(struct interface *ifp) pim_ifp = ifp->info; if (CHECK_FLAG(ifp->status, ZEBRA_INTERFACE_ACTIVE) && pim_ifp && pim_ifp->pim_enable) { pim = pim_ifp->pim; - if (pim && pim->autorp && pim->autorp->do_discovery) { + if (pim && pim->autorp && + (pim->autorp->do_discovery || pim->autorp->send_rp_discovery)) { if (PIM_DEBUG_AUTORP) zlog_debug("%s: Adding interface %s to AutoRP, joining AutoRP groups", __func__, ifp->name); @@ -1477,44 +1492,37 @@ void pim_autorp_rm_ifp(struct interface *ifp) void pim_autorp_start_discovery(struct pim_instance *pim) { - struct interface *ifp; struct pim_autorp *autorp = pim->autorp; + if (autorp->do_discovery) + return; + + autorp->do_discovery = true; + /* Make sure the socket is open and ready */ if (!pim_autorp_socket_enable(autorp)) { zlog_err("%s: AutoRP failed to open socket", __func__); return; } - if (!autorp->do_discovery) { - autorp->do_discovery = true; - autorp_read_on(autorp); - - FOR_ALL_INTERFACES (autorp->pim->vrf, ifp) { - pim_autorp_add_ifp(ifp); - } + autorp_read_on(autorp); - if (PIM_DEBUG_AUTORP) - zlog_debug("%s: AutoRP Discovery started", __func__); - } + if (PIM_DEBUG_AUTORP) + zlog_debug("%s: AutoRP Discovery started", __func__); } void pim_autorp_stop_discovery(struct pim_instance *pim) { - struct interface *ifp; struct pim_autorp *autorp = pim->autorp; - if (autorp->do_discovery) { - FOR_ALL_INTERFACES (autorp->pim->vrf, ifp) { - pim_autorp_rm_ifp(ifp); - } + if (!autorp->do_discovery) + return; - autorp->do_discovery = false; - autorp_read_off(autorp); + autorp->do_discovery = false; + autorp_read_off(autorp); - if (PIM_DEBUG_AUTORP) - zlog_debug("%s: AutoRP Discovery stopped", __func__); - } + if (PIM_DEBUG_AUTORP) + zlog_debug("%s: AutoRP Discovery stopped", __func__); /* Close the socket if we need to */ if (pim_autorp_should_close(autorp) && !pim_autorp_socket_disable(autorp)) @@ -1549,7 +1557,10 @@ void pim_autorp_init(struct pim_instance *pim) if (PIM_DEBUG_AUTORP) zlog_debug("%s: AutoRP Initialized", __func__); +} +void pim_autorp_enable(struct pim_instance *pim) +{ /* Start AutoRP discovery by default on startup */ pim_autorp_start_discovery(pim); } diff --git a/pimd/pim_autorp.h b/pimd/pim_autorp.h index e4c6530109..88aebe5b7d 100644 --- a/pimd/pim_autorp.h +++ b/pimd/pim_autorp.h @@ -173,6 +173,7 @@ void pim_autorp_rm_ifp(struct interface *ifp); void pim_autorp_start_discovery(struct pim_instance *pim); void pim_autorp_stop_discovery(struct pim_instance *pim); void pim_autorp_init(struct pim_instance *pim); +void pim_autorp_enable(struct pim_instance *pim); void pim_autorp_finish(struct pim_instance *pim); int pim_autorp_config_write(struct pim_instance *pim, struct vty *vty); void pim_autorp_show_autorp(struct vty *vty, struct pim_instance *pim, const char *component, diff --git a/pimd/pim_iface.c b/pimd/pim_iface.c index 3408574cae..e0b157b8f6 100644 --- a/pimd/pim_iface.c +++ b/pimd/pim_iface.c @@ -1901,9 +1901,7 @@ static int pim_ifp_up(struct interface *ifp) } #if PIM_IPV == 4 - if (pim->autorp && pim->autorp->do_discovery && pim_ifp && - pim_ifp->pim_enable) - pim_autorp_add_ifp(ifp); + pim_autorp_add_ifp(ifp); #endif pim_cand_addrs_changed(); @@ -2020,8 +2018,7 @@ void pim_pim_interface_delete(struct interface *ifp) return; #if PIM_IPV == 4 - if (pim_ifp->pim_enable) - pim_autorp_rm_ifp(ifp); + pim_autorp_rm_ifp(ifp); #endif pim_ifp->pim_enable = false; diff --git a/pimd/pim_instance.c b/pimd/pim_instance.c index 3945c5923d..f64b02e44d 100644 --- a/pimd/pim_instance.c +++ b/pimd/pim_instance.c @@ -181,8 +181,15 @@ static int pim_vrf_enable(struct vrf *vrf) zlog_debug("%s: for %s %u", __func__, vrf->name, vrf->vrf_id); + if (vrf_bind(vrf->vrf_id, pim->reg_sock, NULL) < 0) + zlog_warn("Failed to bind register socket to VRF %s", vrf->name); + pim_mroute_socket_enable(pim); +#if PIM_IPV == 4 + pim_autorp_enable(pim); +#endif + FOR_ALL_INTERFACES (vrf, ifp) { if (!ifp->info) continue; diff --git a/pimd/pim_mroute.c b/pimd/pim_mroute.c index 6c13e1324f..30daa3a929 100644 --- a/pimd/pim_mroute.c +++ b/pimd/pim_mroute.c @@ -876,17 +876,11 @@ int pim_mroute_socket_enable(struct pim_instance *pim) pim->vrf->name); #endif -#ifdef SO_BINDTODEVICE - if (pim->vrf->vrf_id != VRF_DEFAULT - && setsockopt(fd, SOL_SOCKET, SO_BINDTODEVICE, - pim->vrf->name, strlen(pim->vrf->name))) { - zlog_warn("Could not setsockopt SO_BINDTODEVICE: %s", - safe_strerror(errno)); + if (vrf_bind(pim->vrf->vrf_id, fd, NULL)) { + zlog_warn("Could not bind to vrf: %s", safe_strerror(errno)); close(fd); return -3; } -#endif - } pim->mroute_socket = fd; diff --git a/ripd/rip_cli.c b/ripd/rip_cli.c index 5712a0b825..0bafc6f342 100644 --- a/ripd/rip_cli.c +++ b/ripd/rip_cli.c @@ -681,9 +681,9 @@ DEFPY_YANG (ip_rip_split_horizon, { const char *value; - if (no) + if (no && poisoned_reverse == NULL) value = "disabled"; - else if (poisoned_reverse) + else if (poisoned_reverse && no == NULL) value = "poison-reverse"; else value = "simple"; diff --git a/tests/lib/northbound/test_oper_data.in b/tests/lib/northbound/test_oper_data.in index 0053148953..94fcdc1e1c 100644 --- a/tests/lib/northbound/test_oper_data.in +++ b/tests/lib/northbound/test_oper_data.in @@ -2,4 +2,8 @@ show yang operational-data /frr-test-module:frr-test-module show yang operational-data /frr-test-module:frr-test-module/vrfs/vrf[name='vrf0']/routes/route[2] show yang operational-data /frr-test-module:frr-test-module/vrfs/vrf[name='vrf0']/routes/route[3]/interface show yang operational-data /frr-test-module:frr-test-module/vrfs/vrf[name='vrf0']/routes/route[10] +show yang operational-data /frr-test-module:frr-test-module/c1value +show yang operational-data /frr-test-module:frr-test-module/c2cont +show yang operational-data /frr-test-module:frr-test-module/c2cont/ +show yang operational-data /frr-test-module:frr-test-module/c2cont/c2value test rpc diff --git a/tests/lib/northbound/test_oper_data.refout b/tests/lib/northbound/test_oper_data.refout index 2536e0306b..57061d0371 100644 --- a/tests/lib/northbound/test_oper_data.refout +++ b/tests/lib/northbound/test_oper_data.refout @@ -174,6 +174,36 @@ test# show yang operational-data /frr-test-module:frr-test-module/vrfs/vrf[name= }
test# show yang operational-data /frr-test-module:frr-test-module/vrfs/vrf[name='vrf0']/routes/route[10] {}
+test# show yang operational-data /frr-test-module:frr-test-module/c1value +{ + "frr-test-module:frr-test-module": { + "c1value": 21 + } +} +test# show yang operational-data /frr-test-module:frr-test-module/c2cont +{ + "frr-test-module:frr-test-module": { + "c2cont": { + "c2value": 2868969987 + } + } +} +test# show yang operational-data /frr-test-module:frr-test-module/c2cont/ +{ + "frr-test-module:frr-test-module": { + "c2cont": { + "c2value": 2868969987 + } + } +} +test# show yang operational-data /frr-test-module:frr-test-module/c2cont/c2value +{ + "frr-test-module:frr-test-module": { + "c2cont": { + "c2value": 2868969987 + } + } +} test# test rpc vrf testname data testdata test# diff --git a/tests/topotests/bgp_l3vpn_hidden/__init__.py b/tests/topotests/bgp_l3vpn_hidden/__init__.py new file mode 100644 index 0000000000..e69de29bb2 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/__init__.py diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/frr.conf b/tests/topotests/bgp_l3vpn_hidden/ce1/frr.conf new file mode 100644 index 0000000000..95d84d5e6b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/frr.conf @@ -0,0 +1,52 @@ +debug bgp neighbor-events +! +ip prefix-list PLIST-LAN seq 10 permit 172.20.0.0/16 le 24 +! +ipv6 prefix-list PLIST-LAN6 seq 10 permit 2001:db8::/32 le 64 +! +route-map RMAP-LAN permit 10 + match ip address prefix-list PLIST-LAN +exit +! +route-map RMAP-LAN6 permit 10 + match ipv6 address prefix-list PLIST-LAN6 +exit +! +interface eth-lan + ip address 172.20.1.1/24 + ipv6 address 2001:db8:1::ff/64 +exit +! +interface eth-pe1 + ip address 172.16.1.254/24 + ipv6 address 3fff:1::ff/64 +exit +! +router bgp 65501 + bgp router-id 172.16.1.254 + no bgp ebgp-requires-policy + bgp bestpath compare-routerid + neighbor 172.16.1.1 remote-as external + neighbor 172.16.1.1 bfd profile BGP + neighbor 3fff:1::1 remote-as external + neighbor 3fff:1::1 bfd profile BGP + ! + address-family ipv4 unicast + redistribute connected route-map RMAP-LAN + neighbor 172.16.1.1 next-hop-self + no neighbor 3fff:1::1 activate + exit-address-family +! + address-family ipv6 unicast + redistribute connected route-map RMAP-LAN6 + neighbor 3fff:1::1 activate + neighbor 3fff:1::1 next-hop-self + exit-address-family +exit +bfd + profile BGP + transmit-interval 2000 + receive-interval 2000 + exit + ! +exit diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast.json new file mode 100644 index 0000000000..e9741a2fbd --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast.json @@ -0,0 +1,24 @@ +{ + "vrfName": "default", + "routerId": "172.16.1.254", + "localAS": 65501, + "routes": { + "172.20.1.0/24": [ + { + "valid": true, + "bestpath": true, + "peerId": "(unspec)", + "path": "", + "nexthops": [ + { + "ip": "0.0.0.0", + "hostname": "ce1", + "used": true + } + ] + } + ] + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast_step1.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast_step1.json new file mode 120000 index 0000000000..0d02eacc65 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_unicast_step1.json @@ -0,0 +1 @@ +show_bgp_ipv4_unicast.json
\ No newline at end of file diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_vpn_step1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv4_vpn_step1.json @@ -0,0 +1 @@ +{} diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast.json new file mode 100644 index 0000000000..1120d30edd --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast.json @@ -0,0 +1,25 @@ +{ + "vrfName": "default", + "routerId": "172.16.1.254", + "localAS": 65501, + "routes": { + "2001:db8:1::/64": [ + { + "valid": true, + "bestpath": true, + "peerId": "(unspec)", + "path": "", + "nexthops": [ + { + "ip": "::", + "hostname": "ce1", + "scope": "global", + "used": true + } + ] + } + ] + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast_step1.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast_step1.json new file mode 120000 index 0000000000..94f8f5bba9 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_unicast_step1.json @@ -0,0 +1 @@ +show_bgp_ipv6_unicast.json
\ No newline at end of file diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_vpn_step1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_ipv6_vpn_step1.json @@ -0,0 +1 @@ +{} diff --git a/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_summary.json b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_summary.json new file mode 100644 index 0000000000..5a0699ed9b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/ce1/show_bgp_summary.json @@ -0,0 +1,24 @@ +{ + "default": { + "ipv4Unicast": { + "routerId": "172.16.1.254", + "peers": { + "172.16.1.1": { + "hostname": "pe1", + "state": "Established" + } + }, + "totalPeers": 1 + }, + "ipv6Unicast": { + "routerId": "172.16.1.254", + "peers": { + "3fff:1::1": { + "hostname": "pe1", + "state": "Established" + } + }, + "totalPeers": 1 + } + } +} diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/frr.conf b/tests/topotests/bgp_l3vpn_hidden/pe1/frr.conf new file mode 100644 index 0000000000..95a7262b32 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/frr.conf @@ -0,0 +1,107 @@ +mpls label dynamic-block 1000 1048575 +! +interface lo + ip address 192.168.0.1/32 + ipv6 address 3fff::192:168:0:1/128 +! +interface eth-rr1 + ip address 10.0.1.1/24 +! +interface eth-ce1 + ip address 172.16.1.1/24 + ipv6 address 3fff:1::1/64 +! +router bgp 65000 + bgp router-id 192.168.0.1 + no bgp ebgp-requires-policy + no bgp default ipv4-unicast + neighbor 192.168.0.101 remote-as 65000 + neighbor 192.168.0.101 bfd profile BGP + neighbor 192.168.0.101 update-source 192.168.0.1 + neighbor 3fff::192:168:0:101 remote-as 65000 + neighbor 3fff::192:168:0:101 bfd profile BGP + neighbor 3fff::192:168:0:101 update-source 3fff::192:168:0:1 + +! + address-family ipv4 unicast + no neighbor 192.168.0.101 activate + exit-address-family +! + address-family ipv4 vpn + neighbor 192.168.0.101 activate + neighbor 192.168.0.101 soft-reconfiguration inbound + exit-address-family +! + address-family ipv6 vpn + neighbor 3fff::192:168:0:101 activate + neighbor 3fff::192:168:0:101 soft-reconfiguration inbound + exit-address-family +! +router bgp 65000 vrf RED + bgp router-id 192.168.0.1 + no bgp ebgp-requires-policy + bgp bestpath compare-routerid + neighbor 172.16.1.254 remote-as external + neighbor 172.16.1.254 bfd profile BGP + neighbor 3fff:1::ff remote-as external + neighbor 3fff:1::ff bfd profile BGP + ! + address-family ipv4 unicast + label vpn export 100 + rd vpn export 65000:100 + rt vpn both 65000:100 + export vpn + import vpn + neighbor 172.16.1.254 next-hop-self + no neighbor 3fff:1::ff activate + exit-address-family +! + address-family ipv6 unicast + label vpn export 200 + rd vpn export 65000:100 + rt vpn both 65000:100 + export vpn + import vpn + neighbor 3fff:1::ff activate + neighbor 3fff:1::ff next-hop-self + exit-address-family +exit +! +interface lo + ip router isis 1 + isis hello-interval 2 + ipv6 router isis 1 +! +interface eth-rr1 + ip router isis 1 + isis hello-interval 2 + ipv6 router isis 1 +! +router isis 1 + lsp-gen-interval 2 + net 10.0000.0000.0000.0000.0000.0000.0000.0000.0001.00 + metric-style wide + exit +! +mpls ldp + router-id 192.168.0.1 + ! + address-family ipv4 + discovery transport-address 192.168.0.1 + ! + interface eth-rr1 + ! + address-family ipv6 + discovery transport-address 3fff::192:168:0:1 + ! + interface eth-rr1 + ! + ! +! +bfd + profile BGP + transmit-interval 2000 + receive-interval 2000 + exit + ! +exit diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn.json b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn.json new file mode 100644 index 0000000000..d21dd89291 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn.json @@ -0,0 +1,29 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.1", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": { + "172.20.1.0/24": [ + { + "valid": true, + "bestpath": true, + "peerId": "(unspec)", + "path": "65501", + "nhVrfName": "RED", + "nexthops": [ + { + "ip": "172.16.1.254", + "hostname": "pe1", + "used": true + } + ] + } + ] + } + } + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn_step1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv4_vpn_step1.json @@ -0,0 +1 @@ +{} diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn.json b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn.json new file mode 100644 index 0000000000..b42656f44f --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn.json @@ -0,0 +1,29 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.1", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": { + "2001:db8:1::/64": [ + { + "valid": true, + "bestpath": true, + "peerId": "(unspec)", + "path": "65501", + "nhVrfName": "RED", + "nexthops": [ + { + "ip": "3fff:1::ff", + "hostname": "pe1", + "used": true + } + ] + } + ] + } + } + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn_step1.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_ipv6_vpn_step1.json @@ -0,0 +1 @@ +{} diff --git a/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_summary.json b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_summary.json new file mode 100644 index 0000000000..b414d2e4ae --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/pe1/show_bgp_summary.json @@ -0,0 +1,46 @@ +{ + "default": { + "ipv4Vpn": { + "routerId": "192.168.0.1", + "peers": { + "192.168.0.101": { + "hostname": "rr1", + "state": "Established" + } + }, + "totalPeers": 1 + }, + "ipv6Vpn": { + "routerId": "192.168.0.1", + "peers": { + "3fff::192:168:0:101": { + "hostname": "rr1", + "state": "Established" + } + }, + "totalPeers": 1 + } + }, + "RED": { + "ipv4Unicast": { + "routerId": "192.168.0.1", + "peers": { + "172.16.1.254": { + "hostname": "ce1", + "state": "Established" + } + }, + "totalPeers": 1 + }, + "ipv6Unicast": { + "routerId": "192.168.0.1", + "peers": { + "3fff:1::ff": { + "hostname": "ce1", + "state": "Established" + } + }, + "totalPeers": 1 + } + } +} diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/frr.conf b/tests/topotests/bgp_l3vpn_hidden/rr1/frr.conf new file mode 100644 index 0000000000..b25a7a336b --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/frr.conf @@ -0,0 +1,79 @@ +mpls label dynamic-block 1000 1048575 +! +interface lo + ip address 192.168.0.101/32 + ipv6 address 3fff::192:168:0:101/128 +! +interface eth-pe1 + ip address 10.0.1.101/24 +! +router bgp 65000 + bgp router-id 192.168.0.101 + bgp cluster-id 192.168.0.101 + bgp log-neighbor-changes + no bgp default ipv4-unicast + neighbor PE peer-group + neighbor PE remote-as 65000 + neighbor PE bfd profile BGP + neighbor PE update-source 192.168.0.101 + neighbor PE6 peer-group + neighbor PE6 remote-as 65000 + neighbor PE6 bfd profile BGP + neighbor PE6 update-source 3fff::192:168:0:101 + neighbor 192.168.0.1 peer-group PE + neighbor 3fff::192:168:0:1 peer-group PE6 +! + address-family ipv4 unicast + no neighbor PE activate + exit-address-family +! + address-family ipv4 vpn + neighbor PE activate + neighbor PE route-reflector-client + neighbor PE soft-reconfiguration inbound + exit-address-family +! + address-family ipv6 vpn + neighbor PE6 activate + neighbor PE6 route-reflector-client + neighbor PE6 soft-reconfiguration inbound + exit-address-family +! +! +interface lo + ip router isis 1 + isis hello-interval 2 + ipv6 router isis 1 +! +interface eth-pe1 + ip router isis 1 + isis hello-interval 2 + ipv6 router isis 1 +! +! +router isis 1 + lsp-gen-interval 2 + net 10.0000.0000.0000.0000.0000.0000.0000.0000.0100.00 + metric-style wide + exit +! +mpls ldp + router-id 192.168.0.101 + ! + address-family ipv4 + discovery transport-address 192.168.0.101 + ! + interface eth-pe1 + ! + address-family ipv6 + discovery transport-address 3fff::192:168:0:101 + ! + interface eth-pe1 +! +bfd + profile BGP + transmit-interval 2000 + receive-interval 2000 + exit + ! +exit diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn.json b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn.json new file mode 100644 index 0000000000..3fd451640c --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn.json @@ -0,0 +1,28 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.101", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": { + "172.20.1.0/24": [ + { + "valid": true, + "bestpath": true, + "peerId": "192.168.0.1", + "path": "65501", + "nexthops": [ + { + "ip": "192.168.0.1", + "hostname": "pe1", + "used": true + } + ] + } + ] + } + } + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn_step1.json new file mode 100644 index 0000000000..88d1ac5e6e --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv4_vpn_step1.json @@ -0,0 +1,12 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.101", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": {} + } + }, + "totalRoutes": 0, + "totalPaths": 0 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn.json b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn.json new file mode 100644 index 0000000000..c3ecd71c46 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn.json @@ -0,0 +1,28 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.101", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": { + "2001:db8:1::/64": [ + { + "valid": true, + "bestpath": true, + "peerId": "3fff::192:168:0:1", + "path": "65501", + "nexthops": [ + { + "ip": "3fff::192:168:0:1", + "hostname": "pe1", + "used": true + } + ] + } + ] + } + } + }, + "totalRoutes": 1, + "totalPaths": 1 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn_step1.json b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn_step1.json new file mode 100644 index 0000000000..88d1ac5e6e --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_ipv6_vpn_step1.json @@ -0,0 +1,12 @@ +{ + "vrfName": "default", + "routerId": "192.168.0.101", + "localAS": 65000, + "routes": { + "routeDistinguishers": { + "65000:100": {} + } + }, + "totalRoutes": 0, + "totalPaths": 0 +} diff --git a/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_summary.json b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_summary.json new file mode 100644 index 0000000000..8d6c15526e --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/rr1/show_bgp_summary.json @@ -0,0 +1,24 @@ +{ + "default": { + "ipv4Vpn": { + "routerId": "192.168.0.101", + "peers": { + "192.168.0.1": { + "hostname": "pe1", + "state": "Established" + } + }, + "totalPeers": 1 + }, + "ipv6Vpn": { + "routerId": "192.168.0.101", + "peers": { + "3fff::192:168:0:1": { + "hostname": "pe1", + "state": "Established" + } + }, + "totalPeers": 1 + } + } +} diff --git a/tests/topotests/bgp_l3vpn_hidden/test_bgp_l3vpn_hidden.py b/tests/topotests/bgp_l3vpn_hidden/test_bgp_l3vpn_hidden.py new file mode 100644 index 0000000000..04a8482518 --- /dev/null +++ b/tests/topotests/bgp_l3vpn_hidden/test_bgp_l3vpn_hidden.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python +# SPDX-License-Identifier: ISC + + +""" +Test BGP hidden +See https://github.com/FRRouting/frr/commit/4d0e7a49cf8d4311a485281fa50bbff6ee8ca6cc +""" + +import os +import sys +import re +import json +import pytest +import functools + +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) + +# pylint: disable=C0413 +from lib import topotest +from lib.topolog import logger +from lib.topogen import Topogen, TopoRouter, get_topogen +from lib.common_config import step + + +pytestmark = [pytest.mark.bgpd, pytest.mark.bfdd, pytest.mark.isisd, pytest.mark.ldpd] + + +def build_topo(tgen): + """ + +---+ +---+ +---+ + |ce1|---|pe1|---|rr1| + +---+ +---+ +---+""" + + def connect_routers(tgen, left, right): + for rname in [left, right]: + if rname not in tgen.routers().keys(): + tgen.add_router(rname) + + switch = tgen.add_switch("s-{}-{}".format(left, right)) + switch.add_link(tgen.gears[left], nodeif="eth-{}".format(right)) + switch.add_link(tgen.gears[right], nodeif="eth-{}".format(left)) + if "ce" not in right and "ce" not in left: + tgen.gears[left].cmd(f"sysctl net.mpls.conf.eth-{right}.input=1") + tgen.gears[right].cmd(f"sysctl net.mpls.conf.eth-{left}.input=1") + + def connect_switchs(tgen, rname, switch): + if rname not in tgen.routers().keys(): + tgen.add_router(rname) + + switch.add_link(tgen.gears[rname], nodeif="eth-{}".format(switch.name)) + + def connect_lan(tgen, rname): + if rname not in tgen.routers().keys(): + tgen.add_router(rname) + + # Extra LAN interfaces. Not used for communication with hosts, just to + # hold an address we use to inject routes + switch = tgen.add_switch("s-{}".format(rname)) + switch.add_link(tgen.gears[rname], nodeif="eth-lan") + + # directly connected without switch routers + connect_routers(tgen, "rr1", "pe1") + connect_routers(tgen, "pe1", "ce1") + connect_lan(tgen, "ce1") + + +def setup_module(mod): + tgen = Topogen(build_topo, mod.__name__) + tgen.start_topology() + + pe1 = tgen.gears["pe1"] + pe1.cmd( + f""" +ip link add RED type vrf table 100 +ip link set RED up +ip link set eth-ce1 master RED +""" + ) + + router_list = tgen.routers() + + for _, (rname, router) in enumerate(router_list.items(), 1): + router.load_frr_config( + os.path.join(CWD, "{}/frr.conf".format(rname)), + [ + (TopoRouter.RD_ZEBRA, None), + (TopoRouter.RD_MGMTD, None), + (TopoRouter.RD_BFD, None), + (TopoRouter.RD_LDP, None), + (TopoRouter.RD_ISIS, None), + (TopoRouter.RD_BGP, None), + ], + ) + + tgen.start_router() + + +def teardown_module(mod): + tgen = get_topogen() + tgen.stop_topology() + + +def check_bgp_convergence(step=None): + """ + out was generated using + + FRRGIT=/path/git/frr + + vtysh -c 'show bgp vrf all summary json' | jq . | egrep -v 'ersion|idType|connections|peerState|pfx|outq|inq|msg|As|rib|Count|Memory|Uptime|vrf|\"as|failedPeers|displayedPeers|dynamicPeers' | awk '/ "bestPath": {/ {c=3; next} c-- > 0 {next} 1' | sed -E 's|"totalPeers": (.+),|"totalPeers": \1|g;s|"Established",|"Established"|g' | jq . >$FRRGIT/tests/topotests/bgp_l3vpn_hidden/$HOSTNAME/show_bgp_summary.json + + vtysh -c 'show bgp ipv4 vpn json' | jq . | egrep -v 'selectionReason|pathFrom|prefix|locPrf|ersion|weight|origin|vrfId|afi|defaultLocPrf|network|nhVrfId|announceNexthopSelf|metric|multipath|linkLocalOnly|length' | jq . >$FRRGIT/tests/topotests/bgp_l3vpn_hidden/$HOSTNAME/show_bgp_ipv4_vpn_step1.json + vtysh -c 'show bgp ipv6 vpn json' | jq . | egrep -v 'selectionReason|pathFrom|prefix|locPrf|ersion|weight|origin|vrfId|afi|defaultLocPrf|network|fe80|nhVrfId|announceNexthopSelf|metric|multipath|linkLocalOnly|length' | jq . >$FRRGIT/tests/topotests/bgp_l3vpn_hidden/$HOSTNAME/show_bgp_ipv6_vpn_step1.json + + vtysh -c 'show bgp ipv4 unicast json' | jq . | egrep -v 'selectionReason|pathFrom|prefix|locPrf|ersion|weight|origin|vrfId|afi|defaultLocPrf|network|nhVrfId|announceNexthopSelf|metric|multipath|linkLocalOnly|length' | jq . >$FRRGIT/tests/topotests/bgp_l3vpn_hidden/$HOSTNAME/show_bgp_ipv4_unicast.json + vtysh -c 'show bgp ipv6 unicast json' | jq . | egrep -v 'selectionReason|pathFrom|prefix|locPrf|ersion|weight|origin|vrfId|afi|defaultLocPrf|network|fe80|nhVrfId|announceNexthopSelf|metric|multipath|linkLocalOnly|length' | jq . >$FRRGIT/tests/topotests/bgp_l3vpn_hidden/$HOSTNAME/show_bgp_ipv6_unicast.json + """ + tgen = get_topogen() + + logger.info("waiting for bgp convergence") + + step_suffix = f"_step{step}" if step else "" + + if not step: + logger.info("Check BGP summary") + for rname, router in tgen.routers().items(): + reffile = os.path.join(CWD, f"{rname}/show_bgp_summary.json") + expected = json.loads(open(reffile).read()) + cmd = "show bgp vrf all summary json" + test_func = functools.partial( + topotest.router_json_cmp, router, cmd, expected + ) + _, res = topotest.run_and_expect(test_func, None, count=60, wait=1) + assertmsg = f"BGP did not converge. Error on {rname} {cmd}" + assert res is None, assertmsg + + logger.info("Check BGP IPv4/6 unicast/VPN table") + for rname, router in tgen.routers().items(): + for ipv in [4, 6]: + logger.info(f"Check BGP IPv4/6 unicast/VPN table: {rname} IPv{ipv}") + safi = "unicast" if "ce" in rname else "vpn" + reffile = os.path.join( + CWD, f"{rname}/show_bgp_ipv{ipv}_{safi}{step_suffix}.json" + ) + expected = json.loads(open(reffile).read()) + exact = not expected # exact match if json is void (ie. {}) + cmd = f"show bgp ipv{ipv} {safi} json" + test_func = functools.partial( + topotest.router_json_cmp, + router, + cmd, + expected, + exact=exact, + ) + _, res = topotest.run_and_expect(test_func, None, count=120, wait=1) + assertmsg = f"BGP did not converge. Error on {rname} {cmd}" + assert res is None, assertmsg + + +def configure_bgp(vrf=None, router_name="all", activate=False): + tgen = get_topogen() + + vrf_suffix = f" vrf {vrf}" if vrf else "" + as_pattern = re.compile(rf"^router bgp (\d+){vrf_suffix}$") + + for rname, router in tgen.routers().items(): + if router_name != "all" and router_name != rname: + continue + + if "ce" in rname: + continue + + as_number = "" + cmds = [] + router_bgp = False + with open(os.path.join(CWD, f"{rname}/frr.conf"), "r") as f: + for line in f: + line = line.strip() + if "router bgp" in line: + match = as_pattern.match(line) + if match: + as_number = match.group(1) + router_bgp = True + continue + if router_bgp: + # If we already hit "router bgp <as_number>" once, + # and see another "router bgp" line, break. + break + if not router_bgp: + # Only capture lines after we've matched "router bgp" + continue + cmds.append(line) + + cfg = "configure terminal\n" + if activate: + cfg += f"router bgp {as_number}{vrf_suffix}\n" + for cmd in cmds: + cfg += f"{cmd}\n" + else: + cfg += f"no router bgp {as_number}{vrf_suffix}\n" + + router.vtysh_cmd(cfg) + + +def test_bgp_convergence(): + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + check_bgp_convergence() + + +def test_bgp_l3vpn_hidden_step1(): + """ + Remove pe1 router bgp blocks + The Default BGP instance becomes hidden + """ + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + for vrf in ["RED", None]: + configure_bgp(router_name="pe1", vrf=vrf, activate=False) + + check_bgp_convergence(step=1) + + +def test_bgp_l3vpn_hidden_step2(): + """ + Restore pe1 router bgp blocks + """ + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + for vrf in [None, "RED"]: + configure_bgp(router_name="pe1", vrf=vrf, activate=True) + + # identical to the intitial step + check_bgp_convergence(step=None) + + +def test_bgp_l3vpn_hidden_step3(): + """ + Remove pe1 router bgp blocks + The Default BGP instance becomes hidden + """ + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + for vrf in ["RED", None]: + configure_bgp(router_name="pe1", vrf=vrf, activate=False) + + # identical to the intitial step 1 + check_bgp_convergence(step=1) + + +def test_bgp_l3vpn_hidden_step4(): + """ + Restore pe1 router bgp blocks + Reconfigure VRF block first + """ + + tgen = get_topogen() + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + for vrf in [None, "RED"]: + configure_bgp(router_name="pe1", vrf=vrf, activate=True) + + # identical to the intitial step + check_bgp_convergence(step=None) + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/bgp_vrf_route_leak_basic/r1/bgpd.conf b/tests/topotests/bgp_vrf_route_leak_basic/r1/bgpd.conf index 397f7938d2..8363e2bc99 100644 --- a/tests/topotests/bgp_vrf_route_leak_basic/r1/bgpd.conf +++ b/tests/topotests/bgp_vrf_route_leak_basic/r1/bgpd.conf @@ -1,12 +1,5 @@ hostname r1 -router bgp 99 - no bgp ebgp-requires-policy - address-family ipv4 unicast - redistribute connected - import vrf DONNA - ! -! router bgp 99 vrf DONNA no bgp ebgp-requires-policy address-family ipv4 unicast @@ -31,3 +24,10 @@ router bgp 99 vrf ZITA network 172.16.101.0/24 ! ! +router bgp 99 + no bgp ebgp-requires-policy + address-family ipv4 unicast + redistribute connected + import vrf DONNA + ! +! diff --git a/tests/topotests/bgp_vrf_route_leak_basic/test_bgp-vrf-route-leak-basic.py b/tests/topotests/bgp_vrf_route_leak_basic/test_bgp-vrf-route-leak-basic.py index 3bc36862bf..7a407761d3 100644 --- a/tests/topotests/bgp_vrf_route_leak_basic/test_bgp-vrf-route-leak-basic.py +++ b/tests/topotests/bgp_vrf_route_leak_basic/test_bgp-vrf-route-leak-basic.py @@ -64,6 +64,16 @@ def teardown_module(mod): tgen.stop_topology() +def test_router_bgp_as_pretty(): + tgen = get_topogen() + # Don't run this test if we have any failure. + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + output = tgen.gears["r1"].vtysh_cmd("show run") + assert "router bgp 99\n" in output, "router bgp 99 not found in show run" + + def test_vrf_route_leak_donna(): logger.info("Ensure that routes are leaked back and forth") tgen = get_topogen() diff --git a/tests/topotests/lib/fe_client.py b/tests/topotests/lib/fe_client.py index 078df8cb33..4868c1018e 100755 --- a/tests/topotests/lib/fe_client.py +++ b/tests/topotests/lib/fe_client.py @@ -8,6 +8,7 @@ # # noqa: E501 # +"""A MGMTD front-end client.""" import argparse import logging import os @@ -34,7 +35,7 @@ except Exception as error: try: sys.path[0:0] = "." - import mgmt_pb2 + import mgmt_pb2 # pylint: disable=E0401 except Exception as error: logging.error("can't import proto definition modules %s", error) raise @@ -124,16 +125,22 @@ MSG_FORMAT_LYB = 3 def cstr(mdata): + """Convert a null-term byte array into a string, excluding the null terminator.""" assert mdata[-1] == 0 return mdata[:-1] class FEClientError(Exception): + """Base class for frontend client errors.""" + pass class PBMessageError(FEClientError): + """Exception for errors related to protobuf messages.""" + def __init__(self, msg, errstr): + """Initialize PBMessageError with message and error string.""" self.msg = msg # self.sess_id = mhdr[HDR_FIELD_SESS_ID] # self.req_id = mhdr[HDR_FIELD_REQ_ID] @@ -143,7 +150,10 @@ class PBMessageError(FEClientError): class NativeMessageError(FEClientError): + """Exception for errors related to native messages.""" + def __init__(self, mhdr, mfixed, mdata): + """Initialize NativeMessageError with message header, fixed fields, and data.""" self.mhdr = mhdr self.sess_id = mhdr[HDR_FIELD_SESS_ID] self.req_id = mhdr[HDR_FIELD_REQ_ID] @@ -173,6 +183,7 @@ def recv_wait(sock, size): def recv_msg(sock): + """Receive a message from the socket, ensuring it has a valid marker.""" marker = recv_wait(sock, 4) assert marker in (MGMT_MSG_MARKER_PROTOBUF, MGMT_MSG_MARKER_NATIVE) @@ -197,15 +208,18 @@ class Session: client_id = 1 def __init__(self, sock, use_protobuf): + """Initialize a session with the mgmtd server.""" self.sock = sock self.next_req_id = 1 if use_protobuf: + # Register the client req = mgmt_pb2.FeMessage() req.register_req.client_name = "test-client" self.send_pb_msg(req) logging.debug("Sent FeRegisterReq: %s", req) + # Create a session req = mgmt_pb2.FeMessage() req.session_req.create = 1 req.session_req.client_conn_id = Session.client_id @@ -219,11 +233,11 @@ class Session: assert reply.session_reply.success self.sess_id = reply.session_reply.session_id else: + # Establish a native session self.sess_id = 0 mdata, _ = self.get_native_msg_header(MSG_CODE_SESSION_REQ) mdata += struct.pack(MSG_SESSION_REQ_FMT) mdata += "test-client".encode("utf-8") + b"\x00" - self.send_native_msg(mdata) logging.debug("Sent native SESSION-REQ") @@ -236,6 +250,7 @@ class Session: self.sess_id = mhdr[HDR_FIELD_SESS_ID] def close(self, clean=True): + """Close the session.""" if clean: req = mgmt_pb2.FeMessage() req.session_req.create = 0 @@ -245,6 +260,7 @@ class Session: self.sock = None def get_next_req_id(self): + """Generate the next request ID for a new session.""" req_id = self.next_req_id self.next_req_id += 1 return req_id @@ -301,10 +317,11 @@ class Session: return mhdr, mfixed, mdata def send_native_msg(self, mdata): - """Send a native message.""" + """Send a native message to the mgmtd server.""" return send_msg(self.sock, MGMT_MSG_MARKER_NATIVE, mdata) def get_native_msg_header(self, msg_code): + """Generate a native message header for a given message code.""" req_id = self.get_next_req_id() hdata = struct.pack(MSG_HDR_FMT, msg_code, 0, self.sess_id, req_id) return hdata, req_id @@ -314,6 +331,19 @@ class Session: # ----------------------- def lock(self, lock=True, ds_id=mgmt_pb2.CANDIDATE_DS): + """Lock or unlock a datastore. + + Args: + lock (bool, optional): Whether to lock (True) or unlock (False) the + datastore. Defaults to True. + ds_id (int, optional): The datastore ID. Defaults to mgmt_pb2.CANDIDATE_DS. + + Returns: + None + + Raises: + AssertionError: If the lock request fails. + """ req = mgmt_pb2.FeMessage() req.lockds_req.session_id = self.sess_id req.lockds_req.req_id = self.get_next_req_id() @@ -327,7 +357,20 @@ class Session: assert reply.lockds_reply.success def get_data(self, query, data=True, config=False): - # Create the message + """Retrieve data from the mgmtd server based on an XPath query. + + Args: + query (str): The XPath query string. + data (bool, optional): Whether to retrieve state data. Defaults to True. + config (bool, optional): Whether to retrieve configuration data. + Defaults to False. + + Returns: + str: The retrieved data in JSON format. + + Raises: + AssertionError: If the response data is not properly formatted. + """ mdata, _ = self.get_native_msg_header(MSG_CODE_GET_DATA) flags = GET_DATA_FLAG_STATE if data else 0 flags |= GET_DATA_FLAG_CONFIG if config else 0 @@ -345,7 +388,12 @@ class Session: return result def add_notify_select(self, replace, notif_xpaths): - # Create the message + """Send a request to add notification subscriptions to the given XPaths. + + Args: + replace (bool): Whether to replace existing notification subscriptions. + notif_xpaths (list of str): List of XPaths to subscribe to notifications on. + """ mdata, _ = self.get_native_msg_header(MSG_CODE_NOTIFY_SELECT) mdata += struct.pack(MSG_NOTIFY_SELECT_FMT, replace) @@ -356,6 +404,18 @@ class Session: logging.debug("Sent NOTIFY_SELECT") def recv_notify(self, xpaths=None): + """Receive a notification message, optionally setting up XPath filters first. + + Args: + xpaths (list of str, optional): List of XPaths to filter notifications. + + Returns: + tuple: (result_type, operation, xpath, message data) + + Raises: + TimeoutError: If no notification is received within the timeout period. + Exception: If a non-notification message is received. + """ if xpaths: self.add_notify_select(True, xpaths) @@ -379,6 +439,7 @@ class Session: def __parse_args(): + """Parse command-line arguments for the mgmtd client.""" MPATH = "/var/run/frr/mgmtd_fe.sock" parser = argparse.ArgumentParser() parser.add_argument( @@ -416,6 +477,14 @@ def __parse_args(): def __server_connect(spath): + """Establish a connection to the mgmtd server over a Unix socket. + + Args: + spath (str): Path to the Unix domain socket. + + Returns: + socket: A connected Unix socket. + """ sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) logging.debug("Connecting to server on %s", spath) while ec := sock.connect_ex(str(spath)): @@ -428,6 +497,7 @@ def __server_connect(spath): def __main(): + """Process client commands and handle queries or notifications.""" args = __parse_args() sock = __server_connect(Path(args.server)) sess = Session(sock, use_protobuf=args.use_protobuf) @@ -473,6 +543,7 @@ def __main(): def main(): + """Entry point for the mgmtd client application.""" try: __main() except KeyboardInterrupt: diff --git a/tests/topotests/pim_autorp/test_pim_autorp.py b/tests/topotests/pim_autorp/test_pim_autorp.py index 61cf8ebbc5..5edf1db6af 100644 --- a/tests/topotests/pim_autorp/test_pim_autorp.py +++ b/tests/topotests/pim_autorp/test_pim_autorp.py @@ -158,6 +158,103 @@ def test_pim_autorp_init(request): ) +def test_pim_autorp_disable_enable(request): + "Test PIM AutoRP disable and re-enable works properly" + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + step("Ensure AutoRP groups are joined on all routers") + for rtr in ["r1", "r2", "r3", "r4"]: + expected = { + f"{rtr}-eth0": { + "name": f"{rtr}-eth0", + "224.0.1.39": "*", + "224.0.1.40": "*", + }, + f"{rtr}-eth1": { + "name": f"{rtr}-eth1", + "224.0.1.39": "*", + "224.0.1.40": "*", + }, + } + + test_func = partial( + topotest.router_json_cmp, + tgen.gears[rtr], + "show ip igmp sources json", + expected, + ) + _, result = topotest.run_and_expect(test_func, None) + assert result is None, "{} does not have correct autorp groups joined".format( + rtr + ) + + step("Disable AutoRP on all routers") + for rtr in ["r1", "r2", "r3", "r4"]: + tgen.routers()[rtr].vtysh_cmd( + """ + conf + router pim + no autorp discovery + """ + ) + + step("Ensure AutoRP groups are no longer joined on all routers") + for rtr in ["r1", "r2", "r3", "r4"]: + expected = {f"{rtr}-eth0": None, f"{rtr}-eth1": None} + + test_func = partial( + topotest.router_json_cmp, + tgen.gears[rtr], + "show ip igmp sources json", + expected, + ) + _, result = topotest.run_and_expect(test_func, None) + assert result is None, "{} does not have correct autorp groups joined".format( + rtr + ) + + step("Re-enable AutoRP on all routers") + for rtr in ["r1", "r2", "r3", "r4"]: + tgen.routers()[rtr].vtysh_cmd( + """ + conf + router pim + autorp discovery + """ + ) + + step("Ensure AutoRP groups are re-joined on all routers") + for rtr in ["r1", "r2", "r3", "r4"]: + expected = { + f"{rtr}-eth0": { + "name": f"{rtr}-eth0", + "224.0.1.39": "*", + "224.0.1.40": "*", + }, + f"{rtr}-eth1": { + "name": f"{rtr}-eth1", + "224.0.1.39": "*", + "224.0.1.40": "*", + }, + } + + test_func = partial( + topotest.router_json_cmp, + tgen.gears[rtr], + "show ip igmp sources json", + expected, + ) + _, result = topotest.run_and_expect(test_func, None) + assert result is None, "{} does not have correct autorp groups joined".format( + rtr + ) + + def test_pim_autorp_no_mapping_agent_rp(request): "Test PIM AutoRP candidate with no mapping agent" tgen = get_topogen() diff --git a/tests/topotests/zebra_operational_data/r1/frr.conf b/tests/topotests/zebra_operational_data/r1/frr.conf new file mode 100644 index 0000000000..72a67bf020 --- /dev/null +++ b/tests/topotests/zebra_operational_data/r1/frr.conf @@ -0,0 +1,41 @@ +log timestamp precision 6 +log file frr.log + +no debug memstats-at-exit + +debug northbound notifications +debug northbound libyang +debug northbound events +debug northbound callbacks + +debug mgmt backend datastore frontend transaction +debug mgmt client frontend +debug mgmt client backend + +interface r1-eth0 + ip address 1.1.1.1/24 + ipv6 address 2001:1111::1/64 +exit + +interface r1-eth1 + ip address 2.2.2.1/24 + ipv6 address 2002:2222::1/64 +exit + +interface r1-eth2 vrf red + ip address 3.3.3.1/24 + ipv6 address 2003:333::1/64 +exit + +interface r1-eth3 vrf red + ip address 4.4.4.1/24 + ipv6 address 2004:4444::1/64 +exit + +ip route 11.0.0.0/8 Null0 +ip route 11.11.11.11/32 1.1.1.2 +ip route 12.12.12.12/32 2.2.2.2 + +ip route 13.0.0.0/8 Null0 vrf red +ip route 13.13.13.13/32 3.3.3.2 vrf red +ip route 14.14.14.14/32 4.4.4.2 vrf red
\ No newline at end of file diff --git a/tests/topotests/zebra_operational_data/test_zebra_operational.py b/tests/topotests/zebra_operational_data/test_zebra_operational.py new file mode 100644 index 0000000000..c4ab5be973 --- /dev/null +++ b/tests/topotests/zebra_operational_data/test_zebra_operational.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# SPDX-License-Identifier: ISC +# +# Copyright (c) 2025 Nvidia Inc. +# Donald Sharp +# +""" +Test zebra operational values +""" + +import pytest +import json +from lib.topogen import Topogen +from lib.topolog import logger + + +pytestmark = [pytest.mark.mgmtd] + + +@pytest.fixture(scope="module") +def tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + + topodef = {"s1": ("r1",), "s2": ("r1",), "s3": ("r1",), "s4": ("r1",)} + + tgen = Topogen(topodef, request.module.__name__) + tgen.start_topology() + + router_list = tgen.routers() + for rname, router in router_list.items(): + # Setup VRF red + router.net.add_l3vrf("red", 10) + router.net.add_loop("lo-red") + router.net.attach_iface_to_l3vrf("lo-red", "red") + router.net.attach_iface_to_l3vrf(rname + "-eth2", "red") + router.net.attach_iface_to_l3vrf(rname + "-eth3", "red") + router.load_frr_config("frr.conf") + + tgen.start_router() + yield tgen + tgen.stop_topology() + + +def test_zebra_operationalr(tgen): + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + r1 = tgen.gears["r1"] + + output = json.loads(r1.vtysh_cmd("show mgmt get-data /frr-zebra:zebra")) + + logger.info("Output") + logger.info(output) + + logger.info("Ensuring that the max-multipath value is returned") + assert "max-multipath" in output["frr-zebra:zebra"].keys() + + +if __name__ == "__main__": + # To suppress tracebacks, either use the following pytest call or add "--tb=no" to cli + # retval = pytest.main(["-s", "--tb=no"]) + retval = pytest.main(["-s"]) + sys.exit(retval) diff --git a/yang/frr-zebra.yang b/yang/frr-zebra.yang index a3c066c56c..5f1b711b97 100644 --- a/yang/frr-zebra.yang +++ b/yang/frr-zebra.yang @@ -2842,6 +2842,17 @@ module frr-zebra { container zebra { description "Data model for the Zebra daemon."; + leaf max-multipath { + config false; + type uint16 { + range "1..65535"; + } + description + "The maximum number of nexthops for a route. At this point it + is unlikely that a multipath number will ever get larger then + 1024 but to allow for future expansions, the yang returns a + 16 bit number"; + } leaf ip-forwarding { type boolean; description diff --git a/zebra/zebra_dplane.c b/zebra/zebra_dplane.c index 9c252cc63c..4344a8d79a 100644 --- a/zebra/zebra_dplane.c +++ b/zebra/zebra_dplane.c @@ -6210,8 +6210,7 @@ int dplane_show_provs_helper(struct vty *vty, bool detailed) in_max = atomic_load_explicit(&prov->dp_in_max, memory_order_relaxed); - out = atomic_load_explicit(&prov->dp_out_counter, - memory_order_relaxed); + out = dplane_provider_out_ctx_queue_len(prov); out_max = atomic_load_explicit(&prov->dp_out_max, memory_order_relaxed); diff --git a/zebra/zebra_nb.c b/zebra/zebra_nb.c index 6b41993a95..81fe2ab6a0 100644 --- a/zebra/zebra_nb.c +++ b/zebra/zebra_nb.c @@ -26,6 +26,12 @@ const struct frr_yang_module_info frr_zebra_info = { .features = features, .nodes = { { + .xpath = "/frr-zebra:zebra/max-multipath", + .cbs = { + .get_elem = zebra_max_multipath_get_elem, + } + }, + { .xpath = "/frr-zebra:zebra/ip-forwarding", .cbs = { .modify = zebra_ip_forwarding_modify, diff --git a/zebra/zebra_nb.h b/zebra/zebra_nb.h index 785291bc68..426ad4b7de 100644 --- a/zebra/zebra_nb.h +++ b/zebra/zebra_nb.h @@ -14,6 +14,7 @@ extern "C" { extern const struct frr_yang_module_info frr_zebra_info; /* prototypes */ +struct yang_data *zebra_max_multipath_get_elem(struct nb_cb_get_elem_args *args); int get_route_information_rpc(struct nb_cb_rpc_args *args); int get_v6_mroute_info_rpc(struct nb_cb_rpc_args *args); int get_vrf_info_rpc(struct nb_cb_rpc_args *args); diff --git a/zebra/zebra_nb_state.c b/zebra/zebra_nb_state.c index 6ed11f75f1..a7091821b5 100644 --- a/zebra/zebra_nb_state.c +++ b/zebra/zebra_nb_state.c @@ -1157,3 +1157,12 @@ lib_vrf_zebra_ribs_rib_route_route_entry_nexthop_group_nexthop_weight_get_elem( return NULL; } + +/* + * XPath: + * /frr-zebra:zebra/max-multipath + */ +struct yang_data *zebra_max_multipath_get_elem(struct nb_cb_get_elem_args *args) +{ + return yang_data_new_uint16(args->xpath, zrouter.multipath_num); +} |
