From 97385dddf1e955a428dc3396fb535c592b1f8c79 Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sat, 8 Jan 2022 06:17:15 -0500 Subject: [PATCH 01/11] ospfd: cli: add opaque data to json output Signed-off-by: Christian Hopps --- ospfd/ospf_apiserver.c | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index 5229c6f44c..d6c1e28d41 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -2061,9 +2061,6 @@ void ospf_apiserver_show_info(struct vty *vty, struct json_object *json, struct opaque_lsa *olsa; int opaquelen; - if (json) - return; - olsa = (struct opaque_lsa *)lsa->data; if (VALID_OPAQUE_INFO_LEN(lsa->data)) @@ -2072,7 +2069,10 @@ void ospf_apiserver_show_info(struct vty *vty, struct json_object *json, opaquelen = 0; /* Output information about opaque LSAs */ - if (vty != NULL) { + if (json) + json_object_string_addf(json, "opaqueData", "%*pHXn", + (int)opaquelen, olsa->data); + else if (vty != NULL) { int i; vty_out(vty, " Added using OSPF API: %u octets of opaque data %s\n", From a6c39c3d1a2b1edf74c84c0a1b7b5a590f528626 Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Mon, 17 Jan 2022 07:54:12 -0500 Subject: [PATCH 02/11] ospfd: cli: add client api debug option Signed-off-by: Christian Hopps --- doc/user/ospfd.rst | 4 ++++ ospfd/ospf_dump.c | 46 ++++++++++++++++++++++++++++++++++++++++++++++ ospfd/ospf_dump.h | 4 ++++ 3 files changed, 54 insertions(+) diff --git a/doc/user/ospfd.rst b/doc/user/ospfd.rst index d1a0bb6f7b..5db4d63eed 100644 --- a/doc/user/ospfd.rst +++ b/doc/user/ospfd.rst @@ -1064,6 +1064,10 @@ Debugging OSPF library messages and OSPF BFD integration messages that are mostly state transitions and validation problems. +.. clicmd:: debug ospf client-api + + Show debug information for the OSPF opaque data client API. + .. clicmd:: debug ospf packet (hello|dd|ls-request|ls-update|ls-ack|all) (send|recv) [detail] diff --git a/ospfd/ospf_dump.c b/ospfd/ospf_dump.c index f40d056742..4d76181c6a 100644 --- a/ospfd/ospf_dump.c +++ b/ospfd/ospf_dump.c @@ -62,6 +62,7 @@ unsigned long conf_debug_ospf_defaultinfo = 0; unsigned long conf_debug_ospf_ldp_sync = 0; unsigned long conf_debug_ospf_gr = 0; unsigned long conf_debug_ospf_bfd; +unsigned long conf_debug_ospf_client_api; /* Enable debug option variables -- valid only session. */ unsigned long term_debug_ospf_packet[5] = {0, 0, 0, 0, 0}; @@ -79,6 +80,7 @@ unsigned long term_debug_ospf_defaultinfo; unsigned long term_debug_ospf_ldp_sync; unsigned long term_debug_ospf_gr = 0; unsigned long term_debug_ospf_bfd; +unsigned long term_debug_ospf_client_api; const char *ospf_redist_string(unsigned int route_type) { @@ -1620,6 +1622,33 @@ DEFPY(debug_ospf_bfd, debug_ospf_bfd_cmd, return CMD_SUCCESS; } +DEFUN(debug_ospf_client_api, + debug_ospf_client_api_cmd, + "debug ospf client-api", + DEBUG_STR OSPF_STR + "OSPF client API information\n") +{ + if (vty->node == CONFIG_NODE) + CONF_DEBUG_ON(client_api, CLIENT_API); + TERM_DEBUG_ON(client_api, CLIENT_API); + return CMD_SUCCESS; +} + +DEFUN(no_debug_ospf_client_api, + no_debug_ospf_client_api_cmd, + "no debug ospf client-api", + NO_STR + DEBUG_STR + OSPF_STR + "OSPF client API information\n") +{ + if (vty->node == CONFIG_NODE) + CONF_DEBUG_OFF(client_api, CLIENT_API); + TERM_DEBUG_OFF(client_api, CLIENT_API); + + return CMD_SUCCESS; +} + DEFUN (no_debug_ospf, no_debug_ospf_cmd, "no debug ospf", @@ -1654,6 +1683,7 @@ DEFUN (no_debug_ospf, DEBUG_OFF(te, TE); DEBUG_OFF(sr, SR); DEBUG_OFF(ti_lfa, TI_LFA); + DEBUG_OFF(client_api, CLIENT_API); /* BFD debugging is two parts: OSPF and library. */ DEBUG_OFF(bfd, BFD_LIB); @@ -1690,6 +1720,7 @@ DEFUN (no_debug_ospf, TERM_DEBUG_OFF(sr, SR); TERM_DEBUG_OFF(ti_lfa, TI_LFA); TERM_DEBUG_OFF(bfd, BFD_LIB); + TERM_DEBUG_OFF(client_api, CLIENT_API); return CMD_SUCCESS; } @@ -1815,6 +1846,10 @@ static int show_debugging_ospf_common(struct vty *vty) vty_out(vty, " OSPF BFD integration library debugging is on\n"); + /* Show debug status for LDP-SYNC. */ + if (IS_DEBUG_OSPF(client_api, CLIENT_API) == OSPF_DEBUG_CLIENT_API) + vty_out(vty, " OSPF client-api debugging is on\n"); + vty_out(vty, "\n"); return CMD_SUCCESS; @@ -2007,6 +2042,13 @@ static int config_write_debug(struct vty *vty) write = 1; } + /* debug ospf client-api */ + if (IS_CONF_DEBUG_OSPF(client_api, CLIENT_API) == + OSPF_DEBUG_CLIENT_API) { + vty_out(vty, "debug ospf%s client-api\n", str); + write = 1; + } + return write; } @@ -2027,6 +2069,7 @@ void ospf_debug_init(void) install_element(ENABLE_NODE, &debug_ospf_ti_lfa_cmd); install_element(ENABLE_NODE, &debug_ospf_default_info_cmd); install_element(ENABLE_NODE, &debug_ospf_ldp_sync_cmd); + install_element(ENABLE_NODE, &debug_ospf_client_api_cmd); install_element(ENABLE_NODE, &no_debug_ospf_ism_cmd); install_element(ENABLE_NODE, &no_debug_ospf_nsm_cmd); install_element(ENABLE_NODE, &no_debug_ospf_lsa_cmd); @@ -2038,6 +2081,7 @@ void ospf_debug_init(void) install_element(ENABLE_NODE, &no_debug_ospf_ti_lfa_cmd); install_element(ENABLE_NODE, &no_debug_ospf_default_info_cmd); install_element(ENABLE_NODE, &no_debug_ospf_ldp_sync_cmd); + install_element(ENABLE_NODE, &no_debug_ospf_client_api_cmd); install_element(ENABLE_NODE, &debug_ospf_gr_cmd); install_element(ENABLE_NODE, &debug_ospf_bfd_cmd); @@ -2072,6 +2116,7 @@ void ospf_debug_init(void) install_element(CONFIG_NODE, &debug_ospf_ti_lfa_cmd); install_element(CONFIG_NODE, &debug_ospf_default_info_cmd); install_element(CONFIG_NODE, &debug_ospf_ldp_sync_cmd); + install_element(CONFIG_NODE, &debug_ospf_client_api_cmd); install_element(CONFIG_NODE, &no_debug_ospf_nsm_cmd); install_element(CONFIG_NODE, &no_debug_ospf_lsa_cmd); install_element(CONFIG_NODE, &no_debug_ospf_zebra_cmd); @@ -2082,6 +2127,7 @@ void ospf_debug_init(void) install_element(CONFIG_NODE, &no_debug_ospf_ti_lfa_cmd); install_element(CONFIG_NODE, &no_debug_ospf_default_info_cmd); install_element(CONFIG_NODE, &no_debug_ospf_ldp_sync_cmd); + install_element(CONFIG_NODE, &no_debug_ospf_client_api_cmd); install_element(CONFIG_NODE, &debug_ospf_gr_cmd); install_element(CONFIG_NODE, &debug_ospf_bfd_cmd); diff --git a/ospfd/ospf_dump.h b/ospfd/ospf_dump.h index 58227d038e..251be7c8d1 100644 --- a/ospfd/ospf_dump.h +++ b/ospfd/ospf_dump.h @@ -68,6 +68,8 @@ #define OSPF_DEBUG_BFD_LIB 0x01 +#define OSPF_DEBUG_CLIENT_API 0x01 + /* Macro for setting debug option. */ #define CONF_DEBUG_PACKET_ON(a, b) conf_debug_ospf_packet[a] |= (b) #define CONF_DEBUG_PACKET_OFF(a, b) conf_debug_ospf_packet[a] &= ~(b) @@ -118,6 +120,7 @@ #define IS_DEBUG_OSPF_LDP_SYNC IS_DEBUG_OSPF(ldp_sync, LDP_SYNC) #define IS_DEBUG_OSPF_GR IS_DEBUG_OSPF(gr, GR) +#define IS_DEBUG_OSPF_CLIENT_API IS_DEBUG_OSPF(client_api, CLIENT_API) #define IS_CONF_DEBUG_OSPF_PACKET(a, b) \ (conf_debug_ospf_packet[a] & OSPF_DEBUG_##b) @@ -142,6 +145,7 @@ extern unsigned long term_debug_ospf_defaultinfo; extern unsigned long term_debug_ospf_ldp_sync; extern unsigned long term_debug_ospf_gr; extern unsigned long term_debug_ospf_bfd; +extern unsigned long term_debug_ospf_client_api; /* Message Strings. */ extern char *ospf_lsa_type_str[]; From b538baf352429ef238c7d6c8e23bb643d8e051cd Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sat, 15 Jan 2022 06:13:43 -0500 Subject: [PATCH 03/11] ospfd: add all_rtrs route table when opaque enabled The reachable router table is used by OSPF opaque clients in order to determine if the router advertising the opaque LSA data is reachable (i.e., 2-way conectivity check). Signed-off-by: Christian Hopps --- ospfd/ospf_route.c | 23 ++++++++++++++++++-- ospfd/ospf_route.h | 5 +++-- ospfd/ospf_spf.c | 42 ++++++++++++++++++++++++++++++------- ospfd/ospf_spf.h | 3 +++ ospfd/ospf_ti_lfa.c | 13 +++++++----- ospfd/ospfd.h | 3 +++ tests/ospfd/test_ospf_spf.c | 6 ++++-- 7 files changed, 76 insertions(+), 19 deletions(-) diff --git a/ospfd/ospf_route.c b/ospfd/ospf_route.c index ec0c5524c9..c5b26bbd76 100644 --- a/ospfd/ospf_route.c +++ b/ospfd/ospf_route.c @@ -363,7 +363,7 @@ void ospf_route_install(struct ospf *ospf, struct route_table *rt) /* RFC2328 16.1. (4). For "router". */ void ospf_intra_add_router(struct route_table *rt, struct vertex *v, - struct ospf_area *area) + struct ospf_area *area, bool add_all) { struct route_node *rn; struct ospf_route * or ; @@ -388,7 +388,8 @@ void ospf_intra_add_router(struct route_table *rt, struct vertex *v, /* If the newly added vertex is an area border router or AS boundary router, a routing table entry is added whose destination type is "router". */ - if (!IS_ROUTER_LSA_BORDER(lsa) && !IS_ROUTER_LSA_EXTERNAL(lsa)) { + if (!add_all && !IS_ROUTER_LSA_BORDER(lsa) && + !IS_ROUTER_LSA_EXTERNAL(lsa)) { if (IS_DEBUG_OSPF_EVENT) zlog_debug( "ospf_intra_add_router: this router is neither ASBR nor ABR, skipping it"); @@ -733,6 +734,24 @@ void ospf_route_table_dump(struct route_table *rt) zlog_debug("========================================"); } +void ospf_router_route_table_dump(struct route_table *rt) +{ + struct route_node *rn; + struct ospf_route *or; + struct listnode *node; + + zlog_debug("========== OSPF routing table =========="); + for (rn = route_top(rt); rn; rn = route_next(rn)) { + for (ALL_LIST_ELEMENTS_RO((struct list *)rn->info, node, or)) { + assert(or->type == OSPF_DESTINATION_ROUTER); + zlog_debug("R %-18pI4 %-15pI4 %s %d", &rn->p.u.prefix4, + &or->u.std.area_id, + ospf_path_type_str[or->path_type], or->cost); + } + } + zlog_debug("========================================"); +} + /* This is 16.4.1 implementation. o Intra-area paths using non-backbone areas are always the most preferred. o The other paths, intra-area backbone paths and inter-area paths, diff --git a/ospfd/ospf_route.h b/ospfd/ospf_route.h index 5463e70ffb..fa9478fced 100644 --- a/ospfd/ospf_route.h +++ b/ospfd/ospf_route.h @@ -139,9 +139,10 @@ extern void ospf_route_table_free(struct route_table *); extern void ospf_route_install(struct ospf *, struct route_table *); extern void ospf_route_table_dump(struct route_table *); +extern void ospf_router_route_table_dump(struct route_table *rt); -extern void ospf_intra_add_router(struct route_table *, struct vertex *, - struct ospf_area *); +extern void ospf_intra_add_router(struct route_table *rt, struct vertex *v, + struct ospf_area *area, bool add_all); extern void ospf_intra_add_transit(struct route_table *, struct vertex *, struct ospf_area *); diff --git a/ospfd/ospf_spf.c b/ospfd/ospf_spf.c index baf02365a2..1974a42f52 100644 --- a/ospfd/ospf_spf.c +++ b/ospfd/ospf_spf.c @@ -1669,6 +1669,7 @@ void ospf_spf_cleanup(struct vertex *spf, struct list *vertex_list) /* Calculating the shortest-path tree for an area, see RFC2328 16.1. */ void ospf_spf_calculate(struct ospf_area *area, struct ospf_lsa *root_lsa, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs, bool is_dry_run, bool is_root_node) { @@ -1737,10 +1738,13 @@ void ospf_spf_calculate(struct ospf_area *area, struct ospf_lsa *root_lsa, ospf_vertex_add_parent(v); /* RFC2328 16.1. (4). */ - if (v->type == OSPF_VERTEX_ROUTER) - ospf_intra_add_router(new_rtrs, v, area); - else + if (v->type != OSPF_VERTEX_ROUTER) ospf_intra_add_transit(new_table, v, area); + else { + ospf_intra_add_router(new_rtrs, v, area, false); + if (all_rtrs) + ospf_intra_add_router(all_rtrs, v, area, true); + } /* Iterate back to (2), see RFC2328 16.1. (5). */ } @@ -1748,6 +1752,8 @@ void ospf_spf_calculate(struct ospf_area *area, struct ospf_lsa *root_lsa, if (IS_DEBUG_OSPF_EVENT) { ospf_spf_dump(area->spf, 0); ospf_route_table_dump(new_table); + if (all_rtrs) + ospf_router_route_table_dump(all_rtrs); } /* @@ -1771,10 +1777,11 @@ void ospf_spf_calculate(struct ospf_area *area, struct ospf_lsa *root_lsa, void ospf_spf_calculate_area(struct ospf *ospf, struct ospf_area *area, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs) { - ospf_spf_calculate(area, area->router_lsa_self, new_table, new_rtrs, - false, true); + ospf_spf_calculate(area, area->router_lsa_self, new_table, all_rtrs, + new_rtrs, false, true); if (ospf->ti_lfa_enabled) ospf_ti_lfa_compute(area, new_table, @@ -1787,6 +1794,7 @@ void ospf_spf_calculate_area(struct ospf *ospf, struct ospf_area *area, } void ospf_spf_calculate_areas(struct ospf *ospf, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs) { struct ospf_area *area; @@ -1799,13 +1807,14 @@ void ospf_spf_calculate_areas(struct ospf *ospf, struct route_table *new_table, if (ospf->backbone && ospf->backbone == area) continue; - ospf_spf_calculate_area(ospf, area, new_table, new_rtrs); + ospf_spf_calculate_area(ospf, area, new_table, all_rtrs, + new_rtrs); } /* SPF for backbone, if required */ if (ospf->backbone) ospf_spf_calculate_area(ospf, ospf->backbone, new_table, - new_rtrs); + all_rtrs, new_rtrs); } /* Worker for SPF calculation scheduler. */ @@ -1813,6 +1822,7 @@ static void ospf_spf_calculate_schedule_worker(struct thread *thread) { struct ospf *ospf = THREAD_ARG(thread); struct route_table *new_table, *new_rtrs; + struct route_table *all_rtrs = NULL; struct timeval start_time, spf_start_time; unsigned long ia_time, prune_time, rt_time; unsigned long abr_time, total_spf_time, spf_time; @@ -1829,7 +1839,12 @@ static void ospf_spf_calculate_schedule_worker(struct thread *thread) monotime(&spf_start_time); new_table = route_table_init(); /* routing table */ new_rtrs = route_table_init(); /* ABR/ASBR routing table */ - ospf_spf_calculate_areas(ospf, new_table, new_rtrs); + + /* If we have opaque enabled then track all router reachability */ + if (CHECK_FLAG(ospf->opaque, OPAQUE_OPERATION_READY_BIT)) + all_rtrs = route_table_init(); + + ospf_spf_calculate_areas(ospf, new_table, all_rtrs, new_rtrs); spf_time = monotime_since(&spf_start_time, NULL); ospf_vl_shut_unapproved(ospf); @@ -1842,6 +1857,8 @@ static void ospf_spf_calculate_schedule_worker(struct thread *thread) /* Get rid of transit networks and routers we cannot reach anyway. */ monotime(&start_time); ospf_prune_unreachable_networks(new_table); + if (all_rtrs) + ospf_prune_unreachable_routers(all_rtrs); ospf_prune_unreachable_routers(new_rtrs); prune_time = monotime_since(&start_time, NULL); @@ -1866,6 +1883,15 @@ static void ospf_spf_calculate_schedule_worker(struct thread *thread) ospf_route_install(ospf, new_table); rt_time = monotime_since(&start_time, NULL); + /* Free old all routers routing table */ + if (ospf->oall_rtrs) + /* ospf_route_delete (ospf->old_rtrs); */ + ospf_rtrs_free(ospf->oall_rtrs); + + /* Update all routers routing table */ + ospf->oall_rtrs = ospf->all_rtrs; + ospf->all_rtrs = all_rtrs; + /* Free old ABR/ASBR routing table */ if (ospf->old_rtrs) /* ospf_route_delete (ospf->old_rtrs); */ diff --git a/ospfd/ospf_spf.h b/ospfd/ospf_spf.h index 20f38440aa..834bfd0bb0 100644 --- a/ospfd/ospf_spf.h +++ b/ospfd/ospf_spf.h @@ -76,13 +76,16 @@ extern void ospf_spf_calculate_schedule(struct ospf *, ospf_spf_reason_t); extern void ospf_spf_calculate(struct ospf_area *area, struct ospf_lsa *root_lsa, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs, bool is_dry_run, bool is_root_node); extern void ospf_spf_calculate_area(struct ospf *ospf, struct ospf_area *area, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs); extern void ospf_spf_calculate_areas(struct ospf *ospf, struct route_table *new_table, + struct route_table *all_rtrs, struct route_table *new_rtrs); extern void ospf_rtrs_free(struct route_table *); extern void ospf_spf_cleanup(struct vertex *spf, struct list *vertex_list); diff --git a/ospfd/ospf_ti_lfa.c b/ospfd/ospf_ti_lfa.c index 347128a4f4..28d24bcbe6 100644 --- a/ospfd/ospf_ti_lfa.c +++ b/ospfd/ospf_ti_lfa.c @@ -326,8 +326,8 @@ static void ospf_ti_lfa_generate_inner_label_stack( XCALLOC(MTYPE_OSPF_P_SPACE, sizeof(struct p_spaces_head)); /* dry run true, root node false */ - ospf_spf_calculate(area, start_vertex->lsa_p, new_table, new_rtrs, true, - false); + ospf_spf_calculate(area, start_vertex->lsa_p, new_table, NULL, new_rtrs, + true, false); q_node = ospf_spf_vertex_find(end_vertex->id, area->spf_vertex_list); @@ -676,6 +676,7 @@ static void ospf_ti_lfa_generate_q_spaces(struct ospf_area *area, sizeof(struct ospf_ti_lfa_node_info)); new_table = route_table_init(); + /* XXX do these get freed?? */ new_rtrs = route_table_init(); /* @@ -683,7 +684,8 @@ static void ospf_ti_lfa_generate_q_spaces(struct ospf_area *area, * dry run true, root node false */ area->spf_reversed = true; - ospf_spf_calculate(area, dest->lsa_p, new_table, new_rtrs, true, false); + ospf_spf_calculate(area, dest->lsa_p, new_table, NULL, new_rtrs, true, + false); /* Reset the flag for reverse SPF */ area->spf_reversed = false; @@ -750,6 +752,7 @@ static void ospf_ti_lfa_generate_post_convergence_spf(struct ospf_area *area, struct route_table *new_table, *new_rtrs; new_table = route_table_init(); + /* XXX do these get freed?? */ new_rtrs = route_table_init(); area->spf_protected_resource = p_space->protected_resource; @@ -769,8 +772,8 @@ static void ospf_ti_lfa_generate_post_convergence_spf(struct ospf_area *area, * endeavour (because LSAs are stored as a 'raw' stream), so we go with * this rather hacky way for now. */ - ospf_spf_calculate(area, area->router_lsa_self, new_table, new_rtrs, - true, false); + ospf_spf_calculate(area, area->router_lsa_self, new_table, NULL, + new_rtrs, true, false); p_space->pc_spf = area->spf; p_space->pc_vertex_list = area->spf_vertex_list; diff --git a/ospfd/ospfd.h b/ospfd/ospfd.h index 268e4d6f8d..76501dd6bb 100644 --- a/ospfd/ospfd.h +++ b/ospfd/ospfd.h @@ -234,6 +234,9 @@ struct ospf { struct route_table *old_table; /* Old routing table. */ struct route_table *new_table; /* Current routing table. */ + struct route_table *oall_rtrs; /* Old router RT. */ + struct route_table *all_rtrs; /* New routers RT. */ + struct route_table *old_rtrs; /* Old ABR/ASBR RT. */ struct route_table *new_rtrs; /* New ABR/ASBR RT. */ diff --git a/tests/ospfd/test_ospf_spf.c b/tests/ospfd/test_ospf_spf.c index 3562b5f2f8..73f2e29834 100644 --- a/tests/ospfd/test_ospf_spf.c +++ b/tests/ospfd/test_ospf_spf.c @@ -52,6 +52,7 @@ static void test_run_spf(struct vty *vty, struct ospf *ospf, enum protection_type protection_type, bool verbose) { struct route_table *new_table, *new_rtrs; + struct route_table *all_rtrs = NULL; struct ospf_area *area; struct p_space *p_space; struct q_space *q_space; @@ -63,10 +64,11 @@ static void test_run_spf(struct vty *vty, struct ospf *ospf, new_table = route_table_init(); new_rtrs = route_table_init(); + all_rtrs = route_table_init(); /* dryrun true, root_node false */ - ospf_spf_calculate(area, area->router_lsa_self, new_table, new_rtrs, - true, false); + ospf_spf_calculate(area, area->router_lsa_self, new_table, all_rtrs, + new_rtrs, true, false); if (verbose) { vty_out(vty, "SPF Tree without TI-LFA backup paths:\n\n"); From 149491af80ceaeb666a9bf06f97e918a64c46a5c Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Wed, 1 Jun 2022 15:25:35 -0400 Subject: [PATCH 04/11] ospfd: api: add reachable router notifications Reachable router information is used by OSPF opaque clients in order to determine if the router advertising the opaque LSA data is reachable (i.e., 2-way conectivity check). Signed-off-by: Christian Hopps --- ospfd/ospf_api.c | 31 ++++++++ ospfd/ospf_api.h | 16 ++++ ospfd/ospf_apiserver.c | 168 +++++++++++++++++++++++++++++++++++++++++ ospfd/ospf_apiserver.h | 12 ++- ospfd/ospf_spf.c | 2 + 5 files changed, 228 insertions(+), 1 deletion(-) diff --git a/ospfd/ospf_api.c b/ospfd/ospf_api.c index 81de882754..99bc6c0b03 100644 --- a/ospfd/ospf_api.c +++ b/ospfd/ospf_api.c @@ -177,6 +177,10 @@ const char *ospf_api_typename(int msgtype) { MSG_NSM_CHANGE, "NSM change", }, + { + MSG_REACHABLE_CHANGE, + "Reachable change", + }, }; int i, n = array_size(NameTab); @@ -651,4 +655,31 @@ struct msg *new_msg_lsa_change_notify(uint8_t msgtype, uint32_t seqnum, return msg_new(msgtype, nmsg, seqnum, len); } +struct msg *new_msg_reachable_change(uint32_t seqnum, uint16_t nadd, + struct in_addr *add, uint16_t nremove, + struct in_addr *remove) +{ + uint8_t buf[OSPF_API_MAX_MSG_SIZE]; + struct msg_reachable_change *nmsg = (void *)buf; + const uint insz = sizeof(*nmsg->router_ids); + const uint nmax = (sizeof(buf) - sizeof(*nmsg)) / insz; + uint len; + + if (nadd > nmax) + nadd = nmax; + if (nremove > (nmax - nadd)) + nremove = (nmax - nadd); + + if (nadd) + memcpy(nmsg->router_ids, add, nadd * insz); + if (nremove) + memcpy(&nmsg->router_ids[nadd], remove, nremove * insz); + + nmsg->nadd = htons(nadd); + nmsg->nremove = htons(nremove); + len = sizeof(*nmsg) + insz * (nadd + nremove); + + return msg_new(MSG_REACHABLE_CHANGE, nmsg, seqnum, len); +} + #endif /* SUPPORT_OSPF_API */ diff --git a/ospfd/ospf_api.h b/ospfd/ospf_api.h index c20284aed5..7ff39dc123 100644 --- a/ospfd/ospf_api.h +++ b/ospfd/ospf_api.h @@ -26,6 +26,9 @@ #ifndef _OSPF_API_H #define _OSPF_API_H +#include +#include "ospf_lsa.h" + #define OSPF_API_VERSION 1 /* MTYPE definition is not reflected to "memory.h". */ @@ -112,6 +115,7 @@ extern void msg_fifo_free(struct msg_fifo *fifo); #define MSG_SYNC_LSDB 4 #define MSG_ORIGINATE_REQUEST 5 #define MSG_DELETE_REQUEST 6 +#define MSG_SYNC_REACHABLE 7 /* Messages from OSPF daemon. */ #define MSG_REPLY 10 @@ -122,6 +126,7 @@ extern void msg_fifo_free(struct msg_fifo *fifo); #define MSG_DEL_IF 15 #define MSG_ISM_CHANGE 16 #define MSG_NSM_CHANGE 17 +#define MSG_REACHABLE_CHANGE 18 struct msg_register_opaque_type { uint8_t lsatype; @@ -247,6 +252,12 @@ struct msg_nsm_change { uint8_t pad[3]; }; +struct msg_reachable_change { + uint16_t nadd; + uint16_t nremove; + struct in_addr router_ids[]; /* add followed by remove */ +}; + /* We make use of a union to define a structure that covers all possible API messages. This allows us to find out how much memory needs to be reserved for the largest API message. */ @@ -265,6 +276,7 @@ struct apimsg { struct msg_ism_change ism_change; struct msg_nsm_change nsm_change; struct msg_lsa_change_notify lsa_change_notify; + struct msg_reachable_change reachable_change; } u; }; @@ -320,6 +332,10 @@ extern struct msg *new_msg_lsa_change_notify(uint8_t msgtype, uint32_t seqnum, uint8_t is_self_originated, struct lsa_header *data); +extern struct msg *new_msg_reachable_change(uint32_t seqnum, uint16_t nadd, + struct in_addr *add, + uint16_t nremove, + struct in_addr *remove); /* string printing functions */ extern const char *ospf_api_errname(int errcode); extern const char *ospf_api_typename(int msgtype); diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index d6c1e28d41..259ba2a3f1 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -726,6 +726,7 @@ static int ospf_apiserver_send_msg(struct ospf_apiserver *apiserv, case MSG_DEL_IF: case MSG_ISM_CHANGE: case MSG_NSM_CHANGE: + case MSG_REACHABLE_CHANGE: fifo = apiserv->out_async_fifo; fd = apiserv->fd_async; event = OSPF_APISERVER_ASYNC_WRITE; @@ -799,6 +800,9 @@ int ospf_apiserver_handle_msg(struct ospf_apiserver *apiserv, struct msg *msg) case MSG_DELETE_REQUEST: rc = ospf_apiserver_handle_delete_request(apiserv, msg); break; + case MSG_SYNC_REACHABLE: + rc = ospf_apiserver_handle_sync_reachable(apiserv, msg); + break; default: zlog_warn("ospf_apiserver_handle_msg: Unknown message type: %d", msg->hdr.msgtype); @@ -1343,6 +1347,59 @@ int ospf_apiserver_handle_sync_lsdb(struct ospf_apiserver *apiserv, return rc; } +/* ----------------------------------------------------------- + * Followings are functions for Reachability synchronization. + * ----------------------------------------------------------- + */ + +int ospf_apiserver_handle_sync_reachable(struct ospf_apiserver *apiserv, + struct msg *msg) +{ + struct ospf *ospf = ospf_lookup_by_vrf_id(VRF_DEFAULT); + struct route_table *rt = ospf->all_rtrs; + uint32_t seqnum = msg_get_seq(msg); + struct in_addr *a, *abuf; + struct msg_reachable_change *areach; + struct msg *amsg; + uint mcount, count; + int _rc, rc = 0; + + if (!rt) + goto out; + + /* send all adds based on current reachable routers */ + a = abuf = XCALLOC(MTYPE_OSPF_APISERVER, + sizeof(struct in_addr) * rt->count); + for (struct route_node *rn = route_top(rt); rn; rn = route_next(rn)) + if (listhead((struct list *)rn->info)) + *a++ = rn->p.u.prefix4; + + assert((a - abuf) <= (long)rt->count); + count = (a - abuf); + + a = abuf; + while (count && !rc) { + amsg = new_msg_reachable_change(seqnum, count, a, 0, NULL); + areach = (struct msg_reachable_change *)STREAM_DATA(amsg->s); + mcount = ntohs(areach->nadd) + ntohs(areach->nremove); + assert(mcount <= count); + a = a + mcount; + count -= mcount; + rc = ospf_apiserver_send_msg(apiserv, amsg); + msg_free(amsg); + } + XFREE(MTYPE_OSPF_APISERVER, abuf); + +out: + zlog_info("ospf_apiserver_handle_sync_reachable: rc %d", rc); + /* Send a reply back to client with return code */ + _rc = ospf_apiserver_send_reply(apiserv, seqnum, rc); + zlog_info("ospf_apiserver_handle_sync_reachable: _rc %d", _rc); + rc = rc ? rc : _rc; + apiserv->reachable_sync = !rc; + return rc; +} + /* ----------------------------------------------------------- * Following are functions to originate or update LSA @@ -2471,4 +2528,115 @@ int ospf_apiserver_lsa_delete(struct ospf_lsa *lsa) return apiserver_notify_clients_lsa(MSG_LSA_DELETE_NOTIFY, lsa); } +/* ------------------------------------------------------------- + * Reachable functions + * ------------------------------------------------------------- + */ + +static inline int cmp_route_nodes(struct route_node *orn, + struct route_node *nrn) +{ + if (!orn) + return 1; + else if (!nrn) + return -1; + else if (orn->p.u.prefix4.s_addr < nrn->p.u.prefix4.s_addr) + return -1; + else if (orn->p.u.prefix4.s_addr > nrn->p.u.prefix4.s_addr) + return 1; + else + return 0; +} + +void ospf_apiserver_notify_reachable(struct route_table *ort, + struct route_table *nrt) +{ + struct msg *msg; + struct msg_reachable_change *areach; + struct route_node *orn, *nrn; + const uint insz = sizeof(struct in_addr); + struct in_addr *abuf = NULL, *dbuf = NULL; + struct in_addr *a = NULL, *d = NULL; + uint nadd, nremove; + int cmp; + + if (!ort && !nrt) { + if (IS_DEBUG_OSPF_CLIENT_API) + zlog_debug("%s: no routing tables", __func__); + return; + } + if (nrt && nrt->count) + a = abuf = XCALLOC(MTYPE_OSPF_APISERVER, insz * nrt->count); + if (ort && ort->count) + d = dbuf = XCALLOC(MTYPE_OSPF_APISERVER, insz * ort->count); + + /* walk both tables */ + orn = ort ? route_top(ort) : NULL; + nrn = nrt ? route_top(nrt) : NULL; + while (orn || nrn) { + if (orn && !listhead((struct list *)orn->info)) { + orn = route_next(orn); + continue; + } + if (nrn && !listhead((struct list *)nrn->info)) { + nrn = route_next(nrn); + continue; + } + cmp = cmp_route_nodes(orn, nrn); + if (!cmp) { + /* if old == new advance old and new */ + if (IS_DEBUG_OSPF_CLIENT_API) + zlog_debug("keeping router id: %pI4", + &orn->p.u.prefix4); + orn = route_next(orn); + nrn = route_next(nrn); + } else if (cmp < 0) { + assert(d != NULL); /* Silence SA warning */ + + /* if old < new, delete old, advance old */ + *d++ = orn->p.u.prefix4; + if (IS_DEBUG_OSPF_CLIENT_API) + zlog_debug("removing router id: %pI4", + &orn->p.u.prefix4); + orn = route_next(orn); + } else { + assert(a != NULL); /* Silence SA warning */ + + /* if new < old, add new, advance new */ + *a++ = nrn->p.u.prefix4; + if (IS_DEBUG_OSPF_CLIENT_API) + zlog_debug("adding router id: %pI4", + &nrn->p.u.prefix4); + nrn = route_next(nrn); + } + } + + nadd = abuf ? (a - abuf) : 0; + nremove = dbuf ? (d - dbuf) : 0; + a = abuf; + d = dbuf; + + while (nadd + nremove) { + msg = new_msg_reachable_change(0, nadd, a, nremove, d); + areach = (struct msg_reachable_change *)STREAM_DATA(msg->s); + + a += ntohs(areach->nadd); + nadd = nadd - ntohs(areach->nadd); + + d += ntohs(areach->nremove); + nremove = nremove - ntohs(areach->nremove); + + if (IS_DEBUG_OSPF_CLIENT_API) + zlog_debug("%s: adding %d removing %d", __func__, + ntohs(areach->nadd), ntohs(areach->nremove)); + ospf_apiserver_clients_notify_all(msg); + msg_free(msg); + } + if (abuf) + XFREE(MTYPE_OSPF_APISERVER, abuf); + if (dbuf) + XFREE(MTYPE_OSPF_APISERVER, dbuf); +} + + #endif /* SUPPORT_OSPF_API */ diff --git a/ospfd/ospf_apiserver.h b/ospfd/ospf_apiserver.h index b4d8bb2f52..8a756e9c3c 100644 --- a/ospfd/ospf_apiserver.h +++ b/ospfd/ospf_apiserver.h @@ -22,6 +22,10 @@ #ifndef _OSPF_APISERVER_H #define _OSPF_APISERVER_H +#include +#include "ospf_api.h" +#include "ospf_lsdb.h" + /* MTYPE definition is not reflected to "memory.h". */ #define MTYPE_OSPF_APISERVER MTYPE_TMP #define MTYPE_OSPF_APISERVER_MSGFILTER MTYPE_TMP @@ -52,6 +56,9 @@ struct ospf_apiserver { /* Temporary storage for LSA instances to be refreshed. */ struct ospf_lsdb reserve; + /* Sync reachable routers */ + bool reachable_sync; + /* filter for LSA update/delete notifies */ struct lsa_filter_type *filter; @@ -144,8 +151,11 @@ extern int ospf_apiserver_handle_delete_request(struct ospf_apiserver *apiserv, struct msg *msg); extern int ospf_apiserver_handle_sync_lsdb(struct ospf_apiserver *apiserv, struct msg *msg); +extern int ospf_apiserver_handle_sync_reachable(struct ospf_apiserver *apiserv, + struct msg *msg); - +extern void ospf_apiserver_notify_reachable(struct route_table *ort, + struct route_table *nrt); /* ----------------------------------------------------------- * Following are functions for LSA origination/deletion * ----------------------------------------------------------- diff --git a/ospfd/ospf_spf.c b/ospfd/ospf_spf.c index 1974a42f52..44549b980c 100644 --- a/ospfd/ospf_spf.c +++ b/ospfd/ospf_spf.c @@ -48,6 +48,7 @@ #include "ospfd/ospf_sr.h" #include "ospfd/ospf_ti_lfa.h" #include "ospfd/ospf_errors.h" +#include "ospfd/ospf_apiserver.h" /* Variables to ensure a SPF scheduled log message is printed only once */ @@ -1891,6 +1892,7 @@ static void ospf_spf_calculate_schedule_worker(struct thread *thread) /* Update all routers routing table */ ospf->oall_rtrs = ospf->all_rtrs; ospf->all_rtrs = all_rtrs; + ospf_apiserver_notify_reachable(ospf->oall_rtrs, ospf->all_rtrs); /* Free old ABR/ASBR routing table */ if (ospf->old_rtrs) From d86760acad1225dac16776e4cb7366d9b345825c Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Mon, 17 Jan 2022 07:55:14 -0500 Subject: [PATCH 05/11] ospfd: cli: add "show ip ospf reachable-routers" CLI Signed-off-by: Christian Hopps --- doc/user/ospfd.rst | 6 +++ ospfd/ospf_vty.c | 120 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 124 insertions(+), 2 deletions(-) diff --git a/doc/user/ospfd.rst b/doc/user/ospfd.rst index 5db4d63eed..068bb8ba31 100644 --- a/doc/user/ospfd.rst +++ b/doc/user/ospfd.rst @@ -867,6 +867,12 @@ Opaque LSA Show Opaque LSA from the database. +.. clicmd:: show ip ospf (1-65535) reachable-routers + +.. clicmd:: show ip ospf [vrf ] reachable-routers + + Show routing table of reachable routers. + .. _ospf-traffic-engineering: Traffic Engineering diff --git a/ospfd/ospf_vty.c b/ospfd/ospf_vty.c index 20b9180cff..e5985a8660 100644 --- a/ospfd/ospf_vty.c +++ b/ospfd/ospf_vty.c @@ -10741,8 +10741,9 @@ static void show_ip_ospf_route_router(struct vty *vty, struct ospf *ospf, *json_nexthop = NULL; if (!json) - vty_out(vty, - "============ OSPF router routing table =============\n"); + vty_out(vty, "============ OSPF %s table =============\n", + ospf->all_rtrs == rtrs ? "reachable routers" + : "router routing"); for (rn = route_top(rtrs); rn; rn = route_next(rn)) { if (rn->info == NULL) @@ -11004,6 +11005,114 @@ static void show_ip_ospf_route_external(struct vty *vty, struct ospf *ospf, vty_out(vty, "\n"); } +static int show_ip_ospf_reachable_routers_common(struct vty *vty, + struct ospf *ospf, + uint8_t use_vrf) +{ + if (ospf->instance) + vty_out(vty, "\nOSPF Instance: %d\n\n", ospf->instance); + + ospf_show_vrf_name(ospf, vty, NULL, use_vrf); + + if (ospf->all_rtrs == NULL) { + vty_out(vty, "No OSPF reachable router information exist\n"); + return CMD_SUCCESS; + } + + /* Show Router routes. */ + show_ip_ospf_route_router(vty, ospf, ospf->all_rtrs, NULL); + + vty_out(vty, "\n"); + + return CMD_SUCCESS; +} + +DEFUN (show_ip_ospf_reachable_routers, + show_ip_ospf_reachable_routers_cmd, + "show ip ospf [vrf ] reachable-routers", + SHOW_STR + IP_STR + "OSPF information\n" + VRF_CMD_HELP_STR + "All VRFs\n" + "Show all the reachable OSPF routers\n") +{ + struct ospf *ospf = NULL; + struct listnode *node = NULL; + char *vrf_name = NULL; + bool all_vrf = false; + int ret = CMD_SUCCESS; + int inst = 0; + int idx_vrf = 0; + uint8_t use_vrf = 0; + + OSPF_FIND_VRF_ARGS(argv, argc, idx_vrf, vrf_name, all_vrf); + + if (vrf_name) { + bool ospf_output = false; + + use_vrf = 1; + + if (all_vrf) { + for (ALL_LIST_ELEMENTS_RO(om->ospf, node, ospf)) { + if (!ospf->oi_running) + continue; + + ospf_output = true; + ret = show_ip_ospf_reachable_routers_common( + vty, ospf, use_vrf); + } + + if (!ospf_output) + vty_out(vty, "%% OSPF instance not found\n"); + } else { + ospf = ospf_lookup_by_inst_name(inst, vrf_name); + if (ospf == NULL || !ospf->oi_running) { + vty_out(vty, "%% OSPF instance not found\n"); + return CMD_SUCCESS; + } + + ret = show_ip_ospf_reachable_routers_common(vty, ospf, + use_vrf); + } + } else { + /* Display default ospf (instance 0) info */ + ospf = ospf_lookup_by_vrf_id(VRF_DEFAULT); + if (ospf == NULL || !ospf->oi_running) { + vty_out(vty, "%% OSPF instance not found\n"); + return CMD_SUCCESS; + } + + ret = show_ip_ospf_reachable_routers_common(vty, ospf, use_vrf); + } + + return ret; +} + +DEFUN (show_ip_ospf_instance_reachable_routers, + show_ip_ospf_instance_reachable_routers_cmd, + "show ip ospf (1-65535) reachable-routers", + SHOW_STR + IP_STR + "OSPF information\n" + "Instance ID\n" + "Show all the reachable OSPF routers\n") +{ + int idx_number = 3; + struct ospf *ospf; + unsigned short instance = 0; + + instance = strtoul(argv[idx_number]->arg, NULL, 10); + if (instance != ospf_instance) + return CMD_NOT_MY_INSTANCE; + + ospf = ospf_lookup_instance(instance); + if (!ospf || !ospf->oi_running) + return CMD_SUCCESS; + + return show_ip_ospf_reachable_routers_common(vty, ospf, 0); +} + static int show_ip_ospf_border_routers_common(struct vty *vty, struct ospf *ospf, uint8_t use_vrf) @@ -11146,6 +11255,10 @@ static int show_ip_ospf_route_common(struct vty *vty, struct ospf *ospf, /* Show Router routes. */ show_ip_ospf_route_router(vty, ospf, ospf->new_rtrs, json_vrf); + /* Show Router routes. */ + if (ospf->all_rtrs) + show_ip_ospf_route_router(vty, ospf, ospf->all_rtrs, json_vrf); + /* Show AS External routes. */ show_ip_ospf_route_external(vty, ospf, ospf->old_external_route, json_vrf); @@ -12603,9 +12716,12 @@ void ospf_vty_show_init(void) /* "show ip ospf route" commands. */ install_element(VIEW_NODE, &show_ip_ospf_route_cmd); install_element(VIEW_NODE, &show_ip_ospf_border_routers_cmd); + install_element(VIEW_NODE, &show_ip_ospf_reachable_routers_cmd); install_element(VIEW_NODE, &show_ip_ospf_instance_route_cmd); install_element(VIEW_NODE, &show_ip_ospf_instance_border_routers_cmd); + install_element(VIEW_NODE, + &show_ip_ospf_instance_reachable_routers_cmd); /* "show ip ospf vrfs" commands. */ install_element(VIEW_NODE, &show_ip_ospf_vrfs_cmd); From 5349121b4c6ea2126be87bcbe6400a8f10ea99fc Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Sat, 8 Jan 2022 16:57:10 -0500 Subject: [PATCH 06/11] ospfd: api: fix recovery of LSA after restart of api client Prior to this fix, restarting the client just failed b/c the code tried to "refresh" the existing LSA being added, except that code checked for meta-data to exist, which was deleted when the client disconnected previously (or had never connected and the LSA state was picked up from the network). Signed-off-by: Christian Hopps --- ospfd/ospf_apiserver.c | 24 +++++++++-- ospfd/ospf_apiserver.h | 3 +- ospfd/ospf_opaque.c | 96 +++++++++++++++++++++--------------------- ospfd/ospf_opaque.h | 2 + 4 files changed, 73 insertions(+), 52 deletions(-) diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index 259ba2a3f1..4015566b10 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -1627,9 +1627,9 @@ int ospf_apiserver_handle_originate_request(struct ospf_apiserver *apiserv, /* Determine if LSA is new or an update for an existing one. */ old = ospf_lsdb_lookup(lsdb, new); - if (!old) { + if (!old || !ospf_opaque_is_owned(old)) { /* New LSA install in LSDB. */ - rc = ospf_apiserver_originate1(new); + rc = ospf_apiserver_originate1(new, old); } else { /* * Keep the new LSA instance in the "waiting place" until the @@ -1696,17 +1696,33 @@ void ospf_apiserver_flood_opaque_lsa(struct ospf_lsa *lsa) } } -int ospf_apiserver_originate1(struct ospf_lsa *lsa) +int ospf_apiserver_originate1(struct ospf_lsa *lsa, struct ospf_lsa *old) { struct ospf *ospf; ospf = ospf_lookup_by_vrf_id(VRF_DEFAULT); assert(ospf); + if (old) { + /* + * An old LSA exists that we didn't originate it in this + * session. Dump it, but increment past it's seqnum. + */ + assert(!ospf_opaque_is_owned(old)); + if (IS_LSA_MAX_SEQ(old)) { + flog_warn( + EC_OSPF_LSA_INSTALL_FAILURE, + "ospf_apiserver_originate1: old LSA at maxseq"); + return -1; + } + lsa->data->ls_seqnum = lsa_seqnum_increment(old); + ospf_discard_from_db(ospf, old->lsdb, old); + } + /* Install this LSA into LSDB. */ if (ospf_lsa_install(ospf, lsa->oi, lsa) == NULL) { flog_warn(EC_OSPF_LSA_INSTALL_FAILURE, - "ospf_apiserver_originate1: ospf_lsa_install failed"); + "%s: ospf_lsa_install failed", __func__); return -1; } diff --git a/ospfd/ospf_apiserver.h b/ospfd/ospf_apiserver.h index 8a756e9c3c..a57f172b5d 100644 --- a/ospfd/ospf_apiserver.h +++ b/ospfd/ospf_apiserver.h @@ -174,7 +174,8 @@ extern struct ospf_interface * ospf_apiserver_if_lookup_by_addr(struct in_addr address); extern struct ospf_interface * ospf_apiserver_if_lookup_by_ifp(struct interface *ifp); -extern int ospf_apiserver_originate1(struct ospf_lsa *lsa); +extern int ospf_apiserver_originate1(struct ospf_lsa *lsa, + struct ospf_lsa *old); extern void ospf_apiserver_flood_opaque_lsa(struct ospf_lsa *lsa); diff --git a/ospfd/ospf_opaque.c b/ospfd/ospf_opaque.c index 947454c0df..7e95cb591a 100644 --- a/ospfd/ospf_opaque.c +++ b/ospfd/ospf_opaque.c @@ -74,9 +74,8 @@ int ospf_apiserver_enable; static void ospf_opaque_register_vty(void); static void ospf_opaque_funclist_init(void); static void ospf_opaque_funclist_term(void); -static void free_opaque_info_per_type(void *val); +static void free_opaque_info_per_type_del(void *val); static void free_opaque_info_per_id(void *val); -static void free_opaque_info_owner(void *val); static int ospf_opaque_lsa_install_hook(struct ospf_lsa *lsa); static int ospf_opaque_lsa_delete_hook(struct ospf_lsa *lsa); @@ -141,7 +140,7 @@ int ospf_opaque_type9_lsa_init(struct ospf_interface *oi) list_delete(&oi->opaque_lsa_self); oi->opaque_lsa_self = list_new(); - oi->opaque_lsa_self->del = free_opaque_info_per_type; + oi->opaque_lsa_self->del = free_opaque_info_per_type_del; oi->t_opaque_lsa_self = NULL; return 0; } @@ -161,7 +160,7 @@ int ospf_opaque_type10_lsa_init(struct ospf_area *area) list_delete(&area->opaque_lsa_self); area->opaque_lsa_self = list_new(); - area->opaque_lsa_self->del = free_opaque_info_per_type; + area->opaque_lsa_self->del = free_opaque_info_per_type_del; area->t_opaque_lsa_self = NULL; #ifdef MONITOR_LSDB_CHANGE @@ -189,7 +188,7 @@ int ospf_opaque_type11_lsa_init(struct ospf *top) list_delete(&top->opaque_lsa_self); top->opaque_lsa_self = list_new(); - top->opaque_lsa_self->del = free_opaque_info_per_type; + top->opaque_lsa_self->del = free_opaque_info_per_type_del; top->t_opaque_lsa_self = NULL; #ifdef MONITOR_LSDB_CHANGE @@ -263,6 +262,9 @@ static const char *ospf_opaque_type_name(uint8_t opaque_type) struct opaque_info_per_type; /* Forward declaration. */ +static void free_opaque_info_per_type(struct opaque_info_per_type *oipt, + bool cleanup_owner); + struct ospf_opaque_functab { uint8_t opaque_type; struct opaque_info_per_type *oipt; @@ -433,12 +435,9 @@ void ospf_delete_opaque_functab(uint8_t lsa_type, uint8_t opaque_type) if (functab->opaque_type == opaque_type) { /* Cleanup internal control information, if it * still remains. */ - if (functab->oipt != NULL) { - free_opaque_info_owner(functab->oipt); - free_opaque_info_per_type( - functab->oipt); - } - + if (functab->oipt != NULL) + free_opaque_info_per_type(functab->oipt, + true); /* Dequeue listnode entry from the list. */ listnode_delete(funclist, functab); @@ -558,8 +557,7 @@ register_opaque_info_per_type(struct ospf_opaque_functab *functab, case OSPF_OPAQUE_AS_LSA: top = ospf_lookup_by_vrf_id(new->vrf_id); if (new->area != NULL && (top = new->area->ospf) == NULL) { - free_opaque_info_owner(oipt); - free_opaque_info_per_type(oipt); + free_opaque_info_per_type(oipt, true); oipt = NULL; goto out; /* This case may not exist. */ } @@ -571,8 +569,7 @@ register_opaque_info_per_type(struct ospf_opaque_functab *functab, EC_OSPF_LSA_UNEXPECTED, "register_opaque_info_per_type: Unexpected LSA-type(%u)", new->data->type); - free_opaque_info_owner(oipt); - free_opaque_info_per_type(oipt); + free_opaque_info_per_type(oipt, true); oipt = NULL; goto out; /* This case may not exist. */ } @@ -589,42 +586,13 @@ out: return oipt; } -/* Remove "oipt" from its owner's self-originated LSA list. */ -static void free_opaque_info_owner(void *val) +static void free_opaque_info_per_type(struct opaque_info_per_type *oipt, + bool cleanup_owner) { - struct opaque_info_per_type *oipt = (struct opaque_info_per_type *)val; - - switch (oipt->lsa_type) { - case OSPF_OPAQUE_LINK_LSA: { - struct ospf_interface *oi = - (struct ospf_interface *)(oipt->owner); - listnode_delete(oi->opaque_lsa_self, oipt); - break; - } - case OSPF_OPAQUE_AREA_LSA: { - struct ospf_area *area = (struct ospf_area *)(oipt->owner); - listnode_delete(area->opaque_lsa_self, oipt); - break; - } - case OSPF_OPAQUE_AS_LSA: { - struct ospf *top = (struct ospf *)(oipt->owner); - listnode_delete(top->opaque_lsa_self, oipt); - break; - } - default: - flog_warn(EC_OSPF_LSA_UNEXPECTED, - "free_opaque_info_owner: Unexpected LSA-type(%u)", - oipt->lsa_type); - break; /* This case may not exist. */ - } -} - -static void free_opaque_info_per_type(void *val) -{ - struct opaque_info_per_type *oipt = (struct opaque_info_per_type *)val; struct opaque_info_per_id *oipi; struct ospf_lsa *lsa; struct listnode *node, *nnode; + struct list *l; /* Control information per opaque-id may still exist. */ for (ALL_LIST_ELEMENTS(oipt->id_list, node, nnode, oipi)) { @@ -637,10 +605,37 @@ static void free_opaque_info_per_type(void *val) OSPF_TIMER_OFF(oipt->t_opaque_lsa_self); list_delete(&oipt->id_list); + if (cleanup_owner) { + /* Remove from its owner's self-originated LSA list. */ + switch (oipt->lsa_type) { + case OSPF_OPAQUE_LINK_LSA: + l = ((struct ospf_interface *)oipt->owner) + ->opaque_lsa_self; + break; + case OSPF_OPAQUE_AREA_LSA: + l = ((struct ospf_area *)oipt->owner)->opaque_lsa_self; + break; + case OSPF_OPAQUE_AS_LSA: + l = ((struct ospf *)oipt->owner)->opaque_lsa_self; + break; + default: + flog_warn( + EC_OSPF_LSA_UNEXPECTED, + "free_opaque_info_owner: Unexpected LSA-type(%u)", + oipt->lsa_type); + return; + } + listnode_delete(l, oipt); + } XFREE(MTYPE_OPAQUE_INFO_PER_TYPE, oipt); return; } +static void free_opaque_info_per_type_del(void *val) +{ + free_opaque_info_per_type((struct opaque_info_per_type *)val, false); +} + static struct opaque_info_per_type * lookup_opaque_info_by_type(struct ospf_lsa *lsa) { @@ -758,6 +753,13 @@ out: return oipi; } +int ospf_opaque_is_owned(struct ospf_lsa *lsa) +{ + struct opaque_info_per_type *oipt = lookup_opaque_info_by_type(lsa); + + return (oipt != NULL && lookup_opaque_info_by_id(oipt, lsa) != NULL); +} + /*------------------------------------------------------------------------* * Following are (vty) configuration functions for Opaque-LSAs handling. *------------------------------------------------------------------------*/ diff --git a/ospfd/ospf_opaque.h b/ospfd/ospf_opaque.h index 59d4288bf2..9c76877908 100644 --- a/ospfd/ospf_opaque.h +++ b/ospfd/ospf_opaque.h @@ -173,4 +173,6 @@ extern void ospf_opaque_self_originated_lsa_received(struct ospf_neighbor *nbr, struct ospf_lsa *lsa); extern struct ospf *oi_to_top(struct ospf_interface *oi); +extern int ospf_opaque_is_owned(struct ospf_lsa *lsa); + #endif /* _ZEBRA_OSPF_OPAQUE_H */ From b1e40ef0ea991e157a3299e8aab0c5d50a0412cd Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Fri, 14 Jan 2022 10:39:07 -0500 Subject: [PATCH 07/11] ospfd: api: remove unused api code/message creation Signed-off-by: Christian Hopps --- ospfd/ospf_apiserver.c | 48 ++++++++---------------------------------- ospfd/ospf_apiserver.h | 3 --- 2 files changed, 9 insertions(+), 42 deletions(-) diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index 4015566b10..6d61f53d6e 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -2406,8 +2406,8 @@ void ospf_apiserver_clients_notify_nsm_change(struct ospf_neighbor *nbr) msg_free(msg); } -static void apiserver_clients_lsa_change_notify(uint8_t msgtype, - struct ospf_lsa *lsa) +static int apiserver_clients_lsa_change_notify(uint8_t msgtype, + struct ospf_lsa *lsa) { struct msg *msg; struct listnode *node, *nnode; @@ -2435,7 +2435,7 @@ static void apiserver_clients_lsa_change_notify(uint8_t msgtype, if (!msg) { zlog_warn( "apiserver_clients_lsa_change_notify: msg_new failed"); - return; + return -1; } /* Now send message to all clients with a matching filter */ @@ -2486,6 +2486,8 @@ static void apiserver_clients_lsa_change_notify(uint8_t msgtype, } /* Free message since it is not used anymore */ msg_free(msg); + + return 0; } @@ -2495,53 +2497,21 @@ static void apiserver_clients_lsa_change_notify(uint8_t msgtype, */ -static int apiserver_notify_clients_lsa(uint8_t msgtype, struct ospf_lsa *lsa) +int ospf_apiserver_lsa_update(struct ospf_lsa *lsa) { - struct msg *msg; - /* default area for AS-External and Opaque11 LSAs */ - struct in_addr area_id = {.s_addr = 0L}; - - /* default interface for non Opaque9 LSAs */ - struct in_addr ifaddr = {.s_addr = 0L}; /* Only notify this update if the LSA's age is smaller than MAXAGE. Otherwise clients would see LSA updates with max age just before they are deleted from the LSDB. LSA delete messages have MAXAGE too but should not be filtered. */ - if (IS_LSA_MAXAGE(lsa) && (msgtype == MSG_LSA_UPDATE_NOTIFY)) { + if (IS_LSA_MAXAGE(lsa)) return 0; - } - - if (lsa->area) { - area_id = lsa->area->area_id; - } - if (lsa->data->type == OSPF_OPAQUE_LINK_LSA) { - ifaddr = lsa->oi->address->u.prefix4; - } - msg = new_msg_lsa_change_notify(msgtype, 0L, /* no sequence number */ - ifaddr, area_id, - lsa->flags & OSPF_LSA_SELF, lsa->data); - if (!msg) { - zlog_warn("notify_clients_lsa: msg_new failed"); - return -1; - } - /* Notify all clients that new LSA is added/updated */ - apiserver_clients_lsa_change_notify(msgtype, lsa); - - /* Clients made their own copies of msg so we can free msg here */ - msg_free(msg); - - return 0; -} - -int ospf_apiserver_lsa_update(struct ospf_lsa *lsa) -{ - return apiserver_notify_clients_lsa(MSG_LSA_UPDATE_NOTIFY, lsa); + return apiserver_clients_lsa_change_notify(MSG_LSA_UPDATE_NOTIFY, lsa); } int ospf_apiserver_lsa_delete(struct ospf_lsa *lsa) { - return apiserver_notify_clients_lsa(MSG_LSA_DELETE_NOTIFY, lsa); + return apiserver_clients_lsa_change_notify(MSG_LSA_DELETE_NOTIFY, lsa); } /* ------------------------------------------------------------- diff --git a/ospfd/ospf_apiserver.h b/ospfd/ospf_apiserver.h index a57f172b5d..cb8c6d6573 100644 --- a/ospfd/ospf_apiserver.h +++ b/ospfd/ospf_apiserver.h @@ -212,7 +212,4 @@ extern void ospf_apiserver_flush_opaque_lsa(struct ospf_apiserver *apiserv, extern int ospf_apiserver_lsa_update(struct ospf_lsa *lsa); extern int ospf_apiserver_lsa_delete(struct ospf_lsa *lsa); -extern void ospf_apiserver_clients_lsa_change_notify(uint8_t msgtype, - struct ospf_lsa *lsa); - #endif /* _OSPF_APISERVER_H */ From 97355a6d92759c712b018023f67d07c4726e8c14 Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Wed, 19 Jan 2022 06:42:03 -0500 Subject: [PATCH 08/11] ospfd: api: add new ISM and NSM sync requests Signed-off-by: Christian Hopps --- ospfd/ospf_api.h | 2 + ospfd/ospf_apiserver.c | 87 ++++++++++++++++++++++++++++++++++++++++-- ospfd/ospf_apiserver.h | 5 +++ 3 files changed, 90 insertions(+), 4 deletions(-) diff --git a/ospfd/ospf_api.h b/ospfd/ospf_api.h index 7ff39dc123..50b0c21c77 100644 --- a/ospfd/ospf_api.h +++ b/ospfd/ospf_api.h @@ -116,6 +116,8 @@ extern void msg_fifo_free(struct msg_fifo *fifo); #define MSG_ORIGINATE_REQUEST 5 #define MSG_DELETE_REQUEST 6 #define MSG_SYNC_REACHABLE 7 +#define MSG_SYNC_ISM 8 +#define MSG_SYNC_NSM 9 /* Messages from OSPF daemon. */ #define MSG_REPLY 10 diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index 6d61f53d6e..1d42d2c69d 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -803,6 +803,12 @@ int ospf_apiserver_handle_msg(struct ospf_apiserver *apiserv, struct msg *msg) case MSG_SYNC_REACHABLE: rc = ospf_apiserver_handle_sync_reachable(apiserv, msg); break; + case MSG_SYNC_ISM: + rc = ospf_apiserver_handle_sync_ism(apiserv, msg); + break; + case MSG_SYNC_NSM: + rc = ospf_apiserver_handle_sync_nsm(apiserv, msg); + break; default: zlog_warn("ospf_apiserver_handle_msg: Unknown message type: %d", msg->hdr.msgtype); @@ -1347,8 +1353,9 @@ int ospf_apiserver_handle_sync_lsdb(struct ospf_apiserver *apiserv, return rc; } -/* ----------------------------------------------------------- - * Followings are functions for Reachability synchronization. +/* + * ----------------------------------------------------------- + * Followings are functions for synchronization. * ----------------------------------------------------------- */ @@ -1391,15 +1398,86 @@ int ospf_apiserver_handle_sync_reachable(struct ospf_apiserver *apiserv, XFREE(MTYPE_OSPF_APISERVER, abuf); out: - zlog_info("ospf_apiserver_handle_sync_reachable: rc %d", rc); /* Send a reply back to client with return code */ _rc = ospf_apiserver_send_reply(apiserv, seqnum, rc); - zlog_info("ospf_apiserver_handle_sync_reachable: _rc %d", _rc); rc = rc ? rc : _rc; apiserv->reachable_sync = !rc; return rc; } +int ospf_apiserver_handle_sync_ism(struct ospf_apiserver *apiserv, + struct msg *msg) +{ + struct ospf *ospf = ospf_lookup_by_vrf_id(VRF_DEFAULT); + struct listnode *anode, *inode; + struct ospf_area *area; + struct ospf_interface *oi; + struct msg *m; + uint32_t seqnum = msg_get_seq(msg); + int _rc, rc = 0; + + /* walk all areas */ + for (ALL_LIST_ELEMENTS_RO(ospf->areas, anode, area)) { + /* walk all interfaces */ + for (ALL_LIST_ELEMENTS_RO(area->oiflist, inode, oi)) { + m = new_msg_ism_change(seqnum, oi->address->u.prefix4, + area->area_id, oi->state); + rc = ospf_apiserver_send_msg(apiserv, m); + msg_free(m); + if (rc) + break; + } + if (rc) + break; + } + /* Send a reply back to client with return code */ + _rc = ospf_apiserver_send_reply(apiserv, seqnum, rc); + return rc ? rc : _rc; +} + + +int ospf_apiserver_handle_sync_nsm(struct ospf_apiserver *apiserv, + struct msg *msg) +{ + struct ospf *ospf = ospf_lookup_by_vrf_id(VRF_DEFAULT); + struct listnode *anode, *inode; + struct ospf_area *area; + struct ospf_interface *oi; + struct ospf_neighbor *nbr; + struct route_node *rn; + struct msg *m; + uint32_t seqnum = msg_get_seq(msg); + int _rc, rc = 0; + + /* walk all areas */ + for (ALL_LIST_ELEMENTS_RO(ospf->areas, anode, area)) { + /* walk all interfaces */ + for (ALL_LIST_ELEMENTS_RO(area->oiflist, inode, oi)) { + /* walk all neighbors */ + for (rn = route_top(oi->nbrs); rn; + rn = route_next(rn)) { + nbr = rn->info; + if (!nbr) + continue; + m = new_msg_nsm_change( + seqnum, oi->address->u.prefix4, + nbr->src, nbr->router_id, nbr->state); + rc = ospf_apiserver_send_msg(apiserv, m); + msg_free(m); + if (rc) + break; + } + if (rc) + break; + } + if (rc) + break; + } + /* Send a reply back to client with return code */ + _rc = ospf_apiserver_send_reply(apiserv, seqnum, rc); + return rc ? rc : _rc; +} + /* ----------------------------------------------------------- * Following are functions to originate or update LSA @@ -2088,6 +2166,7 @@ int ospf_apiserver_del_if(struct interface *ifp) if (!oi) { /* This interface is known to Zebra but not to OSPF daemon anymore. No need to tell clients about it */ + zlog_warn("ifp name=%s not known to OSPFd", ifp->name); return 0; } diff --git a/ospfd/ospf_apiserver.h b/ospfd/ospf_apiserver.h index cb8c6d6573..7d728ead93 100644 --- a/ospfd/ospf_apiserver.h +++ b/ospfd/ospf_apiserver.h @@ -153,9 +153,14 @@ extern int ospf_apiserver_handle_sync_lsdb(struct ospf_apiserver *apiserv, struct msg *msg); extern int ospf_apiserver_handle_sync_reachable(struct ospf_apiserver *apiserv, struct msg *msg); +extern int ospf_apiserver_handle_sync_ism(struct ospf_apiserver *apiserv, + struct msg *msg); +extern int ospf_apiserver_handle_sync_nsm(struct ospf_apiserver *apiserv, + struct msg *msg); extern void ospf_apiserver_notify_reachable(struct route_table *ort, struct route_table *nrt); + /* ----------------------------------------------------------- * Following are functions for LSA origination/deletion * ----------------------------------------------------------- From bd1188f904b63664c3b350ca587a04547210bf15 Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Tue, 25 Jan 2022 04:53:53 -0500 Subject: [PATCH 09/11] ospfd: api: always ready to receive opaque from client Signed-off-by: Christian Hopps --- ospfd/ospf_apiserver.c | 37 ++++++------------------------------- 1 file changed, 6 insertions(+), 31 deletions(-) diff --git a/ospfd/ospf_apiserver.c b/ospfd/ospf_apiserver.c index 1d42d2c69d..7c3fef4536 100644 --- a/ospfd/ospf_apiserver.c +++ b/ospfd/ospf_apiserver.c @@ -1562,45 +1562,20 @@ struct ospf_lsa *ospf_apiserver_opaque_lsa_new(struct ospf_area *area, int ospf_apiserver_is_ready_type9(struct ospf_interface *oi) { - /* Type 9 opaque LSA can be originated if there is at least one - active opaque-capable neighbor attached to the outgoing - interface. */ - - return (ospf_nbr_count_opaque_capable(oi) > 0); + /* We can always handle getting opaque's even if we can't flood them */ + return 1; } int ospf_apiserver_is_ready_type10(struct ospf_area *area) { - /* Type 10 opaque LSA can be originated if there is at least one - interface belonging to the area that has an active opaque-capable - neighbor. */ - struct listnode *node, *nnode; - struct ospf_interface *oi; - - for (ALL_LIST_ELEMENTS(area->oiflist, node, nnode, oi)) - /* Is there an active neighbor attached to this interface? */ - if (ospf_apiserver_is_ready_type9(oi)) - return 1; - - /* No active neighbor in area */ - return 0; + /* We can always handle getting opaque's even if we can't flood them */ + return 1; } int ospf_apiserver_is_ready_type11(struct ospf *ospf) { - /* Type 11 opaque LSA can be originated if there is at least one - interface - that has an active opaque-capable neighbor. */ - struct listnode *node, *nnode; - struct ospf_interface *oi; - - for (ALL_LIST_ELEMENTS(ospf->oiflist, node, nnode, oi)) - /* Is there an active neighbor attached to this interface? */ - if (ospf_apiserver_is_ready_type9(oi)) - return 1; - - /* No active neighbor at all */ - return 0; + /* We can always handle getting opaque's even if we can't flood them */ + return 1; } From 9191ac86fdb70e057ba46827a99c975507365bad Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Fri, 24 Dec 2021 02:04:57 -0500 Subject: [PATCH 10/11] ospfclient: add ospfclient api python class Signed-off-by: Christian Hopps --- debian/frr-pythontools.install | 1 + doc/user/installation.rst | 3 +- ospfclient/ospfclient.py | 1133 ++++++++++++++++++++++++++++++++ ospfclient/subdir.am | 8 + redhat/frr.spec.in | 11 +- 5 files changed, 1154 insertions(+), 2 deletions(-) create mode 100755 ospfclient/ospfclient.py diff --git a/debian/frr-pythontools.install b/debian/frr-pythontools.install index 820895ce68..662fbe0f51 100644 --- a/debian/frr-pythontools.install +++ b/debian/frr-pythontools.install @@ -1,3 +1,4 @@ usr/lib/frr/frr-reload.py usr/lib/frr/generate_support_bundle.py usr/lib/frr/frr_babeltrace.py +usr/lib/frr/ospfclient.py diff --git a/doc/user/installation.rst b/doc/user/installation.rst index b24a9fb471..401a1f2721 100644 --- a/doc/user/installation.rst +++ b/doc/user/installation.rst @@ -212,7 +212,8 @@ options from the list below. .. option:: --disable-ospfclient - Disable building of the example OSPF-API client. + Disable installation of the python ospfclient and building of the example + OSPF-API client. .. option:: --disable-isisd diff --git a/ospfclient/ospfclient.py b/ospfclient/ospfclient.py new file mode 100755 index 0000000000..be8b51f007 --- /dev/null +++ b/ospfclient/ospfclient.py @@ -0,0 +1,1133 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# December 22 2021, Christian Hopps +# +# Copyright 2021, LabN Consulting, L.L.C. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +import argparse +import asyncio +import errno +import logging +import socket +import struct +import sys +from asyncio import Event, Lock +from ipaddress import ip_address as ip + +FMT_APIMSGHDR = ">BBHL" +FMT_APIMSGHDR_SIZE = struct.calcsize(FMT_APIMSGHDR) + +FMT_LSA_FILTER = ">HBB" # + plus x"I" areas +LSAF_ORIGIN_NON_SELF = 0 +LSAF_ORIGIN_SELF = 1 +LSAF_ORIGIN_ANY = 2 + +FMT_LSA_HEADER = ">HBBIILHH" +FMT_LSA_HEADER_SIZE = struct.calcsize(FMT_LSA_HEADER) + +# ------------------------ +# Messages to OSPF daemon. +# ------------------------ + +MSG_REGISTER_OPAQUETYPE = 1 +MSG_UNREGISTER_OPAQUETYPE = 2 +MSG_REGISTER_EVENT = 3 +MSG_SYNC_LSDB = 4 +MSG_ORIGINATE_REQUEST = 5 +MSG_DELETE_REQUEST = 6 +MSG_SYNC_REACHABLE = 7 +MSG_SYNC_ISM = 8 +MSG_SYNC_NSM = 9 + +smsg_info = { + MSG_REGISTER_OPAQUETYPE: ("REGISTER_OPAQUETYPE", "BBxx"), + MSG_UNREGISTER_OPAQUETYPE: ("UNREGISTER_OPAQUETYPE", "BBxx"), + MSG_REGISTER_EVENT: ("REGISTER_EVENT", FMT_LSA_FILTER), + MSG_SYNC_LSDB: ("SYNC_LSDB", FMT_LSA_FILTER), + MSG_ORIGINATE_REQUEST: ("ORIGINATE_REQUEST", ">II" + FMT_LSA_HEADER[1:]), + MSG_DELETE_REQUEST: ("DELETE_REQUEST", ">IBBxxL"), + MSG_SYNC_REACHABLE: ("MSG_SYNC_REACHABLE", ""), + MSG_SYNC_ISM: ("MSG_SYNC_ISM", ""), + MSG_SYNC_NSM: ("MSG_SYNC_NSM", ""), +} + +# -------------------------- +# Messages from OSPF daemon. +# -------------------------- + +MSG_REPLY = 10 +MSG_READY_NOTIFY = 11 +MSG_LSA_UPDATE_NOTIFY = 12 +MSG_LSA_DELETE_NOTIFY = 13 +MSG_NEW_IF = 14 +MSG_DEL_IF = 15 +MSG_ISM_CHANGE = 16 +MSG_NSM_CHANGE = 17 +MSG_REACHABLE_CHANGE = 18 + +amsg_info = { + MSG_REPLY: ("REPLY", "bxxx"), + MSG_READY_NOTIFY: ("READY_NOTIFY", ">BBxxI"), + MSG_LSA_UPDATE_NOTIFY: ("LSA_UPDATE_NOTIFY", ">IIBxxx" + FMT_LSA_HEADER[1:]), + MSG_LSA_DELETE_NOTIFY: ("LSA_DELETE_NOTIFY", ">IIBxxx" + FMT_LSA_HEADER[1:]), + MSG_NEW_IF: ("NEW_IF", ">II"), + MSG_DEL_IF: ("DEL_IF", ">I"), + MSG_ISM_CHANGE: ("ISM_CHANGE", ">IIBxxx"), + MSG_NSM_CHANGE: ("NSM_CHANGE", ">IIIBxxx"), + MSG_REACHABLE_CHANGE: ("REACHABLE_CHANGE", ">HH"), +} + +OSPF_API_OK = 0 +OSPF_API_NOSUCHINTERFACE = -1 +OSPF_API_NOSUCHAREA = -2 +OSPF_API_NOSUCHLSA = -3 +OSPF_API_ILLEGALLSATYPE = -4 +OSPF_API_OPAQUETYPEINUSE = -5 +OSPF_API_OPAQUETYPENOTREGISTERED = -6 +OSPF_API_NOTREADY = -7 +OSPF_API_NOMEMORY = -8 +OSPF_API_ERROR = -9 +OSPF_API_UNDEF = -10 + +msg_errname = { + OSPF_API_OK: "OSPF_API_OK", + OSPF_API_NOSUCHINTERFACE: "OSPF_API_NOSUCHINTERFACE", + OSPF_API_NOSUCHAREA: "OSPF_API_NOSUCHAREA", + OSPF_API_NOSUCHLSA: "OSPF_API_NOSUCHLSA", + OSPF_API_ILLEGALLSATYPE: "OSPF_API_ILLEGALLSATYPE", + OSPF_API_OPAQUETYPEINUSE: "OSPF_API_OPAQUETYPEINUSE", + OSPF_API_OPAQUETYPENOTREGISTERED: "OSPF_API_OPAQUETYPENOTREGISTERED", + OSPF_API_NOTREADY: "OSPF_API_NOTREADY", + OSPF_API_NOMEMORY: "OSPF_API_NOMEMORY", + OSPF_API_ERROR: "OSPF_API_ERROR", + OSPF_API_UNDEF: "OSPF_API_UNDEF", +} + +# msg_info = {**smsg_info, **amsg_info} +msg_info = {} +msg_info.update(smsg_info) +msg_info.update(amsg_info) +msg_name = {k: v[0] for k, v in msg_info.items()} +msg_fmt = {k: v[1] for k, v in msg_info.items()} +msg_size = {k: struct.calcsize(v) for k, v in msg_fmt.items()} + + +def api_msgname(mt): + return msg_name.get(mt, str(mt)) + + +def api_errname(ecode): + return msg_errname.get(ecode, str(ecode)) + + +# ------------------- +# API Semantic Errors +# ------------------- + + +class APIError(Exception): + pass + + +class MsgTypeError(Exception): + pass + + +class SeqNumError(Exception): + pass + + +# --------- +# LSA Types +# --------- + +LSA_TYPE_UNKNOWN = 0 +LSA_TYPE_ROUTER = 1 +LSA_TYPE_NETWORK = 2 +LSA_TYPE_SUMMARY = 3 +LSA_TYPE_ASBR_SUMMARY = 4 +LSA_TYPE_AS_EXTERNAL = 5 +LSA_TYPE_GROUP_MEMBER = 6 +LSA_TYPE_AS_NSSA = 7 +LSA_TYPE_EXTERNAL_ATTRIBUTES = 8 +LSA_TYPE_OPAQUE_LINK = 9 +LSA_TYPE_OPAQUE_AREA = 10 +LSA_TYPE_OPAQUE_AS = 11 + + +def lsa_typename(lsa_type): + names = { + LSA_TYPE_ROUTER: "LSA:ROUTER", + LSA_TYPE_NETWORK: "LSA:NETWORK", + LSA_TYPE_SUMMARY: "LSA:SUMMARY", + LSA_TYPE_ASBR_SUMMARY: "LSA:ASBR_SUMMARY", + LSA_TYPE_AS_EXTERNAL: "LSA:AS_EXTERNAL", + LSA_TYPE_GROUP_MEMBER: "LSA:GROUP_MEMBER", + LSA_TYPE_AS_NSSA: "LSA:AS_NSSA", + LSA_TYPE_EXTERNAL_ATTRIBUTES: "LSA:EXTERNAL_ATTRIBUTES", + LSA_TYPE_OPAQUE_LINK: "LSA:OPAQUE_LINK", + LSA_TYPE_OPAQUE_AREA: "LSA:OPAQUE_AREA", + LSA_TYPE_OPAQUE_AS: "LSA:OPAQUE_AS", + } + return names.get(lsa_type, str(lsa_type)) + + +# ------------------------------ +# Interface State Machine States +# ------------------------------ + +ISM_DEPENDUPON = 0 +ISM_DOWN = 1 +ISM_LOOPBACK = 2 +ISM_WAITING = 3 +ISM_POINTTOPOINT = 4 +ISM_DROTHER = 5 +ISM_BACKUP = 6 +ISM_DR = 7 + + +def ism_name(state): + names = { + ISM_DEPENDUPON: "ISM_DEPENDUPON", + ISM_DOWN: "ISM_DOWN", + ISM_LOOPBACK: "ISM_LOOPBACK", + ISM_WAITING: "ISM_WAITING", + ISM_POINTTOPOINT: "ISM_POINTTOPOINT", + ISM_DROTHER: "ISM_DROTHER", + ISM_BACKUP: "ISM_BACKUP", + ISM_DR: "ISM_DR", + } + return names.get(state, str(state)) + + +# ----------------------------- +# Neighbor State Machine States +# ----------------------------- + +NSM_DEPENDUPON = 0 +NSM_DELETED = 1 +NSM_DOWN = 2 +NSM_ATTEMPT = 3 +NSM_INIT = 4 +NSM_TWOWAY = 5 +NSM_EXSTART = 6 +NSM_EXCHANGE = 7 +NSM_LOADING = 8 +NSM_FULL = 9 + + +def nsm_name(state): + names = { + NSM_DEPENDUPON: "NSM_DEPENDUPON", + NSM_DELETED: "NSM_DELETED", + NSM_DOWN: "NSM_DOWN", + NSM_ATTEMPT: "NSM_ATTEMPT", + NSM_INIT: "NSM_INIT", + NSM_TWOWAY: "NSM_TWOWAY", + NSM_EXSTART: "NSM_EXSTART", + NSM_EXCHANGE: "NSM_EXCHANGE", + NSM_LOADING: "NSM_LOADING", + NSM_FULL: "NSM_FULL", + } + return names.get(state, str(state)) + + +# -------------- +# Client Classes +# -------------- + + +class OspfApiClient: + def __str__(self): + return "OspfApiClient({})".format(self.server) + + @staticmethod + def _get_bound_sockets(port): + s1 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + try: + s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # s1.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s1.bind(("", port)) + s2 = socket.socket(socket.AF_INET, socket.SOCK_STREAM, 0) + try: + s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + # s2.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + s2.bind(("", port + 1)) + return s1, s2 + except Exception: + s2.close() + raise + except Exception: + s1.close() + raise + + def __init__(self, server="localhost", handlers=None): + """A client connection to OSPF Daemon using the OSPF API + + The client object is not created in a connected state. To connect to the server + the `connect` method should be called. If an error is encountered when sending + messages to the server an exception will be raised and the connection will be + closed. When this happens `connect` may be called again to restore the + connection. + + Args: + server: hostname or IP address of server default is "localhost" + handlers: dict of message handlers, the key is the API message + type, the value is a function. The functions signature is: + `handler(msg_type, msg, msg_extra, *params)`, where `msg` is the + message data after the API header, `*params` will be the + unpacked message values, and msg_extra are any bytes beyond the + fixed parameters of the message. + Raises: + Will raise exceptions for failures with various `socket` modules + functions such as `socket.socket`, `socket.setsockopt`, `socket.bind`. + """ + self._seq = 0 + self._s = None + self._as = None + self._ls = None + self._ar = self._r = self._w = None + self.server = server + self.handlers = handlers if handlers is not None else dict() + self.write_lock = Lock() + + # try and get consecutive 2 ports + PORTSTART = 49152 + PORTEND = 65534 + for port in range(PORTSTART, PORTEND + 2, 2): + try: + logging.debug("%s: binding to ports %s, %s", self, port, port + 1) + self._s, self._ls = self._get_bound_sockets(port) + break + except OSError as error: + if error.errno != errno.EADDRINUSE or port == PORTEND: + logging.warning("%s: binding port %s error %s", self, port, error) + raise + logging.debug("%s: ports %s, %s in use.", self, port, port + 1) + else: + assert False, "Should not reach this code execution point" + + async def _connect_locked(self): + logging.debug("%s: connect to OSPF API", self) + + loop = asyncio.get_event_loop() + + self._ls.listen() + try: + logging.debug("%s: connecting sync socket to server", self) + await loop.sock_connect(self._s, (self.server, 2607)) + + logging.debug("%s: accepting connect from server", self) + self._as, _ = await loop.sock_accept(self._ls) + except Exception: + await self._close_locked() + raise + + logging.debug("%s: success", self) + self._r, self._w = await asyncio.open_connection(sock=self._s) + self._ar, _ = await asyncio.open_connection(sock=self._as) + self._seq = 1 + + async def connect(self): + async with self.write_lock: + await self._connect_locked() + + @property + def closed(self): + "True if the connection is closed." + return self._seq == 0 + + async def _close_locked(self): + logging.debug("%s: closing", self) + if self._s: + if self._w: + self._w.close() + await self._w.wait_closed() + self._w = None + else: + self._s.close() + self._s = None + self._r = None + assert self._w is None + if self._as: + self._as.close() + self._as = None + self._ar = None + if self._ls: + self._ls.close() + self._ls = None + self._seq = 0 + + async def close(self): + async with self.write_lock: + await self._close_locked() + + @staticmethod + async def _msg_read(r, expseq=-1): + """Read an OSPF API message from the socket `r` + + Args: + r: socket to read msg from + expseq: sequence number to expect or -1 for any. + Raises: + Will raise exceptions for failures with various `socket` modules, + Additionally may raise SeqNumError if unexpected seqnum is received. + """ + try: + mh = await r.readexactly(FMT_APIMSGHDR_SIZE) + v, mt, l, seq = struct.unpack(FMT_APIMSGHDR, mh) + if v != 1: + raise Exception("received unexpected OSPF API version {}".format(v)) + if expseq == -1: + logging.debug("_msg_read: got seq: 0x%x on async read", seq) + elif seq != expseq: + raise SeqNumError("rx {} != {}".format(seq, expseq)) + msg = await r.readexactly(l) if l else b"" + return mt, msg + except asyncio.IncompleteReadError: + raise EOFError + + async def msg_read(self): + """Read a message from the async notify channel. + + Raises: + May raise exceptions for failures with various `socket` modules. + """ + return await OspfApiClient._msg_read(self._ar, -1) + + async def msg_send(self, mt, mp): + """Send a message to OSPF API and wait for error code reply. + + Args: + mt: the messaage type + mp: the message payload + Returns: + error: an OSPF_API_XXX error code, 0 for OK. + Raises: + Raises SeqNumError if the synchronous reply is the wrong sequence number; + MsgTypeError if the synchronous reply is not MSG_REPLY. Also, + may raise exceptions for failures with various `socket` modules, + + The connection will be closed. + """ + logging.debug("SEND: %s: sending %s seq 0x%x", self, api_msgname(mt), self._seq) + mh = struct.pack(FMT_APIMSGHDR, 1, mt, len(mp), self._seq) + + seq = self._seq + self._seq = seq + 1 + + try: + async with self.write_lock: + self._w.write(mh + mp) + await self._w.drain() + mt, mp = await OspfApiClient._msg_read(self._r, seq) + + if mt != MSG_REPLY: + raise MsgTypeError( + "rx {} != {}".format(api_msgname(mt), api_msgname(MSG_REPLY)) + ) + + return struct.unpack(msg_fmt[MSG_REPLY], mp)[0] + except Exception: + # We've written data with a sequence number + await self.close() + raise + + async def msg_send_raises(self, mt, mp=b"\x00" * 4): + """Send a message to OSPF API and wait for error code reply. + + Args: + mt: the messaage type + mp: the message payload + Raises: + APIError if the server replies with an error. + + Also may raise exceptions for failures with various `socket` modules, + as well as MsgTypeError if the synchronous reply is incorrect. + The connection will be closed for these non-API error exceptions. + """ + ecode = await self.msg_send(mt, mp) + if ecode: + raise APIError("{} error {}".format(api_msgname(mt), api_errname(ecode))) + + async def handle_async_msg(self, mt, msg): + if mt not in msg_fmt: + logging.debug("RECV: %s: unknown async msg type %s", self, mt) + return + + fmt = msg_fmt[mt] + sz = msg_size[mt] + tup = struct.unpack(fmt, msg[:sz]) + extra = msg[sz:] + + if mt not in self.handlers: + logging.debug( + "RECV: %s: no handlers for msg type %s", self, api_msgname(mt) + ) + return + + logging.debug("RECV: %s: calling handler for %s", self, api_msgname(mt)) + await self.handlers[mt](mt, msg, extra, *tup) + + # + # Client to Server Messaging + # + @staticmethod + def lsa_type_mask(*lsa_types): + "Return a 16 bit mask for each LSA type passed." + if not lsa_types: + return 0xFFFF + mask = 0 + for t in lsa_types: + assert 0 < t < 16, "LSA type {} out of range [1, 15]".format(t) + mask |= 1 << t + return mask + + @staticmethod + def lsa_filter(origin, areas, lsa_types): + """Return an LSA filter. + + Return the filter message bytes based on `origin` the `areas` list and the LSAs + types in the `lsa_types` list. + """ + mask = OspfApiClient.lsa_type_mask(*lsa_types) + narea = len(areas) + fmt = FMT_LSA_FILTER + ("{}I".format(narea) if narea else "") + # lsa type mask, origin, number of areas, each area + return struct.pack(fmt, mask, origin, narea, *areas) + + async def req_lsdb_sync(self): + "Register for all LSA notifications and request an LSDB synchronoization." + logging.debug("SEND: %s: request LSDB events", self) + mp = OspfApiClient.lsa_filter(LSAF_ORIGIN_ANY, [], []) + await self.msg_send_raises(MSG_REGISTER_EVENT, mp) + + logging.debug("SEND: %s: request LSDB sync", self) + await self.msg_send_raises(MSG_SYNC_LSDB, mp) + + async def req_reachable_routers(self): + "Request a dump of all reachable routers." + logging.debug("SEND: %s: request reachable changes", self) + await self.msg_send_raises(MSG_SYNC_REACHABLE) + + async def req_ism_states(self): + "Request a dump of the current ISM states of all interfaces." + logging.debug("SEND: %s: request ISM changes", self) + await self.msg_send_raises(MSG_SYNC_ISM) + + async def req_nsm_states(self): + "Request a dump of the current NSM states of all neighbors." + logging.debug("SEND: %s: request NSM changes", self) + await self.msg_send_raises(MSG_SYNC_NSM) + + +class OspfOpaqueClient(OspfApiClient): + """A client connection to OSPF Daemon for manipulating Opaque LSA data. + + The client object is not created in a connected state. To connect to the server + the `connect` method should be called. If an error is encountered when sending + messages to the server an exception will be raised and the connection will be + closed. When this happens `connect` may be called again to restore the + connection. + + Args: + server: hostname or IP address of server default is "localhost" + + Raises: + Will raise exceptions for failures with various `socket` modules + functions such as `socket.socket`, `socket.setsockopt`, `socket.bind`. + """ + + def __init__(self, server="localhost"): + handlers = { + MSG_READY_NOTIFY: self._ready_msg, + MSG_LSA_UPDATE_NOTIFY: self._lsa_change_msg, + MSG_LSA_DELETE_NOTIFY: self._lsa_change_msg, + MSG_NEW_IF: self._if_msg, + MSG_DEL_IF: self._if_msg, + MSG_ISM_CHANGE: self._if_change_msg, + MSG_NSM_CHANGE: self._nbr_change_msg, + MSG_REACHABLE_CHANGE: self._reachable_msg, + } + super().__init__(server, handlers) + + self.ready_lock = Lock() + self.ready_cond = { + LSA_TYPE_OPAQUE_LINK: {}, + LSA_TYPE_OPAQUE_AREA: {}, + LSA_TYPE_OPAQUE_AS: {}, + } + self.lsid_seq_num = {} + + self.lsa_change_cb = None + self.opaque_change_cb = {} + + self.reachable_routers = set() + self.reachable_change_cb = None + + self.if_area = {} + self.ism_states = {} + self.ism_change_cb = None + + self.nsm_states = {} + self.nsm_change_cb = None + + async def _register_opaque_data(self, lsa_type, otype): + async with self.ready_lock: + cond = self.ready_cond[lsa_type].get(otype) + assert cond is None, "multiple registers for {} opaque-type {}".format( + lsa_typename(lsa_type), otype + ) + + logging.debug("register %s opaque-type %s", lsa_typename(lsa_type), otype) + + mt = MSG_REGISTER_OPAQUETYPE + mp = struct.pack(msg_fmt[mt], lsa_type, otype) + await self.msg_send_raises(mt, mp) + + async def _assure_opaque_ready(self, lsa_type, otype): + async with self.ready_lock: + if self.ready_cond[lsa_type].get(otype) is True: + return + + await self._register_opaque_data(lsa_type, otype) + await self.wait_opaque_ready(lsa_type, otype) + + async def _handle_msg_loop(self): + try: + logging.debug("entering async msg handling loop") + while True: + mt, msg = await self.msg_read() + if mt in amsg_info: + await self.handle_async_msg(mt, msg) + else: + mts = api_msgname(mt) + logging.warning( + "ignoring unexpected msg: %s len: %s", mts, len(msg) + ) + except EOFError: + logging.info("Got EOF from OSPF API server on async notify socket") + return 2 + + @staticmethod + def _opaque_args(lsa_type, otype, oid, mp): + lsid = (otype << 24) | oid + return 0, 0, lsa_type, lsid, 0, 0, 0, FMT_LSA_HEADER_SIZE + len(mp) + + @staticmethod + def _make_opaque_lsa(lsa_type, otype, oid, mp): + # /* Make a new LSA from parameters */ + lsa = struct.pack( + FMT_LSA_HEADER, *OspfOpaqueClient._opaque_args(lsa_type, otype, oid, mp) + ) + lsa += mp + return lsa + + async def _ready_msg(self, mt, msg, extra, lsa_type, otype, addr): + if lsa_type == LSA_TYPE_OPAQUE_LINK: + e = "ifaddr {}".format(ip(addr)) + elif lsa_type == LSA_TYPE_OPAQUE_AREA: + e = "area {}".format(ip(addr)) + else: + e = "" + logging.info( + "RECV: %s ready notify for %s opaque-type %s%s", + self, + lsa_typename(lsa_type), + otype, + e, + ) + + # Signal all waiting senders they can send now. + async with self.ready_lock: + cond = self.ready_cond[lsa_type].get(otype) + self.ready_cond[lsa_type][otype] = True + + if cond is True: + logging.warning( + "RECV: dup ready received for %s opaque-type %s", + lsa_typename(lsa_type), + otype, + ) + elif cond: + for evt in cond: + evt.set() + + async def _if_msg(self, mt, msg, extra, *args): + if mt == MSG_NEW_IF: + ifaddr, aid = args + else: + assert mt == MSG_DEL_IF + ifaddr, aid = args[0], 0 + logging.info( + "RECV: %s ifaddr %s areaid %s", api_msgname(mt), ip(ifaddr), ip(aid) + ) + + async def _if_change_msg(self, mt, msg, extra, ifaddr, aid, state): + ifaddr = ip(ifaddr) + aid = ip(aid) + + logging.info( + "RECV: %s ifaddr %s areaid %s state %s", + api_msgname(mt), + ifaddr, + aid, + ism_name(state), + ) + + self.if_area[ifaddr] = aid + self.ism_states[ifaddr] = state + + if self.ism_change_cb: + self.ism_change_cb(ifaddr, aid, state) + + async def _nbr_change_msg(self, mt, msg, extra, ifaddr, nbraddr, router_id, state): + ifaddr = ip(ifaddr) + nbraddr = ip(nbraddr) + router_id = ip(router_id) + + logging.info( + "RECV: %s ifaddr %s nbraddr %s router_id %s state %s", + api_msgname(mt), + ifaddr, + nbraddr, + router_id, + nsm_name(state), + ) + + if ifaddr not in self.nsm_states: + self.nsm_states[ifaddr] = {} + self.nsm_states[ifaddr][(nbraddr, router_id)] = state + + if self.nsm_change_cb: + self.nsm_change_cb(ifaddr, nbraddr, router_id, state) + + async def _lsa_change_msg(self, mt, msg, extra, ifaddr, aid, is_self, *ls_header): + ( + lsa_age, # ls_age, + _, # ls_options, + lsa_type, + ls_id, + _, # ls_adv_router, + ls_seq, + _, # ls_cksum, + ls_len, + ) = ls_header + + otype = (ls_id >> 24) & 0xFF + + if mt == MSG_LSA_UPDATE_NOTIFY: + ts = "update" + else: + assert mt == MSG_LSA_DELETE_NOTIFY + ts = "delete" + + logging.info( + "RECV: LSA %s msg for LSA %s in area %s seq 0x%x len %s age %s", + ts, + ip(ls_id), + ip(aid), + ls_seq, + ls_len, + lsa_age, + ) + idx = (lsa_type, otype) + + pre_lsa_size = msg_size[mt] - FMT_LSA_HEADER_SIZE + lsa = msg[pre_lsa_size:] + + if idx in self.opaque_change_cb: + self.opaque_change_cb[idx](mt, ifaddr, aid, ls_header, extra, lsa) + + if self.lsa_change_cb: + self.lsa_change_cb(mt, ifaddr, aid, ls_header, extra, lsa) + + async def _reachable_msg(self, mt, msg, extra, nadd, nremove): + router_ids = struct.unpack(">{}I".format(nadd + nremove), extra) + router_ids = [ip(x) for x in router_ids] + logging.info( + "RECV: %s added %s removed %s", + api_msgname(mt), + router_ids[:nadd], + router_ids[nadd:], + ) + self.reachable_routers |= set(router_ids[:nadd]) + self.reachable_routers -= set(router_ids[nadd:]) + logging.info("RECV: %s new set %s", api_msgname(mt), self.reachable_routers) + + if self.reachable_change_cb: + logging.info("RECV: %s calling callback", api_msgname(mt)) + await self.reachable_change_cb(router_ids[:nadd], router_ids[nadd:]) + + async def add_opaque_data(self, addr, lsa_type, otype, oid, data): + """Add an instance of opaque data. + + Add an instance of opaque data. This call will register for the given + LSA and opaque type if not already done. + + Args: + addr: depends on lsa_type, LINK => ifaddr, AREA => area ID, AS => ignored + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type + oid: (3 octets) ID of this opaque data + data: the opaque data + Raises: + See `msg_send_raises` + """ + + if lsa_type == LSA_TYPE_OPAQUE_LINK: + ifaddr, aid = int(addr), 0 + elif lsa_type == LSA_TYPE_OPAQUE_AREA: + ifaddr, aid = 0, int(addr) + else: + assert lsa_type == LSA_TYPE_OPAQUE_AS + ifaddr, aid = 0, 0 + + mt = MSG_ORIGINATE_REQUEST + msg = struct.pack( + msg_fmt[mt], + ifaddr, + aid, + *OspfOpaqueClient._opaque_args(lsa_type, otype, oid, data), + ) + msg += data + await self._assure_opaque_ready(lsa_type, otype) + await self.msg_send_raises(mt, msg) + + async def delete_opaque_data(self, addr, lsa_type, otype, oid): + """Delete an instance of opaque data. + + Delete an instance of opaque data. This call will register for the given + LSA and opaque type if not already done. + + Args: + addr: depends on lsa_type, LINK => ifaddr, AREA => area ID, AS => ignored + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type. Note: the type will be registered if the user + has not explicity done that yet with `register_opaque_data`. + oid: (3 octets) ID of this opaque data + Raises: + See `msg_send_raises` + """ + if (lsa_type, otype) in self.opaque_change_cb: + del self.opaque_change_cb[(lsa_type, otype)] + + mt = MSG_DELETE_REQUEST + await self._assure_opaque_ready(lsa_type, otype) + mp = struct.pack(msg_fmt[mt], int(addr), lsa_type, otype, oid) + await self.msg_send_raises(mt, mp) + + async def register_opaque_data(self, lsa_type, otype, callback=None): + """Register intent to advertise opaque data. + + The application should wait for the async notificaiton that the server is + ready to advertise the given opaque data type. The API currently only allows + a single "owner" of each unique (lsa_type,otype). To wait call `wait_opaque_ready` + + Args: + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type. Note: the type will be registered if the user + has not explicity done that yet with `register_opaque_data`. + callback: if given, callback will be called when changes are received for + LSA of the given (lsa_type, otype). The callbacks signature is: + + `callback(msg_type, ifaddr, area_id, lsa_header, data, lsa)` + + Args: + msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY + ifaddr: integer identifying an interface (by IP address) + area_id: integer identifying an area + lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH") + data: the opaque data that follows the LSA header + lsa: the octets of the full lsa + Raises: + See `msg_send_raises` + """ + if callback: + self.opaque_change_cb[(lsa_type, otype)] = callback + elif (lsa_type, otype) in self.opaque_change_cb: + logging.warning( + "OSPFCLIENT: register: removing callback for %s opaque-type %s", + lsa_typename(lsa_type), + otype, + ) + del self.opaque_change_cb[(lsa_type, otype)] + + await self._register_opaque_data(lsa_type, otype) + + async def wait_opaque_ready(self, lsa_type, otype): + async with self.ready_lock: + cond = self.ready_cond[lsa_type].get(otype) + if cond is True: + return + + logging.debug( + "waiting for ready %s opaque-type %s", lsa_typename(lsa_type), otype + ) + + if not cond: + cond = self.ready_cond[lsa_type][otype] = [] + + evt = Event() + cond.append(evt) + + await evt.wait() + logging.debug("READY for %s opaque-type %s", lsa_typename(lsa_type), otype) + + async def register_opaque_data_wait(self, lsa_type, otype, callback=None): + """Register intent to advertise opaque data and wait for ready. + + The API currently only allows a single "owner" of each unique (lsa_type,otype). + + Args: + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type. Note: the type will be registered if the user + has not explicity done that yet with `register_opaque_data`. + callback: if given, callback will be called when changes are received for + LSA of the given (lsa_type, otype). The callbacks signature is: + + `callback(msg_type, ifaddr, area_id, lsa_header, data, lsa)` + + Args: + msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY + ifaddr: integer identifying an interface (by IP address) + area_id: integer identifying an area + lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH") + data: the opaque data that follows the LSA header + lsa: the octets of the full lsa + Raises: + + See `msg_send_raises` + """ + if callback: + self.opaque_change_cb[(lsa_type, otype)] = callback + elif (lsa_type, otype) in self.opaque_change_cb: + logging.warning( + "OSPFCLIENT: register: removing callback for %s opaque-type %s", + lsa_typename(lsa_type), + otype, + ) + del self.opaque_change_cb[(lsa_type, otype)] + + return await self._assure_opaque_ready(lsa_type, otype) + + async def unregister_opaque_data(self, lsa_type, otype): + """Unregister intent to advertise opaque data. + + This will also cause the server to flush/delete all opaque data of + the given (lsa_type,otype). + + Args: + lsa_type: LSA_TYPE_OPAQUE_{LINK,AREA,AS} + otype: (octet) opaque type. Note: the type will be registered if the user + has not explicity done that yet with `register_opaque_data`. + Raises: + See `msg_send_raises` + """ + + if (lsa_type, otype) in self.opaque_change_cb: + del self.opaque_change_cb[(lsa_type, otype)] + + mt = MSG_UNREGISTER_OPAQUETYPE + mp = struct.pack(msg_fmt[mt], lsa_type, otype) + await self.msg_send_raises(mt, mp) + + async def monitor_lsa(self, callback=None): + """Monitor changes to LSAs. + + Args: + callback: if given, callback will be called when changes are received for + any LSA. The callback signature is: + + `callback(msg_type, ifaddr, area_id, lsa_header, extra, lsa)` + + Args: + msg_type: MSG_LSA_UPDATE_NOTIFY or MSG_LSA_DELETE_NOTIFY + ifaddr: integer identifying an interface (by IP address) + area_id: integer identifying an area + lsa_header: the LSA header as an unpacked tuple (fmt: ">HBBIILHH") + extra: the octets that follow the LSA header + lsa: the octets of the full lsa + """ + self.lsa_change_cb = callback + await self.req_lsdb_sync() + + async def monitor_reachable(self, callback=None): + """Monitor the set of reachable routers. + + The property `reachable_routers` contains the set() of reachable router IDs + as integers. This set is updated prior to calling the `callback` + + Args: + callback: callback will be called when the set of reachable + routers changes. The callback signature is: + + `callback(added, removed)` + + Args: + added: list of integer router IDs being added + removed: list of integer router IDs being removed + """ + self.reachable_change_cb = callback + await self.req_reachable_routers() + + async def monitor_ism(self, callback=None): + """Monitor the state of OSPF enabled interfaces. + + Args: + callback: callback will be called when an interface changes state. + The callback signature is: + + `callback(ifaddr, area_id, state)` + + Args: + ifaddr: integer identifying an interface (by IP address) + area_id: integer identifying an area + state: ISM_* + """ + self.ism_change_cb = callback + await self.req_ism_states() + + async def monitor_nsm(self, callback=None): + """Monitor the state of OSPF neighbors. + + Args: + callback: callback will be called when a neighbor changes state. + The callback signature is: + + `callback(ifaddr, nbr_addr, router_id, state)` + + Args: + ifaddr: integer identifying an interface (by IP address) + nbr_addr: integer identifying neighbor by IP address + router_id: integer identifying neighbor router ID + state: NSM_* + """ + self.nsm_change_cb = callback + await self.req_nsm_states() + + +# ================ +# CLI/Script Usage +# ================ + + +async def async_main(args): + c = OspfOpaqueClient(args.server) + await c.connect() + + try: + # Start handling async messages from server. + if sys.version_info[1] > 6: + asyncio.create_task(c._handle_msg_loop()) + else: + asyncio.get_event_loop().create_task(c._handle_msg_loop()) + + await c.req_lsdb_sync() + await c.req_reachable_routers() + await c.req_ism_states() + await c.req_nsm_states() + + if args.actions: + for action in args.actions: + _s = action.split(",") + what = _s.pop(False) + ltype = int(_s.pop(False)) + if ltype == 11: + addr = ip(0) + else: + aval = _s.pop(False) + try: + addr = ip(int(aval)) + except ValueError: + addr = ip(aval) + oargs = [addr, ltype, int(_s.pop(False)), int(_s.pop(False))] + assert len(_s) <= 1, "Bad format for action argument" + try: + b = bytes.fromhex(_s.pop(False)) + except IndexError: + b = b"" + logging.info("opaque data is %s octets", len(b)) + # Needs to be multiple of 4 in length + mod = len(b) % 4 + if mod: + b += b"\x00" * (4 - mod) + logging.info("opaque padding to %s octets", len(b)) + + if what.casefold() == "add": + await c.add_opaque_data(*oargs, b) + else: + assert what.casefold().startswith("del") + await c.delete_opaque_data(*oargs) + if args.exit: + return 0 + except Exception as error: + logging.error("async_main: unexpected error: %s", error, exc_info=True) + return 2 + + try: + logging.info("Sleeping forever") + while True: + await asyncio.sleep(120) + except EOFError: + logging.info("Got EOF from OSPF API server on async notify socket") + return 2 + + +def main(*args): + ap = argparse.ArgumentParser(args) + ap.add_argument("--exit", action="store_true", help="Exit after commands") + ap.add_argument("--server", default="localhost", help="OSPF API server") + ap.add_argument("-v", "--verbose", action="store_true", help="be verbose") + ap.add_argument( + "actions", nargs="*", help="(ADD|DEL),LSATYPE,[ADDR,],OTYPE,OID,[HEXDATA]" + ) + args = ap.parse_args() + + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig( + level=level, format="%(asctime)s %(levelname)s: CLIENT: %(name)s %(message)s" + ) + + logging.info("ospfclient: starting") + + status = 3 + try: + if sys.version_info[1] > 6: + # python >= 3.7 + status = asyncio.run(async_main(args)) + else: + loop = asyncio.get_event_loop() + try: + status = loop.run_until_complete(async_main(args)) + finally: + loop.close() + except KeyboardInterrupt: + logging.info("Exiting, received KeyboardInterrupt in main") + except Exception as error: + logging.info("Exiting, unexpected exception %s", error, exc_info=True) + else: + logging.info("ospfclient: clean exit") + + return status + + +if __name__ == "__main__": + exit_status = main() + sys.exit(exit_status) diff --git a/ospfclient/subdir.am b/ospfclient/subdir.am index 1f9547ab87..b8c82c0bcf 100644 --- a/ospfclient/subdir.am +++ b/ospfclient/subdir.am @@ -6,6 +6,10 @@ if OSPFCLIENT lib_LTLIBRARIES += ospfclient/libfrrospfapiclient.la noinst_PROGRAMS += ospfclient/ospfclient #man8 += $(MANBUILD)/frr-ospfclient.8 + +sbin_SCRIPTS += \ + ospfclient/ospfclient.py \ + # end endif ospfclient_libfrrospfapiclient_la_LDFLAGS = $(LIB_LDFLAGS) -version-info 0:0:0 @@ -41,3 +45,7 @@ endif ospfclient_ospfclient_SOURCES = \ ospfclient/ospfclient.c \ # end + +EXTRA_DIST += \ + ospfclient/ospfclient.py \ + # end diff --git a/redhat/frr.spec.in b/redhat/frr.spec.in index c479a7d4cf..91829122ee 100644 --- a/redhat/frr.spec.in +++ b/redhat/frr.spec.in @@ -466,6 +466,9 @@ install -d -m750 %{buildroot}%{rundir} # avoid `ERROR: ambiguous python shebang in` errors pathfix.py -pni "%{__python3} %{py3_shbang_opts}" %{buildroot}/usr/lib/frr/*.py %py_byte_compile %{__python3} %{buildroot}/usr/lib/frr/*.py +%else +# remove ospfclient.py (if present) as it requires > python36 +rm -f %{buildroot}%{_sbindir}/ospfclient.py %endif %pre @@ -719,11 +722,13 @@ fi %files contrib %doc tools - %files pythontools %{_sbindir}/generate_support_bundle.py %{_sbindir}/frr-reload.py %{_sbindir}/frr_babeltrace.py +%if %{with_ospfclient} && (0%{?rhel} > 7 || 0%{?fedora} > 29) +%{_sbindir}/ospfclient.py +%endif %if 0%{?rhel} > 7 || 0%{?fedora} > 29 %{_sbindir}/__pycache__/* %else @@ -774,6 +779,10 @@ sed -i 's/ -M rpki//' %{_sysconfdir}/frr/daemons %changelog +* Sun May 29 2022 Christian Hopps - %{version} +- ospfclient: +- Add OSPF API python client ospfclient.py + * Tue Mar 1 2022 Martin Winter - %{version} * Tue Mar 1 2022 Jafar Al-Gharaibeh - 8.2 From ad9c18f375f71aaa1b04db399755939d7f5cf74d Mon Sep 17 00:00:00 2001 From: Christian Hopps Date: Tue, 10 May 2022 12:13:04 -0400 Subject: [PATCH 11/11] tests: add opaque api test Signed-off-by: Christian Hopps --- tests/topotests/ospfapi/ctester.py | 137 +++++ tests/topotests/ospfapi/lib | 1 + tests/topotests/ospfapi/r1/ospfd.conf | 10 + tests/topotests/ospfapi/r1/zebra.conf | 4 + tests/topotests/ospfapi/r2/ospfd.conf | 15 + tests/topotests/ospfapi/r2/zebra.conf | 7 + tests/topotests/ospfapi/r3/ospfd.conf | 15 + tests/topotests/ospfapi/r3/zebra.conf | 7 + tests/topotests/ospfapi/r4/ospfd.conf | 10 + tests/topotests/ospfapi/r4/zebra.conf | 4 + .../topotests/ospfapi/test_ospf_clientapi.py | 470 ++++++++++++++++++ 11 files changed, 680 insertions(+) create mode 100755 tests/topotests/ospfapi/ctester.py create mode 120000 tests/topotests/ospfapi/lib create mode 100644 tests/topotests/ospfapi/r1/ospfd.conf create mode 100644 tests/topotests/ospfapi/r1/zebra.conf create mode 100644 tests/topotests/ospfapi/r2/ospfd.conf create mode 100644 tests/topotests/ospfapi/r2/zebra.conf create mode 100644 tests/topotests/ospfapi/r3/ospfd.conf create mode 100644 tests/topotests/ospfapi/r3/zebra.conf create mode 100644 tests/topotests/ospfapi/r4/ospfd.conf create mode 100644 tests/topotests/ospfapi/r4/zebra.conf create mode 100644 tests/topotests/ospfapi/test_ospf_clientapi.py diff --git a/tests/topotests/ospfapi/ctester.py b/tests/topotests/ospfapi/ctester.py new file mode 100755 index 0000000000..243fc0613f --- /dev/null +++ b/tests/topotests/ospfapi/ctester.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# January 17 2022, Christian Hopps +# +# Copyright 2022, LabN Consulting, L.L.C. +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +import argparse +import asyncio +import logging +import os +import sys + +CWD = os.path.dirname(os.path.realpath(__file__)) + +CLIENTDIR = os.path.abspath(os.path.join(CWD, "../../../ospfclient")) +if not os.path.exists(CLIENTDIR): + CLIENTDIR = os.path.join(CWD, "/usr/lib/frr") +assert os.path.exists( + os.path.join(CLIENTDIR, "ospfclient.py") +), "can't locate ospfclient.py" + +sys.path[0:0] = [CLIENTDIR] + +import ospfclient as api # pylint: disable=E0401 # noqa: E402 + + +async def do_wait(c, args): + cv = asyncio.Condition() + + async def cb(added, removed): + logging.debug("callback: added: %s removed: %s", added, removed) + sys.stdout.flush() + async with cv: + cv.notify_all() + + logging.debug("API using callback") + await c.monitor_reachable(callback=cb) + + for w in args.wait: + check = ",".join(sorted(list(w.split(",")))) + logging.info("Waiting for %s", check) + + while True: + async with cv: + got = ",".join(sorted([str(x) for x in c.reachable_routers])) + if check == got: + break + logging.debug("expected '%s' != '%s'\nwaiting on notify", check, got) + await cv.wait() + + logging.info("SUCCESS: %s", check) + print("SUCCESS: {}".format(check)) + sys.stdout.flush() + + +async def async_main(args): + c = api.OspfOpaqueClient(args.server) + await c.connect() + if sys.version_info[1] > 6: + asyncio.create_task(c._handle_msg_loop()) # pylint: disable=W0212 + else: + asyncio.get_event_loop().create_task( + c._handle_msg_loop() # pylint: disable=W0212 + ) + + if args.wait: + await do_wait(c, args) + return 0 + + +def main(*args): + ap = argparse.ArgumentParser(args) + ap.add_argument("--server", default="localhost", help="OSPF API server") + ap.add_argument( + "--wait", action="append", help="wait for comma-sep set of reachable routers" + ) + ap.add_argument("-v", "--verbose", action="store_true", help="be verbose") + args = ap.parse_args() + + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig( + level=level, format="%(asctime)s %(levelname)s: TESTER: %(name)s: %(message)s" + ) + + # We need to flush this output to stdout right away + h = logging.StreamHandler(sys.stdout) + h.flush = sys.stdout.flush + f = logging.Formatter("%(asctime)s %(name)s: %(levelname)s: %(message)s") + h.setFormatter(f) + logger = logging.getLogger("ospfclient") + logger.addHandler(h) + logger.propagate = False + + logging.info("ctester: starting") + sys.stdout.flush() + + status = 3 + try: + if sys.version_info[1] > 6: + status = asyncio.run(async_main(args)) + else: + loop = asyncio.get_event_loop() + try: + status = loop.run_until_complete(async_main(args)) + finally: + loop.close() + except KeyboardInterrupt: + logging.info("Exiting, received KeyboardInterrupt in main") + except Exception as error: + logging.info("Exiting, unexpected exception %s", error, exc_info=True) + else: + logging.info("api: clean exit") + + return status + + +if __name__ == "__main__": + exit_status = main() + sys.exit(exit_status) diff --git a/tests/topotests/ospfapi/lib b/tests/topotests/ospfapi/lib new file mode 120000 index 0000000000..dc598c56dc --- /dev/null +++ b/tests/topotests/ospfapi/lib @@ -0,0 +1 @@ +../lib \ No newline at end of file diff --git a/tests/topotests/ospfapi/r1/ospfd.conf b/tests/topotests/ospfapi/r1/ospfd.conf new file mode 100644 index 0000000000..8d13556847 --- /dev/null +++ b/tests/topotests/ospfapi/r1/ospfd.conf @@ -0,0 +1,10 @@ +! +interface r1-eth0 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +router ospf + ospf router-id 192.168.0.1 + capability opaque +! diff --git a/tests/topotests/ospfapi/r1/zebra.conf b/tests/topotests/ospfapi/r1/zebra.conf new file mode 100644 index 0000000000..aae87408c3 --- /dev/null +++ b/tests/topotests/ospfapi/r1/zebra.conf @@ -0,0 +1,4 @@ +! +interface r1-eth0 + ip address 10.0.1.1/24 +! diff --git a/tests/topotests/ospfapi/r2/ospfd.conf b/tests/topotests/ospfapi/r2/ospfd.conf new file mode 100644 index 0000000000..be0712742b --- /dev/null +++ b/tests/topotests/ospfapi/r2/ospfd.conf @@ -0,0 +1,15 @@ +! +interface r2-eth0 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +interface r2-eth1 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +router ospf + ospf router-id 192.168.0.2 + capability opaque +! diff --git a/tests/topotests/ospfapi/r2/zebra.conf b/tests/topotests/ospfapi/r2/zebra.conf new file mode 100644 index 0000000000..e66d52da49 --- /dev/null +++ b/tests/topotests/ospfapi/r2/zebra.conf @@ -0,0 +1,7 @@ +! +interface r2-eth0 + ip address 10.0.1.2/24 +! +interface r2-eth1 + ip address 10.0.2.2/24 +! diff --git a/tests/topotests/ospfapi/r3/ospfd.conf b/tests/topotests/ospfapi/r3/ospfd.conf new file mode 100644 index 0000000000..77cd86a975 --- /dev/null +++ b/tests/topotests/ospfapi/r3/ospfd.conf @@ -0,0 +1,15 @@ +! +interface r3-eth0 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +interface r3-eth1 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +router ospf + ospf router-id 192.168.0.3 + capability opaque +! diff --git a/tests/topotests/ospfapi/r3/zebra.conf b/tests/topotests/ospfapi/r3/zebra.conf new file mode 100644 index 0000000000..072c297926 --- /dev/null +++ b/tests/topotests/ospfapi/r3/zebra.conf @@ -0,0 +1,7 @@ +! +interface r3-eth0 + ip address 10.0.2.3/24 +! +interface r3-eth1 + ip address 10.0.3.3/24 +! diff --git a/tests/topotests/ospfapi/r4/ospfd.conf b/tests/topotests/ospfapi/r4/ospfd.conf new file mode 100644 index 0000000000..32e55d546b --- /dev/null +++ b/tests/topotests/ospfapi/r4/ospfd.conf @@ -0,0 +1,10 @@ +! +interface r4-eth0 + ip ospf hello-interval 2 + ip ospf dead-interval 10 + ip ospf area 1.2.3.4 +! +router ospf + ospf router-id 192.168.0.4 + capability opaque +! diff --git a/tests/topotests/ospfapi/r4/zebra.conf b/tests/topotests/ospfapi/r4/zebra.conf new file mode 100644 index 0000000000..702219720d --- /dev/null +++ b/tests/topotests/ospfapi/r4/zebra.conf @@ -0,0 +1,4 @@ +! +interface r4-eth0 + ip address 10.0.3.4/24 +! diff --git a/tests/topotests/ospfapi/test_ospf_clientapi.py b/tests/topotests/ospfapi/test_ospf_clientapi.py new file mode 100644 index 0000000000..dca91412dc --- /dev/null +++ b/tests/topotests/ospfapi/test_ospf_clientapi.py @@ -0,0 +1,470 @@ +#!/usr/bin/env python +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# Copyright (c) 2021, LabN Consulting, L.L.C. +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; see the file COPYING; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# + +""" +test_ospf_clientapi.py: Test the OSPF client API. +""" + +import logging +import os +import re +import signal +import subprocess +import sys +import time +from datetime import datetime, timedelta + +import pytest + +from lib.common_config import retry, run_frr_cmd, step +from lib.micronet import comm_error +from lib.topogen import Topogen, TopoRouter +from lib.topotest import interface_set_status, json_cmp + +pytestmark = [pytest.mark.ospfd] + +CWD = os.path.dirname(os.path.realpath(__file__)) +TESTDIR = os.path.abspath(CWD) + +CLIENTDIR = os.path.abspath(os.path.join(CWD, "../../../ospfclient")) +if not os.path.exists(CLIENTDIR): + CLIENTDIR = os.path.join(CWD, "/usr/lib/frr") + +assert os.path.exists( + os.path.join(CLIENTDIR, "ospfclient.py") +), "can't locate ospfclient.py" + + +# ---------- +# Test Setup +# ---------- + + +@pytest.fixture(scope="function", name="tgen") +def _tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + nrouters = request.param + topodef = {f"sw{i}": (f"r{i}", f"r{i+1}") for i in range(1, nrouters)} + + tgen = Topogen(topodef, request.module.__name__) + tgen.start_topology() + + router_list = tgen.routers() + for _, router in router_list.items(): + router.load_config(TopoRouter.RD_ZEBRA, "zebra.conf") + router.load_config(TopoRouter.RD_OSPF, "ospfd.conf") + router.net.daemons_options["ospfd"] = "--apiserver" + + tgen.start_router() + + yield tgen + + tgen.stop_topology() + + +# Fixture that executes before each test +@pytest.fixture(autouse=True) +def skip_on_failure(tgen): + if tgen.routers_have_failure(): + pytest.skip("skipped because of previous test failure") + + +# ------------ +# Test Utility +# ------------ + + +@retry(retry_timeout=45) +def verify_ospf_database(tgen, dut, input_dict, cmd="show ip ospf database json"): + del tgen + show_ospf_json = run_frr_cmd(dut, cmd, isjson=True) + if not bool(show_ospf_json): + return "ospf is not running" + result = json_cmp(show_ospf_json, input_dict) + return str(result) if result else None + + +def myreadline(f): + buf = b"" + while True: + # logging.info("READING 1 CHAR") + c = f.read(1) + if not c: + return buf if buf else None + buf += c + # logging.info("READ CHAR: '%s'", c) + if c == b"\n": + return buf + + +def _wait_output(p, regex, timeout=120): + retry_until = datetime.now() + timedelta(seconds=timeout) + while datetime.now() < retry_until: + # line = p.stdout.readline() + line = myreadline(p.stdout) + if not line: + assert None, "Timeout waiting for '{}'".format(regex) + line = line.decode("utf-8") + line = line.rstrip() + if line: + logging.debug("GOT LINE: '%s'", line) + m = re.search(regex, line) + if m: + return m + assert None, "Failed to get output withint {}s".format(timeout) + + +# ----- +# Tests +# ----- + + +def _test_reachability(tgen, testbin): + waitlist = [ + "192.168.0.1,192.168.0.2,192.168.0.4", + "192.168.0.2,192.168.0.4", + "192.168.0.1,192.168.0.2,192.168.0.4", + ] + r2 = tgen.gears["r2"] + r3 = tgen.gears["r3"] + + wait_args = [f"--wait={x}" for x in waitlist] + + p = None + try: + step("reachable: check for initial reachability") + p = r3.popen( + ["/usr/bin/timeout", "120", testbin, "-v", *wait_args], + encoding=None, # don't buffer + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + _wait_output(p, "SUCCESS: {}".format(waitlist[0])) + + step("reachable: check for modified reachability") + interface_set_status(r2, "r2-eth0", False) + _wait_output(p, "SUCCESS: {}".format(waitlist[1])) + + step("reachable: check for restored reachability") + interface_set_status(r2, "r2-eth0", True) + _wait_output(p, "SUCCESS: {}".format(waitlist[2])) + except Exception as error: + logging.error("ERROR: %s", error) + raise + finally: + if p: + p.terminate() + p.wait() + + +@pytest.mark.parametrize("tgen", [4], indirect=True) +def test_ospf_reachability(tgen): + testbin = os.path.join(TESTDIR, "ctester.py") + rc, o, e = tgen.gears["r2"].net.cmd_status([testbin, "--help"]) + logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", testbin, rc, o, e) + _test_reachability(tgen, testbin) + + +def _test_add_data(tgen, apibin): + "Test adding opaque data to domain" + + r1 = tgen.gears["r1"] + + step("add opaque: add opaque link local") + + p = None + try: + p = r1.popen([apibin, "-v", "add,9,10.0.1.1,230,2,00000202"]) + input_dict = { + "routerId": "192.168.0.1", + "areas": { + "1.2.3.4": { + "linkLocalOpaqueLsa": [ + { + "lsId": "230.0.0.2", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + } + ], + } + }, + } + # Wait for it to show up + assert verify_ospf_database(tgen, r1, input_dict) is None + + input_dict = { + "linkLocalOpaqueLsa": { + "areas": { + "1.2.3.4": [ + { + "linkStateId": "230.0.0.2", + "advertisingRouter": "192.168.0.1", + "lsaSeqNumber": "80000001", + "opaqueData": "00000202", + }, + ], + } + }, + } + # verify content + json_cmd = "show ip ospf da opaque-link json" + assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None + + step("reset client, add opaque area, verify link local flushing") + + p.send_signal(signal.SIGINT) + time.sleep(2) + p.wait() + p = None + p = r1.popen([apibin, "-v", "add,10,1.2.3.4,231,1,00010101"]) + input_dict = { + "routerId": "192.168.0.1", + "areas": { + "1.2.3.4": { + "linkLocalOpaqueLsa": [ + { + "lsId": "230.0.0.2", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + "lsaAge": 3600, + } + ], + "areaLocalOpaqueLsa": [ + { + "lsId": "231.0.0.1", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + }, + ], + } + }, + } + # Wait for it to show up + assert verify_ospf_database(tgen, r1, input_dict) is None + + input_dict = { + "areaLocalOpaqueLsa": { + "areas": { + "1.2.3.4": [ + { + "linkStateId": "231.0.0.1", + "advertisingRouter": "192.168.0.1", + "lsaSeqNumber": "80000001", + "opaqueData": "00010101", + }, + ], + } + }, + } + # verify content + json_cmd = "show ip ospf da opaque-area json" + assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None + + step("reset client, add opaque AS, verify area flushing") + + p.send_signal(signal.SIGINT) + time.sleep(2) + p.wait() + p = None + + p = r1.popen([apibin, "-v", "add,11,232,3,deadbeaf01234567"]) + input_dict = { + "routerId": "192.168.0.1", + "areas": { + "1.2.3.4": { + "areaLocalOpaqueLsa": [ + { + "lsId": "231.0.0.1", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + "lsaAge": 3600, + }, + ], + } + }, + "asExternalOpaqueLsa": [ + { + "lsId": "232.0.0.3", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + }, + ], + } + # Wait for it to show up + assert verify_ospf_database(tgen, r1, input_dict) is None + + input_dict = { + "asExternalOpaqueLsa": [ + { + "linkStateId": "232.0.0.3", + "advertisingRouter": "192.168.0.1", + "lsaSeqNumber": "80000001", + "opaqueData": "deadbeaf01234567", + }, + ] + } + # verify content + json_cmd = "show ip ospf da opaque-as json" + assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None + + step("stop client, verify AS flushing") + + p.send_signal(signal.SIGINT) + time.sleep(2) + p.wait() + p = None + + input_dict = { + "routerId": "192.168.0.1", + "asExternalOpaqueLsa": [ + { + "lsId": "232.0.0.3", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000001", + "lsaAge": 3600, + }, + ], + } + # Wait for it to be flushed + assert verify_ospf_database(tgen, r1, input_dict) is None + + step("start client adding opaque domain, verify new sequence number and data") + + # Originate it again + p = r1.popen([apibin, "-v", "add,11,232,3,ebadf00d"]) + input_dict = { + "routerId": "192.168.0.1", + "asExternalOpaqueLsa": [ + { + "lsId": "232.0.0.3", + "advertisedRouter": "192.168.0.1", + "sequenceNumber": "80000002", + }, + ], + } + assert verify_ospf_database(tgen, r1, input_dict) is None + + input_dict = { + "asExternalOpaqueLsa": [ + { + "linkStateId": "232.0.0.3", + "advertisingRouter": "192.168.0.1", + "lsaSeqNumber": "80000002", + "opaqueData": "ebadf00d", + }, + ] + } + # verify content + json_cmd = "show ip ospf da opaque-as json" + assert verify_ospf_database(tgen, r1, input_dict, json_cmd) is None + + p.send_signal(signal.SIGINT) + time.sleep(2) + p.wait() + p = None + + except Exception: + if p: + p.terminate() + if p.wait(): + comm_error(p) + p = None + raise + finally: + if p: + p.terminate() + p.wait() + + +@pytest.mark.parametrize("tgen", [2], indirect=True) +def test_ospf_opaque_add_data3(tgen): + apibin = os.path.join(CLIENTDIR, "ospfclient.py") + rc, o, e = tgen.gears["r2"].net.cmd_status([apibin, "--help"]) + logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", apibin, rc, o, e) + _test_add_data(tgen, apibin) + + +def _test_opaque_add_del(tgen, apibin): + "Test adding opaque data to domain" + + r1 = tgen.gears["r1"] + r2 = tgen.gears["r2"] + + p = None + pread = None + try: + step("reachable: check for add notification") + pread = r2.popen( + ["/usr/bin/timeout", "120", apibin, "-v"], + encoding=None, # don't buffer + stdin=subprocess.DEVNULL, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + p = r1.popen([apibin, "-v", "add,11,232,3,ebadf00d"]) + + # Wait for add notification + # RECV: LSA update msg for LSA 232.0.0.3 in area 0.0.0.0 seq 0x80000001 len 24 age 9 + + ls_id = "232.0.0.3" + waitfor = "RECV:.*update msg.*LSA {}.*age ([0-9]+)".format(ls_id) + _ = _wait_output(pread, waitfor) + + p.terminate() + if p.wait(): + comm_error(p) + + # step("reachable: check for flush/age out") + # # Wait for max age notification + # waitfor = "RECV:.*update msg.*LSA {}.*age 3600".format(ls_id) + # _wait_output(pread, waitfor) + + step("reachable: check for delete") + # Wait for delete notification + waitfor = "RECV:.*delete msg.*LSA {}.*".format(ls_id) + _wait_output(pread, waitfor) + except Exception: + if p: + p.terminate() + if p.wait(): + comm_error(p) + p = None + raise + finally: + if pread: + pread.terminate() + pread.wait() + if p: + p.terminate() + p.wait() + + +@pytest.mark.parametrize("tgen", [2], indirect=True) +def test_ospf_opaque_delete_data3(tgen): + apibin = os.path.join(CLIENTDIR, "ospfclient.py") + rc, o, e = tgen.gears["r2"].net.cmd_status([apibin, "--help"]) + logging.info("%s --help: rc: %s stdout: '%s' stderr: '%s'", apibin, rc, o, e) + _test_opaque_add_del(tgen, apibin) + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args))