diff options
| -rw-r--r-- | doc/developer/topotests.rst | 3 | ||||
| -rw-r--r-- | lib/northbound_grpc.cpp | 27 | ||||
| -rw-r--r-- | pimd/pim6_main.c | 3 | ||||
| -rw-r--r-- | pimd/pim_igmpv2.c | 21 | ||||
| -rw-r--r-- | pimd/pim_igmpv3.c | 93 | ||||
| -rw-r--r-- | pimd/pim_join.c | 49 | ||||
| -rw-r--r-- | pimd/pim_nht.c | 10 | ||||
| -rw-r--r-- | pimd/pim_pim.c | 18 | ||||
| -rw-r--r-- | pimd/pim_tlv.c | 32 | ||||
| -rw-r--r-- | tests/.gitignore | 2 | ||||
| -rw-r--r-- | tests/lib/test_grpc.py | 10 | ||||
| -rwxr-xr-x | tests/topotests/analyze.py | 7 | ||||
| l--------- | tests/topotests/grpc_basic/lib | 1 | ||||
| -rw-r--r-- | tests/topotests/grpc_basic/r1/zebra.conf | 8 | ||||
| -rw-r--r-- | tests/topotests/grpc_basic/r2/zebra.conf | 8 | ||||
| -rw-r--r-- | tests/topotests/grpc_basic/test_basic_grpc.py | 179 | ||||
| -rwxr-xr-x | tests/topotests/lib/grpc-query.py | 155 |
17 files changed, 519 insertions, 107 deletions
diff --git a/doc/developer/topotests.rst b/doc/developer/topotests.rst index b41181f4e9..6c1d9148d1 100644 --- a/doc/developer/topotests.rst +++ b/doc/developer/topotests.rst @@ -35,6 +35,9 @@ Installing Topotest Requirements python2 -m pip install 'exabgp<4.0.0' useradd -d /var/run/exabgp/ -s /bin/false exabgp + # To enable the gRPC topotest install: + python3 -m pip install grpcio grpcio-tools + Enable Coredumps """""""""""""""" diff --git a/lib/northbound_grpc.cpp b/lib/northbound_grpc.cpp index e2a6290035..0a458b6262 100644 --- a/lib/northbound_grpc.cpp +++ b/lib/northbound_grpc.cpp @@ -78,7 +78,7 @@ class Candidates { // Delete candidates. for (auto it = _cdb.begin(); it != _cdb.end(); it++) - delete_candidate(&it->second); + delete_candidate(it->first); } struct candidate *create_candidate(void) @@ -94,8 +94,14 @@ class Candidates return c; } - void delete_candidate(struct candidate *c) + bool contains(uint64_t candidate_id) { + return _cdb.count(candidate_id) > 0; + } + + void delete_candidate(uint64_t candidate_id) + { + struct candidate *c = &_cdb[candidate_id]; char errmsg[BUFSIZ] = {0}; nb_config_free(c->config); @@ -105,14 +111,14 @@ class Candidates _cdb.erase(c->id); } - struct candidate *get_candidate(uint32_t id) + struct candidate *get_candidate(uint64_t id) { return _cdb.count(id) == 0 ? NULL : &_cdb[id]; } private: uint64_t _next_id = 0; - std::map<uint32_t, struct candidate> _cdb; + std::map<uint64_t, struct candidate> _cdb; }; class RpcStateBase @@ -183,6 +189,9 @@ template <typename Q, typename S> class NewRpcState : RpcStateBase pthread_cond_wait(&this->cond, &this->cmux); pthread_mutex_unlock(&this->cmux); + if (enter_state == FINISH) + assert(this->state == DELETED); + if (this->state == DELETED) { grpc_debug("%s RPC: -> [DELETED]", name); delete this; @@ -617,15 +626,14 @@ void HandleUnaryDeleteCandidate(NewRpcState<frr::DeleteCandidateRequest, grpc_debug("%s(candidate_id: %u)", __func__, candidate_id); - struct candidate *candidate = tag->cdb->get_candidate(candidate_id); - if (!candidate) { + if (!tag->cdb->contains(candidate_id)) { tag->responder.Finish( tag->response, grpc::Status(grpc::StatusCode::NOT_FOUND, "candidate configuration not found"), tag); } else { - tag->cdb->delete_candidate(candidate); + tag->cdb->delete_candidate(candidate_id); tag->responder.Finish(tag->response, grpc::Status::OK, tag); } tag->state = FINISH; @@ -1400,6 +1408,11 @@ static int frr_grpc_finish(void) grpc_debug("%s: joining and destroy grpc thread", __func__); pthread_join(fpt->thread, NULL); frr_pthread_destroy(fpt); + + // Fix protobuf 'memory leaks' during shutdown. + // https://groups.google.com/g/protobuf/c/4y_EmQiCGgs + google::protobuf::ShutdownProtobufLibrary(); + return 0; } diff --git a/pimd/pim6_main.c b/pimd/pim6_main.c index 8c7fca174c..ed53924616 100644 --- a/pimd/pim6_main.c +++ b/pimd/pim6_main.c @@ -178,8 +178,7 @@ int main(int argc, char **argv, char **envp) pim_route_map_init(); #endif - /* pim_init(); */ - pim_cmd_init(); + pim_init(); /* * Initialize zclient "update" and "lookup" sockets */ diff --git a/pimd/pim_igmpv2.c b/pimd/pim_igmpv2.c index 09a82069a2..34cda25963 100644 --- a/pimd/pim_igmpv2.c +++ b/pimd/pim_igmpv2.c @@ -24,6 +24,7 @@ #include "pim_igmp.h" #include "pim_igmpv2.h" #include "pim_igmpv3.h" +#include "pim_ssm.h" #include "pim_str.h" #include "pim_time.h" #include "pim_util.h" @@ -107,10 +108,13 @@ int igmp_v2_recv_report(struct gm_sock *igmp, struct in_addr from, { struct interface *ifp = igmp->interface; struct in_addr group_addr; + struct pim_interface *pim_ifp; char group_str[INET_ADDRSTRLEN]; on_trace(__func__, igmp->interface, from); + pim_ifp = ifp->info; + if (igmp->mtrace_only) return 0; @@ -142,6 +146,23 @@ int igmp_v2_recv_report(struct gm_sock *igmp, struct in_addr from, } /* + * RFC 4604 + * section 2.2.1 + * EXCLUDE mode does not apply to SSM addresses, and an SSM-aware router + * will ignore MODE_IS_EXCLUDE and CHANGE_TO_EXCLUDE_MODE requests in + * the SSM range. + */ + if (pim_is_grp_ssm(pim_ifp->pim, group_addr)) { + if (PIM_DEBUG_IGMP_PACKETS) { + zlog_debug( + "Ignoring IGMPv2 group record %pI4 from %s on %s exclude mode in SSM range", + &group_addr.s_addr, from_str, ifp->name); + } + return -1; + } + + + /* * RFC 3376 * 7.3.2. In the Presence of Older Version Group Members * diff --git a/pimd/pim_igmpv3.c b/pimd/pim_igmpv3.c index 027a79da98..b6114f9ead 100644 --- a/pimd/pim_igmpv3.c +++ b/pimd/pim_igmpv3.c @@ -32,6 +32,7 @@ #include "pim_time.h" #include "pim_zebra.h" #include "pim_oil.h" +#include "pim_ssm.h" static void group_retransmit_timer_on(struct gm_group *group); static long igmp_group_timer_remain_msec(struct gm_group *group); @@ -1820,6 +1821,64 @@ void igmp_v3_recv_query(struct gm_sock *igmp, const char *from_str, } /* s_flag is clear: timer updates */ } +static bool igmp_pkt_grp_addr_ok(struct interface *ifp, const char *from_str, + struct in_addr grp, int rec_type) +{ + struct pim_interface *pim_ifp; + struct in_addr grp_addr; + + pim_ifp = ifp->info; + + /* determine filtering status for group */ + if (pim_is_group_filtered(pim_ifp, &grp)) { + if (PIM_DEBUG_IGMP_PACKETS) { + zlog_debug( + "Filtering IGMPv3 group record %pI4 from %s on %s per prefix-list %s", + &grp.s_addr, from_str, ifp->name, + pim_ifp->boundary_oil_plist); + } + return false; + } + + /* + * If we receive a igmp report with the group in 224.0.0.0/24 + * then we should ignore it + */ + + grp_addr.s_addr = ntohl(grp.s_addr); + + if (pim_is_group_224_0_0_0_24(grp_addr)) { + if (PIM_DEBUG_IGMP_PACKETS) { + zlog_debug( + "Ignoring IGMPv3 group record %pI4 from %s on %s group range falls in 224.0.0.0/24", + &grp.s_addr, from_str, ifp->name); + } + return false; + } + + /* + * RFC 4604 + * section 2.2.1 + * EXCLUDE mode does not apply to SSM addresses, and an SSM-aware router + * will ignore MODE_IS_EXCLUDE and CHANGE_TO_EXCLUDE_MODE requests in + * the SSM range. + */ + if (pim_is_grp_ssm(pim_ifp->pim, grp)) { + switch (rec_type) { + case IGMP_GRP_REC_TYPE_MODE_IS_EXCLUDE: + case IGMP_GRP_REC_TYPE_CHANGE_TO_EXCLUDE_MODE: + if (PIM_DEBUG_IGMP_PACKETS) { + zlog_debug( + "Ignoring IGMPv3 group record %pI4 from %s on %s exclude mode in SSM range", + &grp.s_addr, from_str, ifp->name); + } + return false; + } + } + + return true; +} + int igmp_v3_recv_report(struct gm_sock *igmp, struct in_addr from, const char *from_str, char *igmp_msg, int igmp_msg_len) { @@ -1828,14 +1887,10 @@ int igmp_v3_recv_report(struct gm_sock *igmp, struct in_addr from, uint8_t *report_pastend = (uint8_t *)igmp_msg + igmp_msg_len; struct interface *ifp = igmp->interface; int i; - int local_ncb = 0; - struct pim_interface *pim_ifp; if (igmp->mtrace_only) return 0; - pim_ifp = igmp->interface->info; - if (igmp_msg_len < IGMP_V3_MSG_MIN_SIZE) { zlog_warn( "Recv IGMP report v3 from %s on %s: size=%d shorter than minimum=%d", @@ -1880,9 +1935,6 @@ int igmp_v3_recv_report(struct gm_sock *igmp, struct in_addr from, int rec_auxdatalen; int rec_num_sources; int j; - struct prefix lncb; - struct prefix g; - bool filtered = false; if ((group_record + IGMP_V3_GROUP_RECORD_MIN_SIZE) > report_pastend) { @@ -1940,31 +1992,7 @@ int igmp_v3_recv_report(struct gm_sock *igmp, struct in_addr from, } /* for (sources) */ - lncb.family = AF_INET; - lncb.u.prefix4.s_addr = 0x000000E0; - lncb.prefixlen = 24; - - g.family = AF_INET; - g.u.prefix4 = rec_group; - g.prefixlen = IPV4_MAX_BITLEN; - - /* determine filtering status for group */ - filtered = pim_is_group_filtered(ifp->info, &rec_group); - - if (PIM_DEBUG_IGMP_PACKETS && filtered) - zlog_debug( - "Filtering IGMPv3 group record %pI4 from %s on %s per prefix-list %s", - &rec_group, from_str, ifp->name, - pim_ifp->boundary_oil_plist); - - /* - * If we receive a igmp report with the group in 224.0.0.0/24 - * then we should ignore it - */ - if (prefix_match(&lncb, &g)) - local_ncb = 1; - - if (!local_ncb && !filtered) + if (igmp_pkt_grp_addr_ok(ifp, from_str, rec_group, rec_type)) switch (rec_type) { case IGMP_GRP_REC_TYPE_MODE_IS_INCLUDE: igmpv3_report_isin(igmp, from, rec_group, @@ -2004,7 +2032,6 @@ int igmp_v3_recv_report(struct gm_sock *igmp, struct in_addr from, group_record += 8 + (rec_num_sources << 2) + (rec_auxdatalen << 2); - local_ncb = 0; } /* for (group records) */ diff --git a/pimd/pim_join.c b/pimd/pim_join.c index 929beea26b..2c11d5d13f 100644 --- a/pimd/pim_join.c +++ b/pimd/pim_join.c @@ -43,14 +43,10 @@ #include "pim_util.h" #include "pim_ssm.h" -static void on_trace(const char *label, struct interface *ifp, - struct in_addr src) +static void on_trace(const char *label, struct interface *ifp, pim_addr src) { - if (PIM_DEBUG_PIM_TRACE) { - char src_str[INET_ADDRSTRLEN]; - pim_inet4_dump("<src?>", src, src_str, sizeof(src_str)); - zlog_debug("%s: from %s on %s", label, src_str, ifp->name); - } + if (PIM_DEBUG_PIM_TRACE) + zlog_debug("%s: from %pPA on %s", label, &src, ifp->name); } static void recv_join(struct interface *ifp, struct pim_neighbor *neigh, @@ -422,6 +418,7 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) size_t packet_left = 0; size_t packet_size = 0; size_t group_size = 0; + pim_addr rpf_addr; if (rpf->source_nexthop.interface) pim_ifp = rpf->source_nexthop.interface->info; @@ -430,8 +427,9 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) return -1; } - on_trace(__func__, rpf->source_nexthop.interface, - rpf->rpf_addr.u.prefix4); + rpf_addr = pim_addr_from_prefix(&rpf->rpf_addr); + + on_trace(__func__, rpf->source_nexthop.interface, rpf_addr); if (!pim_ifp) { zlog_warn("%s: multicast not enabled on interface %s", __func__, @@ -439,15 +437,12 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) return -1; } - if (rpf->rpf_addr.u.prefix4.s_addr == INADDR_ANY) { - if (PIM_DEBUG_PIM_J_P) { - char dst_str[INET_ADDRSTRLEN]; - pim_inet4_dump("<dst?>", rpf->rpf_addr.u.prefix4, - dst_str, sizeof(dst_str)); - zlog_debug("%s: upstream=%s is myself on interface %s", - __func__, dst_str, - rpf->source_nexthop.interface->name); - } + if (pim_addr_is_any(rpf_addr)) { + if (PIM_DEBUG_PIM_J_P) + zlog_debug( + "%s: upstream=%pPA is myself on interface %s", + __func__, &rpf_addr, + rpf->source_nexthop.interface->name); return 0; } @@ -468,8 +463,8 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) memset(msg, 0, sizeof(*msg)); - pim_msg_addr_encode_ipv4_ucast((uint8_t *)&msg->addr, - rpf->rpf_addr.u.prefix4); + pim_msg_addr_encode_ucast((uint8_t *)&msg->addr, + rpf_addr); msg->reserved = 0; msg->holdtime = htons(PIM_JP_HOLDTIME); @@ -485,15 +480,11 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) packet_left = rpf->source_nexthop.interface->mtu - 24; packet_left -= packet_size; } - if (PIM_DEBUG_PIM_J_P) { - char dst_str[INET_ADDRSTRLEN]; - pim_inet4_dump("<dst?>", rpf->rpf_addr.u.prefix4, - dst_str, sizeof(dst_str)); + if (PIM_DEBUG_PIM_J_P) zlog_debug( - "%s: sending (G)=%pPAs to upstream=%s on interface %s", - __func__, &group->group, dst_str, + "%s: sending (G)=%pPAs to upstream=%pPA on interface %s", + __func__, &group->group, &rpf_addr, rpf->source_nexthop.interface->name); - } group_size = pim_msg_get_jp_group_size(group->sources); if (group_size > packet_left) { @@ -513,8 +504,8 @@ int pim_joinprune_send(struct pim_rpf *rpf, struct list *groups) msg = (struct pim_jp *)pim_msg; memset(msg, 0, sizeof(*msg)); - pim_msg_addr_encode_ipv4_ucast((uint8_t *)&msg->addr, - rpf->rpf_addr.u.prefix4); + pim_msg_addr_encode_ucast((uint8_t *)&msg->addr, + rpf_addr); msg->reserved = 0; msg->holdtime = htons(PIM_JP_HOLDTIME); diff --git a/pimd/pim_nht.c b/pimd/pim_nht.c index 4e7aad99f1..35c8469090 100644 --- a/pimd/pim_nht.c +++ b/pimd/pim_nht.c @@ -993,12 +993,14 @@ int pim_ecmp_nexthop_lookup(struct pim_instance *pim, if (!nbr && !if_is_loopback(ifp)) { if (i == mod_val) mod_val++; - i++; if (PIM_DEBUG_PIM_NHT) zlog_debug( - "%s: NBR not found on input interface %s(%s) (RPF for source %pPA)", - __func__, ifp->name, - pim->vrf->name, &src_addr); + "%s: NBR (%pFXh) not found on input interface %s(%s) (RPF for source %pPA)", + __func__, + &nexthop_tab[i].nexthop_addr, + ifp->name, pim->vrf->name, + &src_addr); + i++; continue; } } diff --git a/pimd/pim_pim.c b/pimd/pim_pim.c index 3980e4828d..616cad16c6 100644 --- a/pimd/pim_pim.c +++ b/pimd/pim_pim.c @@ -523,7 +523,8 @@ static uint16_t ip_id = 0; static int pim_msg_send_frame(int fd, char *buf, size_t len, - struct sockaddr *dst, size_t salen) + struct sockaddr *dst, size_t salen, + const char *ifname) { struct ip *ip = (struct ip *)buf; @@ -539,8 +540,8 @@ static int pim_msg_send_frame(int fd, char *buf, size_t len, ip->ip_len = htons(sendlen); ip->ip_off = htons(offset | IP_MF); - if (pim_msg_send_frame(fd, buf, sendlen, dst, salen) - == 0) { + if (pim_msg_send_frame(fd, buf, sendlen, dst, salen, + ifname) == 0) { struct ip *ip2 = (struct ip *)(buf + newlen1); size_t newlen2 = len - sendlen; sendlen = newlen2 + hdrsize; @@ -549,7 +550,8 @@ static int pim_msg_send_frame(int fd, char *buf, size_t len, ip2->ip_len = htons(sendlen); ip2->ip_off = htons(offset + (newlen1 >> 3)); return pim_msg_send_frame(fd, (char *)ip2, - sendlen, dst, salen); + sendlen, dst, salen, + ifname); } } @@ -559,9 +561,9 @@ static int pim_msg_send_frame(int fd, char *buf, size_t len, pim_inet4_dump("<dst?>", ip->ip_dst, dst_str, sizeof(dst_str)); zlog_warn( - "%s: sendto() failure to %s: fd=%d msg_size=%zd: errno=%d: %s", - __func__, dst_str, fd, len, errno, - safe_strerror(errno)); + "%s: sendto() failure to %s: iface=%s fd=%d msg_size=%zd: errno=%d: %s", + __func__, dst_str, ifname, fd, len, + errno, safe_strerror(errno)); } return -1; } @@ -643,7 +645,7 @@ int pim_msg_send(int fd, pim_addr src, pim_addr dst, uint8_t *pim_msg, } pim_msg_send_frame(fd, (char *)buffer, sendlen, (struct sockaddr *)&to, - tolen); + tolen, ifname); return 0; } diff --git a/pimd/pim_tlv.c b/pimd/pim_tlv.c index 86403dd54a..028514659b 100644 --- a/pimd/pim_tlv.c +++ b/pimd/pim_tlv.c @@ -127,11 +127,7 @@ int pim_encode_addr_ucast(uint8_t *buf, pim_addr addr) { uint8_t *start = buf; -#if PIM_IPV == 4 - *buf++ = PIM_MSG_ADDRESS_FAMILY_IPV4; -#else - *buf++ = PIM_MSG_ADDRESS_FAMILY_IPV6; -#endif + *buf++ = PIM_MSG_ADDRESS_FAMILY; *buf++ = 0; memcpy(buf, &addr, sizeof(addr)); buf += sizeof(addr); @@ -624,16 +620,15 @@ int pim_parse_addr_source(pim_sgaddr *sg, uint8_t *flags, const uint8_t *buf, } switch (family) { - case PIM_MSG_ADDRESS_FAMILY_IPV4: - if ((addr + sizeof(struct in_addr)) > pastend) { + case PIM_MSG_ADDRESS_FAMILY: + if ((addr + sizeof(sg->src)) > pastend) { zlog_warn( - "%s: IPv4 source address overflow: left=%td needed=%zu", - __func__, pastend - addr, - sizeof(struct in_addr)); + "%s: IP source address overflow: left=%td needed=%zu", + __func__, pastend - addr, sizeof(sg->src)); return -3; } - memcpy(&sg->src, addr, sizeof(struct in_addr)); + memcpy(&sg->src, addr, sizeof(sg->src)); /* RFC 4601: 4.9.1 Encoded Source and Group Address Formats @@ -642,27 +637,24 @@ int pim_parse_addr_source(pim_sgaddr *sg, uint8_t *flags, const uint8_t *buf, The mask length MUST be equal to the mask length in bits for the given Address Family and Encoding Type (32 for IPv4 - native - and 128 for IPv6 native). A router SHOULD ignore any - messages - received with any other mask length. + native and 128 for IPv6 native). A router SHOULD ignore any + messages received with any other mask length. */ - if (mask_len != IPV4_MAX_BITLEN) { - zlog_warn("%s: IPv4 bad source address mask: %d", + if (mask_len != PIM_MAX_BITLEN) { + zlog_warn("%s: IP bad source address mask: %d", __func__, mask_len); return -4; } - addr += sizeof(struct in_addr); + addr += sizeof(sg->src); break; - default: { + default: zlog_warn( "%s: unknown source address encoding family=%d: %02x%02x%02x%02x", __func__, family, buf[0], buf[1], buf[2], buf[3]); return -5; } - } return addr - buf; } diff --git a/tests/.gitignore b/tests/.gitignore index 70d0ef6e0a..f00177abd8 100644 --- a/tests/.gitignore +++ b/tests/.gitignore @@ -1,6 +1,8 @@ *.log *.sum *.xml +frr-northbound.proto +frr_northbound* .pytest_cache /bgpd/test_aspath /bgpd/test_bgp_table diff --git a/tests/lib/test_grpc.py b/tests/lib/test_grpc.py index 06ae6c05dd..2e292fadc9 100644 --- a/tests/lib/test_grpc.py +++ b/tests/lib/test_grpc.py @@ -1,8 +1,10 @@ import inspect import os import subprocess -import pytest + import frrtest +import pytest + class TestGRPC(object): program = "./test_grpc" @@ -15,9 +17,13 @@ class TestGRPC(object): basedir = os.path.dirname(inspect.getsourcefile(type(self))) program = os.path.join(basedir, self.program) proc = subprocess.Popen( - [frrtest.binpath(program)], stdin=subprocess.PIPE, stdout=subprocess.PIPE + [frrtest.binpath(program)], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, ) output, _ = proc.communicate() self.exitcode = proc.wait() if self.exitcode != 0: + print("OUTPUT:\n" + output.decode("ascii")) raise frrtest.TestExitNonzero(self) diff --git a/tests/topotests/analyze.py b/tests/topotests/analyze.py index 888e706339..bdb2e56ee1 100755 --- a/tests/topotests/analyze.py +++ b/tests/topotests/analyze.py @@ -198,9 +198,12 @@ def main(): logging.critical("%s doesn't exist", args.results) sys.exit(1) ttfiles = [args.results] + elif os.path.exists("/tmp/topotests/topotests.xml"): + ttfiles.append("/tmp/topotests/topotests.xml") - if not ttfiles and os.path.exists("/tmp/topotests.xml"): - ttfiles.append("/tmp/topotests.xml") + if not ttfiles: + if os.path.exists("/tmp/topotests.xml"): + ttfiles.append("/tmp/topotests.xml") for f in ttfiles: m = re.match(r"tt-group-(\d+)/topotests.xml", f) diff --git a/tests/topotests/grpc_basic/lib b/tests/topotests/grpc_basic/lib new file mode 120000 index 0000000000..dc598c56dc --- /dev/null +++ b/tests/topotests/grpc_basic/lib @@ -0,0 +1 @@ +../lib
\ No newline at end of file diff --git a/tests/topotests/grpc_basic/r1/zebra.conf b/tests/topotests/grpc_basic/r1/zebra.conf new file mode 100644 index 0000000000..49a0911c53 --- /dev/null +++ b/tests/topotests/grpc_basic/r1/zebra.conf @@ -0,0 +1,8 @@ +log record-priority +log timestamp precision 6 +log extended extlog + destination file ext-log.txt create + timestamp precision 6 + structured-data code-location +interface r1-eth0 + ip address 192.168.1.1/24
\ No newline at end of file diff --git a/tests/topotests/grpc_basic/r2/zebra.conf b/tests/topotests/grpc_basic/r2/zebra.conf new file mode 100644 index 0000000000..20da1885d4 --- /dev/null +++ b/tests/topotests/grpc_basic/r2/zebra.conf @@ -0,0 +1,8 @@ +log record-priority +log timestamp precision 6 +log extended extlog + destination file ext-log.txt create + timestamp precision 6 + structured-data code-location +interface r2-eth0 + ip address 192.168.1.2/24 diff --git a/tests/topotests/grpc_basic/test_basic_grpc.py b/tests/topotests/grpc_basic/test_basic_grpc.py new file mode 100644 index 0000000000..b6812a5afc --- /dev/null +++ b/tests/topotests/grpc_basic/test_basic_grpc.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# February 21 2022, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 2022, 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_basic_grpc.py: Test Basic gRPC. +""" + +import logging +import os +import sys + +import pytest + +from lib.common_config import step +from lib.micronet import commander +from lib.topogen import Topogen, TopoRouter +from lib.topolog import logger + +CWD = os.path.dirname(os.path.realpath(__file__)) + +GRPCP_ZEBRA = 50051 +GRPCP_STATICD = 50052 +GRPCP_BFDD = 50053 +GRPCP_ISISD = 50054 +GRPCP_OSPFD = 50055 +GRPCP_PIMD = 50056 + +pytestmark = [ + # pytest.mark.mgmtd -- Need a new non-protocol marker + # pytest.mark.bfdd, + # pytest.mark.isisd, + # pytest.mark.ospfd, + # pytest.mark.pimd, + pytest.mark.staticd, +] + +script_path = os.path.realpath(os.path.join(CWD, "../lib/grpc-query.py")) + +try: + commander.cmd_raises([script_path, "--check"]) +except Exception: + pytest.skip( + "skipping; cannot create or import gRPC proto modules", allow_module_level=True + ) + + +@pytest.fixture(scope="module") +def tgen(request): + "Setup/Teardown the environment and provide tgen argument to tests" + topodef = {"s1": ("r1", "r2")} + tgen = Topogen(topodef, request.module.__name__) + + tgen.start_topology() + router_list = tgen.routers() + + for rname, router in router_list.items(): + router.load_config(TopoRouter.RD_ZEBRA, "zebra.conf", f"-M grpc:{GRPCP_ZEBRA}") + router.load_config(TopoRouter.RD_STATIC, None, f"-M grpc:{GRPCP_STATICD}") + # router.load_config(TopoRouter.RD_BFD, None, f"-M grpc:{GRPCP_BFDD}") + # router.load_config(TopoRouter.RD_ISIS, None, f"-M grpc:{GRPCP_ISISD}") + # router.load_config(TopoRouter.RD_OSPF, None, f"-M grpc:{GRPCP_OSPFD}") + # router.load_config(TopoRouter.RD_PIM, None, f"-M grpc:{GRPCP_PIMD}") + + tgen.start_router() + yield tgen + + logging.info("Stopping all routers (no assert on error)") + tgen.stop_topology() + + +# Let's not do this so we catch errors +# 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") + + +# =================== +# The tests functions +# =================== + + +def run_grpc_client(r, port, commands): + if not isinstance(commands, str): + commands = "\n".join(commands) + "\n" + if not commands.endswith("\n"): + commands += "\n" + return r.cmd_raises([script_path, f"--port={port}"], stdin=commands) + + +def test_connectivity(tgen): + r1 = tgen.gears["r1"] + output = r1.cmd_raises("ping -c1 192.168.1.2") + logging.info("ping output: %s", output) + + +def test_capabilities(tgen): + r1 = tgen.gears["r1"] + output = run_grpc_client(r1, GRPCP_ZEBRA, "GETCAP") + logging.info("grpc output: %s", output) + + +def test_get_config(tgen): + nrepeat = 5 + r1 = tgen.gears["r1"] + + step("'GET' inteface config 10 times, once per invocation") + + for i in range(0, nrepeat): + output = run_grpc_client(r1, GRPCP_ZEBRA, "GET,/frr-interface:lib") + logging.info("[iteration %s]: grpc GET output: %s", i, output) + + step(f"'GET' YANG {nrepeat} times in one invocation") + commands = ["GET,/frr-interface:lib" for _ in range(0, 10)] + output = run_grpc_client(r1, GRPCP_ZEBRA, commands) + logging.info("grpc GET*{%d} output: %s", nrepeat, output) + + +def test_get_vrf_config(tgen): + r1 = tgen.gears["r1"] + + step("'GET' get VRF config") + + output = run_grpc_client(r1, GRPCP_ZEBRA, "GET,/frr-vrf:lib") + logging.info("grpc GET /frr-vrf:lib output: %s", output) + + +def test_shutdown_checks(tgen): + # Start a process rnuning that will fetch bunches of data then shut the routers down + # and check for cores. + nrepeat = 100 + r1 = tgen.gears["r1"] + commands = ["GET,/frr-interface:lib" for _ in range(0, nrepeat)] + p = r1.popen([script_path, f"--port={GRPCP_ZEBRA}"] + commands) + import time + + time.sleep(1) + try: + for r in tgen.routers().values(): + r.net.stopRouter() + r.net.checkRouterCores() + finally: + if p: + p.terminate() + p.wait() + + +# Memory leak test template +# Not compatible with the shutdown check above +def _test_memory_leak(tgen): + "Run the memory leak test and report results." + + if not tgen.is_memleak_enabled(): + pytest.skip("Memory leak test/report is disabled") + + tgen.report_memory_leaks() + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/lib/grpc-query.py b/tests/topotests/lib/grpc-query.py new file mode 100755 index 0000000000..61f01c36bb --- /dev/null +++ b/tests/topotests/lib/grpc-query.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# February 22 2022, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 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 logging +import os +import sys + +import pytest + +CWD = os.path.dirname(os.path.realpath(__file__)) + +# This is painful but works if you have installed grpc and grpc_tools would be *way* +# better if we actually built and installed these but ... python packaging. +try: + import grpc + import grpc_tools + + from micronet import commander + + commander.cmd_raises(f"cp {CWD}/../../../grpc/frr-northbound.proto .") + commander.cmd_raises( + f"python -m grpc_tools.protoc --python_out=. --grpc_python_out=. -I . frr-northbound.proto" + ) +except Exception as error: + logging.error("can't create proto definition modules %s", error) + raise + +try: + sys.path[0:0] = "." + import frr_northbound_pb2 + import frr_northbound_pb2_grpc + + # Would be nice if compiling the modules internally from the source worked + # # import grpc_tools.protoc + # # proto_include = pkg_resources.resource_filename("grpc_tools", "_proto") + # from grpc_tools.protoc import _proto_file_to_module_name, _protos_and_services + # try: + # frr_northbound_pb2, frr_northbound_pb2_grpc = _protos_and_services( + # "frr_northbound.proto" + # ) + # finally: + # os.chdir(CWD) +except Exception as error: + logging.error("can't import proto definition modules %s", error) + raise + + +class GRPCClient: + def __init__(self, server, port): + self.channel = grpc.insecure_channel("{}:{}".format(server, port)) + self.stub = frr_northbound_pb2_grpc.NorthboundStub(self.channel) + + def get_capabilities(self): + request = frr_northbound_pb2.GetCapabilitiesRequest() + response = "NONE" + try: + response = self.stub.GetCapabilities(request) + except Exception as error: + logging.error("Got exception from stub: %s", error) + + logging.debug("GRPC Capabilities: %s", response) + return response + + def get(self, xpath): + request = frr_northbound_pb2.GetRequest() + request.path.append(xpath) + request.type = frr_northbound_pb2.GetRequest.ALL + request.encoding = frr_northbound_pb2.XML + xml = "" + for r in self.stub.Get(request): + logging.info('GRPC Get path: "%s" value: %s', request.path, r) + xml += str(r.data.data) + return xml + + +def next_action(action_list=None): + "Get next action from list or STDIN" + if action_list: + for action in action_list: + yield action + else: + while True: + try: + action = input("") + if not action: + break + yield action.strip() + except EOFError: + break + + +def main(*args): + parser = argparse.ArgumentParser(description="gRPC Client") + parser.add_argument( + "-s", "--server", default="localhost", help="gRPC Server Address" + ) + parser.add_argument( + "-p", "--port", type=int, default=50051, help="gRPC Server TCP Port" + ) + parser.add_argument("-v", "--verbose", action="store_true", help="be verbose") + parser.add_argument("--check", action="store_true", help="check runable") + parser.add_argument("actions", nargs="*", help="GETCAP|GET,xpath") + args = parser.parse_args(*args) + + level = logging.DEBUG if args.verbose else logging.INFO + logging.basicConfig( + level=level, + format="%(asctime)s %(levelname)s: GRPC-CLI-CLIENT: %(name)s %(message)s", + ) + + if args.check: + sys.exit(0) + + c = GRPCClient(args.server, args.port) + + for action in next_action(args.actions): + action = action.casefold() + logging.info("GOT ACTION: %s", action) + if action == "getcap": + caps = c.get_capabilities() + print("Capabilities:", caps) + elif action.startswith("get,"): + # Print Interface State and Config + _, xpath = action.split(",", 1) + print("Get XPath: ", xpath) + xml = c.get(xpath) + print("{}: {}".format(xpath, xml)) + # for _ in range(0, 1): + + +if __name__ == "__main__": + main() |
