diff options
Diffstat (limited to 'tests/topotests/lib')
26 files changed, 25295 insertions, 0 deletions
diff --git a/tests/topotests/lib/__init__.py b/tests/topotests/lib/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/topotests/lib/__init__.py diff --git a/tests/topotests/lib/bgp.py b/tests/topotests/lib/bgp.py new file mode 100644 index 0000000..7ab36c4 --- /dev/null +++ b/tests/topotests/lib/bgp.py @@ -0,0 +1,5564 @@ +# +# Copyright (c) 2019 by VMware, Inc. ("VMware") +# Used Copyright (c) 2018 by Network Device Education Foundation, Inc. +# ("NetDEF") in this file. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import ipaddress +import sys +import traceback +from copy import deepcopy +from time import sleep + +# Import common_config to use commomnly used APIs +from lib.common_config import ( + create_common_configurations, + FRRCFG_FILE, + InvalidCLIError, + apply_raw_config, + check_address_types, + find_interface_with_greater_ip, + generate_ips, + get_frr_ipv6_linklocal, + retry, + run_frr_cmd, + validate_ip_address, +) +from lib.topogen import get_topogen +from lib.topolog import logger +from lib.topotest import frr_unicode + +from lib import topotest + + +def create_router_bgp(tgen, topo=None, input_dict=None, build=False, load_config=True): + """ + API to configure bgp on router + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict = { + "r1": { + "bgp": { + "local_as": "200", + "router_id": "22.22.22.22", + "graceful-restart": { + "graceful-restart": True, + "preserve-fw-state": True, + "timer": { + "restart-time": 30, + "rib-stale-time": 30, + "select-defer-time": 30, + } + }, + "address_family": { + "ipv4": { + "unicast": { + "default_originate":{ + "neighbor":"R2", + "add_type":"lo" + "route_map":"rm" + + }, + "redistribute": [{ + "redist_type": "static", + "attribute": { + "metric" : 123 + } + }, + {"redist_type": "connected"} + ], + "advertise_networks": [ + { + "network": "20.0.0.0/32", + "no_of_network": 10 + }, + { + "network": "30.0.0.0/32", + "no_of_network": 10 + } + ], + "neighbor": { + "r3": { + "keepalivetimer": 60, + "holddowntimer": 180, + "dest_link": { + "r4": { + "allowas-in": { + "number_occurences": 2 + }, + "prefix_lists": [ + { + "name": "pf_list_1", + "direction": "in" + } + ], + "route_maps": [{ + "name": "RMAP_MED_R3", + "direction": "in" + }], + "next_hop_self": True + }, + "r1": {"graceful-restart-helper": True} + } + } + } + } + } + } + } + } + } + + + Returns + ------- + True or False + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + + if topo is None: + topo = tgen.json_topo + + # Flag is used when testing ipv6 over ipv4 or vice-versa + afi_test = False + + if not input_dict: + input_dict = deepcopy(topo) + else: + topo = topo["routers"] + input_dict = deepcopy(input_dict) + + config_data_dict = {} + + for router in input_dict.keys(): + if "bgp" not in input_dict[router]: + logger.debug("Router %s: 'bgp' not present in input_dict", router) + continue + + bgp_data_list = input_dict[router]["bgp"] + + if type(bgp_data_list) is not list: + bgp_data_list = [bgp_data_list] + + config_data = [] + + for bgp_data in bgp_data_list: + data_all_bgp = __create_bgp_global(tgen, bgp_data, router, build) + if data_all_bgp: + bgp_addr_data = bgp_data.setdefault("address_family", {}) + + if not bgp_addr_data: + logger.debug( + "Router %s: 'address_family' not present in " + "input_dict for BGP", + router, + ) + else: + + ipv4_data = bgp_addr_data.setdefault("ipv4", {}) + ipv6_data = bgp_addr_data.setdefault("ipv6", {}) + l2vpn_data = bgp_addr_data.setdefault("l2vpn", {}) + + neigh_unicast = ( + True + if ipv4_data.setdefault("unicast", {}) + or ipv6_data.setdefault("unicast", {}) + else False + ) + + l2vpn_evpn = True if l2vpn_data.setdefault("evpn", {}) else False + + if neigh_unicast: + data_all_bgp = __create_bgp_unicast_neighbor( + tgen, + topo, + bgp_data, + router, + afi_test, + config_data=data_all_bgp, + ) + + if l2vpn_evpn: + data_all_bgp = __create_l2vpn_evpn_address_family( + tgen, topo, bgp_data, router, config_data=data_all_bgp + ) + if data_all_bgp: + config_data.extend(data_all_bgp) + + if config_data: + config_data_dict[router] = config_data + + try: + result = create_common_configurations( + tgen, config_data_dict, "bgp", build, load_config + ) + except InvalidCLIError: + logger.error("create_router_bgp", exc_info=True) + result = False + + logger.debug("Exiting lib API: create_router_bgp()") + return result + + +def __create_bgp_global(tgen, input_dict, router, build=False): + """ + Helper API to create bgp global configuration. + + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured. + * `build` : Only for initial setup phase this is set as True. + + Returns + ------- + list of config commands + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + bgp_data = input_dict + del_bgp_action = bgp_data.setdefault("delete", False) + + config_data = [] + + if "local_as" not in bgp_data and build: + logger.debug( + "Router %s: 'local_as' not present in input_dict" "for BGP", router + ) + return config_data + + local_as = bgp_data.setdefault("local_as", "") + cmd = "router bgp {}".format(local_as) + vrf_id = bgp_data.setdefault("vrf", None) + if vrf_id: + cmd = "{} vrf {}".format(cmd, vrf_id) + + if del_bgp_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + return config_data + + config_data.append(cmd) + config_data.append("no bgp ebgp-requires-policy") + + router_id = bgp_data.setdefault("router_id", None) + del_router_id = bgp_data.setdefault("del_router_id", False) + if del_router_id: + config_data.append("no bgp router-id") + if router_id: + config_data.append("bgp router-id {}".format(router_id)) + + config_data.append("bgp log-neighbor-changes") + config_data.append("no bgp network import-check") + bgp_peer_grp_data = bgp_data.setdefault("peer-group", {}) + + if "peer-group" in bgp_data and bgp_peer_grp_data: + peer_grp_data = __create_bgp_peer_group(tgen, bgp_peer_grp_data, router) + config_data.extend(peer_grp_data) + + bst_path = bgp_data.setdefault("bestpath", None) + if bst_path: + if "aspath" in bst_path: + if "delete" in bst_path: + config_data.append( + "no bgp bestpath as-path {}".format(bst_path["aspath"]) + ) + else: + config_data.append("bgp bestpath as-path {}".format(bst_path["aspath"])) + + if "graceful-restart" in bgp_data: + graceful_config = bgp_data["graceful-restart"] + + graceful_restart = graceful_config.setdefault("graceful-restart", None) + + graceful_restart_disable = graceful_config.setdefault( + "graceful-restart-disable", None + ) + + preserve_fw_state = graceful_config.setdefault("preserve-fw-state", None) + + disable_eor = graceful_config.setdefault("disable-eor", None) + + if graceful_restart == False: + cmd = "no bgp graceful-restart" + if graceful_restart: + cmd = "bgp graceful-restart" + + if graceful_restart is not None: + config_data.append(cmd) + + if graceful_restart_disable == False: + cmd = "no bgp graceful-restart-disable" + if graceful_restart_disable: + cmd = "bgp graceful-restart-disable" + + if graceful_restart_disable is not None: + config_data.append(cmd) + + if preserve_fw_state == False: + cmd = "no bgp graceful-restart preserve-fw-state" + if preserve_fw_state: + cmd = "bgp graceful-restart preserve-fw-state" + + if preserve_fw_state is not None: + config_data.append(cmd) + + if disable_eor == False: + cmd = "no bgp graceful-restart disable-eor" + if disable_eor: + cmd = "bgp graceful-restart disable-eor" + + if disable_eor is not None: + config_data.append(cmd) + + if "timer" in bgp_data["graceful-restart"]: + timer = bgp_data["graceful-restart"]["timer"] + + if "delete" in timer: + del_action = timer["delete"] + else: + del_action = False + + for rs_timer, value in timer.items(): + rs_timer_value = timer.setdefault(rs_timer, None) + + if rs_timer_value and rs_timer != "delete": + cmd = "bgp graceful-restart {} {}".format(rs_timer, rs_timer_value) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return config_data + + +def __create_bgp_unicast_neighbor( + tgen, topo, input_dict, router, afi_test, config_data=None +): + """ + Helper API to create configuration for address-family unicast + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured. + * `afi_test` : use when ipv6 needs to be tested over ipv4 or vice-versa + * `build` : Only for initial setup phase this is set as True. + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + add_neigh = True + bgp_data = input_dict + if "router bgp" in config_data: + add_neigh = False + + bgp_data = input_dict["address_family"] + + for addr_type, addr_dict in bgp_data.items(): + if not addr_dict: + continue + + if not check_address_types(addr_type) and not afi_test: + continue + + addr_data = addr_dict["unicast"] + if addr_data: + config_data.append("address-family {} unicast".format(addr_type)) + + advertise_network = addr_data.setdefault("advertise_networks", []) + for advertise_network_dict in advertise_network: + network = advertise_network_dict["network"] + if type(network) is not list: + network = [network] + + if "no_of_network" in advertise_network_dict: + no_of_network = advertise_network_dict["no_of_network"] + else: + no_of_network = 1 + + del_action = advertise_network_dict.setdefault("delete", False) + + # Generating IPs for verification + network_list = generate_ips(network, no_of_network) + for ip in network_list: + ip = str(ipaddress.ip_network(frr_unicode(ip))) + + cmd = "network {}".format(ip) + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + import_cmd = addr_data.setdefault("import", {}) + if import_cmd: + try: + if import_cmd["delete"]: + config_data.append("no import vrf {}".format(import_cmd["vrf"])) + except KeyError: + config_data.append("import vrf {}".format(import_cmd["vrf"])) + + max_paths = addr_data.setdefault("maximum_paths", {}) + if max_paths: + ibgp = max_paths.setdefault("ibgp", None) + ebgp = max_paths.setdefault("ebgp", None) + del_cmd = max_paths.setdefault("delete", False) + if ibgp: + if del_cmd: + config_data.append("no maximum-paths ibgp {}".format(ibgp)) + else: + config_data.append("maximum-paths ibgp {}".format(ibgp)) + if ebgp: + if del_cmd: + config_data.append("no maximum-paths {}".format(ebgp)) + else: + config_data.append("maximum-paths {}".format(ebgp)) + + aggregate_addresses = addr_data.setdefault("aggregate_address", []) + for aggregate_address in aggregate_addresses: + network = aggregate_address.setdefault("network", None) + if not network: + logger.debug( + "Router %s: 'network' not present in " "input_dict for BGP", router + ) + else: + cmd = "aggregate-address {}".format(network) + + as_set = aggregate_address.setdefault("as_set", False) + summary = aggregate_address.setdefault("summary", False) + del_action = aggregate_address.setdefault("delete", False) + if as_set: + cmd = "{} as-set".format(cmd) + if summary: + cmd = "{} summary".format(cmd) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + redistribute_data = addr_data.setdefault("redistribute", {}) + if redistribute_data: + for redistribute in redistribute_data: + if "redist_type" not in redistribute: + logger.debug( + "Router %s: 'redist_type' not present in " "input_dict", router + ) + else: + cmd = "redistribute {}".format(redistribute["redist_type"]) + redist_attr = redistribute.setdefault("attribute", None) + if redist_attr: + if type(redist_attr) is dict: + for key, value in redist_attr.items(): + cmd = "{} {} {}".format(cmd, key, value) + else: + cmd = "{} {}".format(cmd, redist_attr) + + del_action = redistribute.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + admin_dist_data = addr_data.setdefault("distance", {}) + if admin_dist_data: + if len(admin_dist_data) < 2: + logger.debug( + "Router %s: pass the admin distance values for " + "ebgp, ibgp and local routes", + router, + ) + cmd = "distance bgp {} {} {}".format( + admin_dist_data["ebgp"], + admin_dist_data["ibgp"], + admin_dist_data["local"], + ) + + del_action = admin_dist_data.setdefault("delete", False) + if del_action: + cmd = "no distance bgp" + config_data.append(cmd) + + import_vrf_data = addr_data.setdefault("import", {}) + if import_vrf_data: + cmd = "import vrf {}".format(import_vrf_data["vrf"]) + + del_action = import_vrf_data.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if "neighbor" in addr_data: + neigh_data = __create_bgp_neighbor( + topo, input_dict, router, addr_type, add_neigh + ) + config_data.extend(neigh_data) + # configure default originate + if "default_originate" in addr_data: + default_originate_config = __create_bgp_default_originate_neighbor( + topo, input_dict, router, addr_type, add_neigh + ) + config_data.extend(default_originate_config) + + for addr_type, addr_dict in bgp_data.items(): + if not addr_dict or not check_address_types(addr_type): + continue + + addr_data = addr_dict["unicast"] + if "neighbor" in addr_data: + neigh_addr_data = __create_bgp_unicast_address_family( + topo, input_dict, router, addr_type, add_neigh + ) + + config_data.extend(neigh_addr_data) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return config_data + + +def __create_bgp_default_originate_neighbor( + topo, input_dict, router, addr_type, add_neigh=True +): + """ + Helper API to create neighbor default - originate configuration + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured + """ + tgen = get_topogen() + config_data = [] + logger.debug("Entering lib API: __create_bgp_default_originate_neighbor()") + + bgp_data = input_dict["address_family"] + neigh_data = bgp_data[addr_type]["unicast"]["default_originate"] + for name, peer_dict in neigh_data.items(): + nh_details = topo[name] + + neighbor_ip = None + if "dest-link" in neigh_data[name]: + dest_link = neigh_data[name]["dest-link"] + neighbor_ip = nh_details["links"][dest_link][addr_type].split("/")[0] + elif "add_type" in neigh_data[name]: + add_type = neigh_data[name]["add_type"] + neighbor_ip = nh_details["links"][add_type][addr_type].split("/")[0] + else: + neighbor_ip = nh_details["links"][router][addr_type].split("/")[0] + + config_data.append("address-family {} unicast".format(addr_type)) + if "route_map" in peer_dict: + route_map = peer_dict["route_map"] + if "delete" in peer_dict: + if peer_dict["delete"]: + config_data.append( + "no neighbor {} default-originate route-map {}".format( + neighbor_ip, route_map + ) + ) + else: + config_data.append( + " neighbor {} default-originate route-map {}".format( + neighbor_ip, route_map + ) + ) + else: + config_data.append( + " neighbor {} default-originate route-map {}".format( + neighbor_ip, route_map + ) + ) + + else: + if "delete" in peer_dict: + if peer_dict["delete"]: + config_data.append( + "no neighbor {} default-originate".format(neighbor_ip) + ) + else: + config_data.append( + "neighbor {} default-originate".format(neighbor_ip) + ) + else: + config_data.append("neighbor {} default-originate".format(neighbor_ip)) + + logger.debug("Exiting lib API: __create_bgp_default_originate_neighbor()") + return config_data + + +def __create_l2vpn_evpn_address_family( + tgen, topo, input_dict, router, config_data=None +): + """ + Helper API to create configuration for l2vpn evpn address-family + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring + from testcase + * `router` : router id to be configured. + * `build` : Only for initial setup phase this is set as True. + """ + + result = False + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + bgp_data = input_dict["address_family"] + + for family_type, family_dict in bgp_data.items(): + if family_type != "l2vpn": + continue + + family_data = family_dict["evpn"] + if family_data: + config_data.append("address-family l2vpn evpn") + + advertise_data = family_data.setdefault("advertise", {}) + neighbor_data = family_data.setdefault("neighbor", {}) + advertise_all_vni_data = family_data.setdefault("advertise-all-vni", None) + rd_data = family_data.setdefault("rd", None) + no_rd_data = family_data.setdefault("no rd", False) + route_target_data = family_data.setdefault("route-target", {}) + + if advertise_data: + for address_type, unicast_type in advertise_data.items(): + + if type(unicast_type) is dict: + for key, value in unicast_type.items(): + cmd = "advertise {} {}".format(address_type, key) + + if value: + route_map = value.setdefault("route-map", {}) + advertise_del_action = value.setdefault("delete", None) + + if route_map: + cmd = "{} route-map {}".format(cmd, route_map) + + if advertise_del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + if neighbor_data: + for neighbor, neighbor_data in neighbor_data.items(): + ipv4_neighbor = neighbor_data.setdefault("ipv4", {}) + ipv6_neighbor = neighbor_data.setdefault("ipv6", {}) + + if ipv4_neighbor: + for neighbor_name, action in ipv4_neighbor.items(): + neighbor_ip = topo[neighbor]["links"][neighbor_name][ + "ipv4" + ].split("/")[0] + + if type(action) is dict: + next_hop_self = action.setdefault("next_hop_self", None) + route_maps = action.setdefault("route_maps", {}) + + if next_hop_self is not None: + if next_hop_self is True: + config_data.append( + "neighbor {} " + "next-hop-self".format(neighbor_ip) + ) + elif next_hop_self is False: + config_data.append( + "no neighbor {} " + "next-hop-self".format(neighbor_ip) + ) + + if route_maps: + for route_map in route_maps: + name = route_map.setdefault("name", {}) + direction = route_map.setdefault("direction", "in") + del_action = route_map.setdefault("delete", False) + + if not name: + logger.info( + "Router %s: 'name' " + "not present in " + "input_dict for BGP " + "neighbor route name", + router, + ) + else: + cmd = "neighbor {} route-map {} " "{}".format( + neighbor_ip, name, direction + ) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + else: + if action == "activate": + cmd = "neighbor {} activate".format(neighbor_ip) + elif action == "deactivate": + cmd = "no neighbor {} activate".format(neighbor_ip) + + config_data.append(cmd) + + if ipv6_neighbor: + for neighbor_name, action in ipv4_neighbor.items(): + neighbor_ip = topo[neighbor]["links"][neighbor_name][ + "ipv6" + ].split("/")[0] + if action == "activate": + cmd = "neighbor {} activate".format(neighbor_ip) + elif action == "deactivate": + cmd = "no neighbor {} activate".format(neighbor_ip) + + config_data.append(cmd) + + if advertise_all_vni_data == True: + cmd = "advertise-all-vni" + config_data.append(cmd) + elif advertise_all_vni_data == False: + cmd = "no advertise-all-vni" + config_data.append(cmd) + + if rd_data: + cmd = "rd {}".format(rd_data) + config_data.append(cmd) + + if no_rd_data: + cmd = "no rd {}".format(no_rd_data) + config_data.append(cmd) + + if route_target_data: + for rt_type, rt_dict in route_target_data.items(): + for _rt_dict in rt_dict: + rt_value = _rt_dict.setdefault("value", None) + del_rt = _rt_dict.setdefault("delete", None) + + if rt_value: + cmd = "route-target {} {}".format(rt_type, rt_value) + if del_rt: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return config_data + + +def __create_bgp_peer_group(topo, input_dict, router): + """ + Helper API to create neighbor specific configuration + + Parameters + ---------- + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured + """ + config_data = [] + logger.debug("Entering lib API: __create_bgp_peer_group()") + + for grp, grp_dict in input_dict.items(): + config_data.append("neighbor {} peer-group".format(grp)) + neigh_cxt = "neighbor {} ".format(grp) + update_source = grp_dict.setdefault("update-source", None) + remote_as = grp_dict.setdefault("remote-as", None) + capability = grp_dict.setdefault("capability", None) + if update_source: + config_data.append("{} update-source {}".format(neigh_cxt, update_source)) + + if remote_as: + config_data.append("{} remote-as {}".format(neigh_cxt, remote_as)) + + if capability: + config_data.append("{} capability {}".format(neigh_cxt, capability)) + + logger.debug("Exiting lib API: __create_bgp_peer_group()") + return config_data + + +def __create_bgp_neighbor(topo, input_dict, router, addr_type, add_neigh=True): + """ + Helper API to create neighbor specific configuration + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured + """ + config_data = [] + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + bgp_data = input_dict["address_family"] + neigh_data = bgp_data[addr_type]["unicast"]["neighbor"] + global_connect = input_dict.get("connecttimer", 5) + + for name, peer_dict in neigh_data.items(): + for dest_link, peer in peer_dict["dest_link"].items(): + nh_details = topo[name] + + if "vrfs" in topo[router] or type(nh_details["bgp"]) is list: + for vrf_data in nh_details["bgp"]: + if "vrf" in nh_details["links"][dest_link] and "vrf" in vrf_data: + if nh_details["links"][dest_link]["vrf"] == vrf_data["vrf"]: + remote_as = vrf_data["local_as"] + break + else: + if "vrf" not in vrf_data: + remote_as = vrf_data["local_as"] + break + + else: + remote_as = nh_details["bgp"]["local_as"] + + update_source = None + + if "neighbor_type" in peer and peer["neighbor_type"] == "unnumbered": + ip_addr = nh_details["links"][dest_link]["peer-interface"] + elif "neighbor_type" in peer and peer["neighbor_type"] == "link-local": + intf = topo[name]["links"][dest_link]["interface"] + ip_addr = get_frr_ipv6_linklocal(tgen, name, intf) + elif dest_link in nh_details["links"].keys(): + try: + ip_addr = nh_details["links"][dest_link][addr_type].split("/")[0] + except KeyError: + intf = topo[name]["links"][dest_link]["interface"] + ip_addr = get_frr_ipv6_linklocal(tgen, name, intf) + if "delete" in peer and peer["delete"]: + neigh_cxt = "no neighbor {}".format(ip_addr) + config_data.append("{}".format(neigh_cxt)) + return config_data + else: + neigh_cxt = "neighbor {}".format(ip_addr) + + if "peer-group" in peer: + config_data.append( + "neighbor {} interface peer-group {}".format( + ip_addr, peer["peer-group"] + ) + ) + + # Loopback interface + if "source_link" in peer: + if peer["source_link"] == "lo": + update_source = topo[router]["links"]["lo"][addr_type].split("/")[0] + else: + update_source = topo[router]["links"][peer["source_link"]][ + "interface" + ] + if "peer-group" not in peer: + if "neighbor_type" in peer and peer["neighbor_type"] == "unnumbered": + config_data.append( + "{} interface remote-as {}".format(neigh_cxt, remote_as) + ) + elif add_neigh: + config_data.append("{} remote-as {}".format(neigh_cxt, remote_as)) + + if addr_type == "ipv6": + config_data.append("address-family ipv6 unicast") + config_data.append("{} activate".format(neigh_cxt)) + + if "neighbor_type" in peer and peer["neighbor_type"] == "link-local": + config_data.append( + "{} update-source {}".format( + neigh_cxt, nh_details["links"][dest_link]["peer-interface"] + ) + ) + config_data.append( + "{} interface {}".format( + neigh_cxt, nh_details["links"][dest_link]["peer-interface"] + ) + ) + + disable_connected = peer.setdefault("disable_connected_check", False) + connect = peer.get("connecttimer", global_connect) + keep_alive = peer.setdefault("keepalivetimer", 3) + hold_down = peer.setdefault("holddowntimer", 10) + password = peer.setdefault("password", None) + no_password = peer.setdefault("no_password", None) + capability = peer.setdefault("capability", None) + max_hop_limit = peer.setdefault("ebgp_multihop", 1) + + graceful_restart = peer.setdefault("graceful-restart", None) + graceful_restart_helper = peer.setdefault("graceful-restart-helper", None) + graceful_restart_disable = peer.setdefault("graceful-restart-disable", None) + if capability: + config_data.append("{} capability {}".format(neigh_cxt, capability)) + + if update_source: + config_data.append( + "{} update-source {}".format(neigh_cxt, update_source) + ) + if disable_connected: + config_data.append( + "{} disable-connected-check".format(disable_connected) + ) + if update_source: + config_data.append( + "{} update-source {}".format(neigh_cxt, update_source) + ) + if int(keep_alive) != 60 and int(hold_down) != 180: + config_data.append( + "{} timers {} {}".format(neigh_cxt, keep_alive, hold_down) + ) + if int(connect) != 120: + config_data.append("{} timers connect {}".format(neigh_cxt, connect)) + + if graceful_restart: + config_data.append("{} graceful-restart".format(neigh_cxt)) + elif graceful_restart == False: + config_data.append("no {} graceful-restart".format(neigh_cxt)) + + if graceful_restart_helper: + config_data.append("{} graceful-restart-helper".format(neigh_cxt)) + elif graceful_restart_helper == False: + config_data.append("no {} graceful-restart-helper".format(neigh_cxt)) + + if graceful_restart_disable: + config_data.append("{} graceful-restart-disable".format(neigh_cxt)) + elif graceful_restart_disable == False: + config_data.append("no {} graceful-restart-disable".format(neigh_cxt)) + + if password: + config_data.append("{} password {}".format(neigh_cxt, password)) + + if no_password: + config_data.append("no {} password {}".format(neigh_cxt, no_password)) + + if max_hop_limit > 1: + config_data.append( + "{} ebgp-multihop {}".format(neigh_cxt, max_hop_limit) + ) + config_data.append("{} enforce-multihop".format(neigh_cxt)) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return config_data + + +def __create_bgp_unicast_address_family( + topo, input_dict, router, addr_type, add_neigh=True +): + """ + API prints bgp global config to bgp_json file. + + Parameters + ---------- + * `bgp_cfg` : BGP class variables have BGP config saved in it for + particular router, + * `local_as_no` : Local as number + * `router_id` : Router-id + * `ecmp_path` : ECMP max path + * `gr_enable` : BGP global gracefull restart config + """ + + config_data = [] + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + bgp_data = input_dict["address_family"] + neigh_data = bgp_data[addr_type]["unicast"]["neighbor"] + + for peer_name, peer_dict in deepcopy(neigh_data).items(): + for dest_link, peer in peer_dict["dest_link"].items(): + deactivate = None + activate = None + nh_details = topo[peer_name] + activate_addr_family = peer.setdefault("activate", None) + deactivate_addr_family = peer.setdefault("deactivate", None) + # Loopback interface + if "source_link" in peer and peer["source_link"] == "lo": + for destRouterLink, data in sorted(nh_details["links"].items()): + if "type" in data and data["type"] == "loopback": + if dest_link == destRouterLink: + ip_addr = ( + nh_details["links"][destRouterLink][addr_type] + .split("/")[0] + .lower() + ) + + # Physical interface + else: + # check the neighbor type if un numbered nbr, use interface. + if "neighbor_type" in peer and peer["neighbor_type"] == "unnumbered": + ip_addr = nh_details["links"][dest_link]["peer-interface"] + elif "neighbor_type" in peer and peer["neighbor_type"] == "link-local": + intf = topo[peer_name]["links"][dest_link]["interface"] + ip_addr = get_frr_ipv6_linklocal(tgen, peer_name, intf) + elif dest_link in nh_details["links"].keys(): + try: + ip_addr = nh_details["links"][dest_link][addr_type].split("/")[ + 0 + ] + except KeyError: + intf = topo[peer_name]["links"][dest_link]["interface"] + ip_addr = get_frr_ipv6_linklocal(tgen, peer_name, intf) + if ( + addr_type == "ipv4" + and bgp_data["ipv6"] + and check_address_types("ipv6") + and "ipv6" in nh_details["links"][dest_link] + ): + deactivate = nh_details["links"][dest_link]["ipv6"].split("/")[ + 0 + ] + + neigh_cxt = "neighbor {}".format(ip_addr) + config_data.append("address-family {} unicast".format(addr_type)) + + if activate_addr_family is not None: + config_data.append( + "address-family {} unicast".format(activate_addr_family) + ) + + config_data.append("{} activate".format(neigh_cxt)) + + if deactivate and activate_addr_family is None: + config_data.append("no neighbor {} activate".format(deactivate)) + + if deactivate_addr_family is not None: + config_data.append( + "address-family {} unicast".format(deactivate_addr_family) + ) + config_data.append("no {} activate".format(neigh_cxt)) + + next_hop_self = peer.setdefault("next_hop_self", None) + send_community = peer.setdefault("send_community", None) + prefix_lists = peer.setdefault("prefix_lists", {}) + route_maps = peer.setdefault("route_maps", {}) + no_send_community = peer.setdefault("no_send_community", None) + capability = peer.setdefault("capability", None) + allowas_in = peer.setdefault("allowas-in", None) + + # next-hop-self + if next_hop_self is not None: + if next_hop_self is True: + config_data.append("{} next-hop-self".format(neigh_cxt)) + else: + config_data.append("no {} next-hop-self".format(neigh_cxt)) + + # send_community + if send_community: + config_data.append("{} send-community".format(neigh_cxt)) + + # no_send_community + if no_send_community: + config_data.append( + "no {} send-community {}".format(neigh_cxt, no_send_community) + ) + + # capability_ext_nh + if capability and addr_type == "ipv6": + config_data.append("address-family ipv4 unicast") + config_data.append("{} activate".format(neigh_cxt)) + + if "allowas_in" in peer: + allow_as_in = peer["allowas_in"] + config_data.append("{} allowas-in {}".format(neigh_cxt, allow_as_in)) + + if "no_allowas_in" in peer: + allow_as_in = peer["no_allowas_in"] + config_data.append("no {} allowas-in {}".format(neigh_cxt, allow_as_in)) + + if "shutdown" in peer: + config_data.append( + "{} {} shutdown".format( + "no" if not peer["shutdown"] else "", neigh_cxt + ) + ) + + if prefix_lists: + for prefix_list in prefix_lists: + name = prefix_list.setdefault("name", {}) + direction = prefix_list.setdefault("direction", "in") + del_action = prefix_list.setdefault("delete", False) + if not name: + logger.info( + "Router %s: 'name' not present in " + "input_dict for BGP neighbor prefix lists", + router, + ) + else: + cmd = "{} prefix-list {} {}".format(neigh_cxt, name, direction) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if route_maps: + for route_map in route_maps: + name = route_map.setdefault("name", {}) + direction = route_map.setdefault("direction", "in") + del_action = route_map.setdefault("delete", False) + if not name: + logger.info( + "Router %s: 'name' not present in " + "input_dict for BGP neighbor route name", + router, + ) + else: + cmd = "{} route-map {} {}".format(neigh_cxt, name, direction) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if allowas_in: + number_occurences = allowas_in.setdefault("number_occurences", {}) + del_action = allowas_in.setdefault("delete", False) + + cmd = "{} allowas-in {}".format(neigh_cxt, number_occurences) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + return config_data + + +def modify_bgp_config_when_bgpd_down(tgen, topo, input_dict): + """ + API will save the current config to router's /etc/frr/ for BGPd + daemon(bgpd.conf file) + + Paramters + --------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : defines for which router, and which config + needs to be modified + + Usage: + ------ + # Modify graceful-restart config not to set f-bit + # and write to /etc/frr + + # Api call to delete advertised networks + input_dict_2 = { + "r5": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "advertise_networks": [ + { + "network": "101.0.20.1/32", + "no_of_network": 5, + "delete": True + } + ], + } + }, + "ipv6": { + "unicast": { + "advertise_networks": [ + { + "network": "5::1/128", + "no_of_network": 5, + "delete": True + } + ], + } + } + } + } + } + } + + result = modify_bgp_config_when_bgpd_down(tgen, topo, input_dict) + + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + try: + result = create_router_bgp( + tgen, topo, input_dict, build=False, load_config=False + ) + if result is not True: + return result + + # Copy bgp config file to /etc/frr + for dut in input_dict.keys(): + router_list = tgen.routers() + for router, rnode in router_list.items(): + if router != dut: + continue + + logger.info("Delete BGP config when BGPd is down in {}".format(router)) + # Reading the config from "rundir" and copy to /etc/frr/bgpd.conf + cmd = "cat {}/{}/{} >> /etc/frr/bgpd.conf".format( + tgen.logdir, router, FRRCFG_FILE + ) + router_list[router].run(cmd) + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +############################################# +# Verification APIs +############################################# +@retry(retry_timeout=8) +def verify_router_id(tgen, topo, input_dict, expected=True): + """ + Running command "show ip bgp json" for DUT and reading router-id + from input_dict and verifying with command output. + 1. Statically modfified router-id should take place + 2. When static router-id is deleted highest loopback should + become router-id + 3. When loopback intf is down then highest physcial intf + should become router-id + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `input_dict`: input dictionary, have details of Device Under Test, for + which user wants to test the data + * `expected` : expected results from API, by-default True + + Usage + ----- + # Verify if router-id for r1 is 12.12.12.12 + input_dict = { + "r1":{ + "router_id": "12.12.12.12" + } + # Verify that router-id for r1 is highest interface ip + input_dict = { + "routers": ["r1"] + } + result = verify_router_id(tgen, topo, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for router in input_dict.keys(): + if router not in tgen.routers(): + continue + + rnode = tgen.routers()[router] + + del_router_id = input_dict[router]["bgp"].setdefault("del_router_id", False) + + logger.info("Checking router %s router-id", router) + show_bgp_json = run_frr_cmd(rnode, "show bgp summary json", isjson=True) + router_id_out = show_bgp_json["ipv4Unicast"]["routerId"] + router_id_out = ipaddress.IPv4Address(frr_unicode(router_id_out)) + + # Once router-id is deleted, highest interface ip should become + # router-id + if del_router_id: + router_id = find_interface_with_greater_ip(topo, router) + else: + router_id = input_dict[router]["bgp"]["router_id"] + router_id = ipaddress.IPv4Address(frr_unicode(router_id)) + + if router_id == router_id_out: + logger.info("Found expected router-id %s for router %s", router_id, router) + else: + errormsg = ( + "Router-id for router:{} mismatch, expected:" + " {} but found:{}".format(router, router_id, router_id_out) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=150) +def verify_bgp_convergence(tgen, topo=None, dut=None, expected=True): + """ + API will verify if BGP is converged with in the given time frame. + Running "show bgp summary json" command and verify bgp neighbor + state is established, + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `dut`: device under test + + Usage + ----- + # To veriry is BGP is converged for all the routers used in + topology + results = verify_bgp_convergence(tgen, topo, dut="r1") + + Returns + ------- + errormsg(str) or True + """ + + if topo is None: + topo = tgen.json_topo + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + for router, rnode in tgen.routers().items(): + if "bgp" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying BGP Convergence on router %s:", router) + show_bgp_json = run_frr_cmd(rnode, "show bgp vrf all summary json", isjson=True) + # Verifying output dictionary show_bgp_json is empty or not + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + # To find neighbor ip type + bgp_data_list = topo["routers"][router]["bgp"] + + if type(bgp_data_list) is not list: + bgp_data_list = [bgp_data_list] + + for bgp_data in bgp_data_list: + if "vrf" in bgp_data: + vrf = bgp_data["vrf"] + if vrf is None: + vrf = "default" + else: + vrf = "default" + + # To find neighbor ip type + bgp_addr_type = bgp_data["address_family"] + if "l2vpn" in bgp_addr_type: + total_evpn_peer = 0 + + if "neighbor" not in bgp_addr_type["l2vpn"]["evpn"]: + continue + + bgp_neighbors = bgp_addr_type["l2vpn"]["evpn"]["neighbor"] + total_evpn_peer += len(bgp_neighbors) + + no_of_evpn_peer = 0 + for bgp_neighbor, peer_data in bgp_neighbors.items(): + for _addr_type, dest_link_dict in peer_data.items(): + data = topo["routers"][bgp_neighbor]["links"] + for dest_link in dest_link_dict.keys(): + if dest_link in data: + peer_details = peer_data[_addr_type][dest_link] + + neighbor_ip = data[dest_link][_addr_type].split("/")[0] + nh_state = None + + if ( + "ipv4Unicast" in show_bgp_json[vrf] + or "ipv6Unicast" in show_bgp_json[vrf] + ): + errormsg = ( + "[DUT: %s] VRF: %s, " + "ipv4Unicast/ipv6Unicast" + " address-family present" + " under l2vpn" % (router, vrf) + ) + return errormsg + + l2VpnEvpn_data = show_bgp_json[vrf]["l2VpnEvpn"][ + "peers" + ] + nh_state = l2VpnEvpn_data[neighbor_ip]["state"] + + if nh_state == "Established": + no_of_evpn_peer += 1 + + if no_of_evpn_peer == total_evpn_peer: + logger.info( + "[DUT: %s] VRF: %s, BGP is Converged for " "epvn peers", + router, + vrf, + ) + result = True + else: + errormsg = ( + "[DUT: %s] VRF: %s, BGP is not converged " + "for evpn peers" % (router, vrf) + ) + return errormsg + else: + total_peer = 0 + for addr_type in bgp_addr_type.keys(): + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor in bgp_neighbors: + total_peer += len(bgp_neighbors[bgp_neighbor]["dest_link"]) + + no_of_peer = 0 + for addr_type in bgp_addr_type.keys(): + if not check_address_types(addr_type): + continue + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + for dest_link in peer_data["dest_link"].keys(): + data = topo["routers"][bgp_neighbor]["links"] + if dest_link in data: + peer_details = peer_data["dest_link"][dest_link] + # for link local neighbors + if ( + "neighbor_type" in peer_details + and peer_details["neighbor_type"] == "link-local" + ): + intf = topo["routers"][bgp_neighbor]["links"][ + dest_link + ]["interface"] + neighbor_ip = get_frr_ipv6_linklocal( + tgen, bgp_neighbor, intf + ) + elif "source_link" in peer_details: + neighbor_ip = topo["routers"][bgp_neighbor][ + "links" + ][peer_details["source_link"]][addr_type].split( + "/" + )[ + 0 + ] + elif ( + "neighbor_type" in peer_details + and peer_details["neighbor_type"] == "unnumbered" + ): + neighbor_ip = data[dest_link]["peer-interface"] + else: + neighbor_ip = data[dest_link][addr_type].split("/")[ + 0 + ] + nh_state = None + neighbor_ip = neighbor_ip.lower() + if addr_type == "ipv4": + ipv4_data = show_bgp_json[vrf]["ipv4Unicast"][ + "peers" + ] + nh_state = ipv4_data[neighbor_ip]["state"] + else: + ipv6_data = show_bgp_json[vrf]["ipv6Unicast"][ + "peers" + ] + if neighbor_ip in ipv6_data: + nh_state = ipv6_data[neighbor_ip]["state"] + + if nh_state == "Established": + no_of_peer += 1 + + if no_of_peer == total_peer and no_of_peer > 0: + logger.info("[DUT: %s] VRF: %s, BGP is Converged", router, vrf) + result = True + else: + errormsg = "[DUT: %s] VRF: %s, BGP is not converged" % (router, vrf) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=16) +def verify_bgp_community( + tgen, + addr_type, + router, + network, + input_dict=None, + vrf=None, + bestpath=False, + expected=True, +): + """ + API to veiryf BGP large community is attached in route for any given + DUT by running "show bgp ipv4/6 {route address} json" command. + + Parameters + ---------- + * `tgen`: topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test + * `network`: network for which set criteria needs to be verified + * `input_dict`: having details like - for which router, community and + values needs to be verified + * `vrf`: VRF name + * `bestpath`: To check best path cli + * `expected` : expected results from API, by-default True + + Usage + ----- + networks = ["200.50.2.0/32"] + input_dict = { + "largeCommunity": "2:1:1 2:2:2 2:3:3 2:4:4 2:5:5" + } + result = verify_bgp_community(tgen, "ipv4", dut, network, input_dict=None) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: verify_bgp_community()") + if router not in tgen.routers(): + return False + + rnode = tgen.routers()[router] + + logger.info( + "Verifying BGP community attributes on dut %s: for %s " "network %s", + router, + addr_type, + network, + ) + + command = "show bgp" + + for net in network: + if vrf: + cmd = "{} vrf {} {} {} json".format(command, vrf, addr_type, net) + elif bestpath: + cmd = "{} {} {} bestpath json".format(command, addr_type, net) + else: + cmd = "{} {} {} json".format(command, addr_type, net) + + show_bgp_json = run_frr_cmd(rnode, cmd, isjson=True) + if "paths" not in show_bgp_json: + return "Prefix {} not found in BGP table of router: {}".format(net, router) + + as_paths = show_bgp_json["paths"] + found = False + for i in range(len(as_paths)): + if ( + "largeCommunity" in show_bgp_json["paths"][i] + or "community" in show_bgp_json["paths"][i] + ): + found = True + logger.info( + "Large Community attribute is found for route:" " %s in router: %s", + net, + router, + ) + if input_dict is not None: + for criteria, comm_val in input_dict.items(): + show_val = show_bgp_json["paths"][i][criteria]["string"] + if comm_val == show_val: + logger.info( + "Verifying BGP %s for prefix: %s" + " in router: %s, found expected" + " value: %s", + criteria, + net, + router, + comm_val, + ) + else: + errormsg = ( + "Failed: Verifying BGP attribute" + " {} for route: {} in router: {}" + ", expected value: {} but found" + ": {}".format(criteria, net, router, comm_val, show_val) + ) + return errormsg + + if not found: + errormsg = ( + "Large Community attribute is not found for route: " + "{} in router: {} ".format(net, router) + ) + return errormsg + + logger.debug("Exiting lib API: verify_bgp_community()") + return True + + +def modify_as_number(tgen, topo, input_dict): + """ + API reads local_as and remote_as from user defined input_dict and + modify router"s ASNs accordingly. Router"s config is modified and + recent/changed config is loadeded to router. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : defines for which router ASNs needs to be modified + + Usage + ----- + To modify ASNs for router r1 + input_dict = { + "r1": { + "bgp": { + "local_as": 131079 + } + } + result = modify_as_number(tgen, topo, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + try: + + new_topo = deepcopy(topo["routers"]) + router_dict = {} + for router in input_dict.keys(): + # Remove bgp configuration + + router_dict.update({router: {"bgp": {"delete": True}}}) + try: + new_topo[router]["bgp"]["local_as"] = input_dict[router]["bgp"][ + "local_as" + ] + except TypeError: + new_topo[router]["bgp"][0]["local_as"] = input_dict[router]["bgp"][ + "local_as" + ] + logger.info("Removing bgp configuration") + create_router_bgp(tgen, topo, router_dict) + + logger.info("Applying modified bgp configuration") + result = create_router_bgp(tgen, new_topo) + if result is not True: + result = "Error applying new AS number config" + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=8) +def verify_as_numbers(tgen, topo, input_dict, expected=True): + """ + This API is to verify AS numbers for given DUT by running + "show ip bgp neighbor json" command. Local AS and Remote AS + will ve verified with input_dict data and command output. + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type, ipv4/ipv6 + * `input_dict`: defines - for which router, AS numbers needs to be verified + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "r1": { + "bgp": { + "local_as": 131079 + } + } + } + result = verify_as_numbers(tgen, topo, addr_type, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for router in input_dict.keys(): + if router not in tgen.routers(): + continue + + rnode = tgen.routers()[router] + + logger.info("Verifying AS numbers for dut %s:", router) + + show_ip_bgp_neighbor_json = run_frr_cmd( + rnode, "show ip bgp neighbor json", isjson=True + ) + local_as = input_dict[router]["bgp"]["local_as"] + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + + for addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + remote_as = input_dict[bgp_neighbor]["bgp"]["local_as"] + for dest_link, peer_dict in peer_data["dest_link"].items(): + neighbor_ip = None + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + neigh_data = show_ip_bgp_neighbor_json[neighbor_ip] + # Verify Local AS for router + if neigh_data["localAs"] != local_as: + errormsg = ( + "Failed: Verify local_as for dut {}," + " found: {} but expected: {}".format( + router, neigh_data["localAs"], local_as + ) + ) + return errormsg + else: + logger.info( + "Verified local_as for dut %s, found" " expected: %s", + router, + local_as, + ) + + # Verify Remote AS for neighbor + if neigh_data["remoteAs"] != remote_as: + errormsg = ( + "Failed: Verify remote_as for dut " + "{}'s neighbor {}, found: {} but " + "expected: {}".format( + router, bgp_neighbor, neigh_data["remoteAs"], remote_as + ) + ) + return errormsg + else: + logger.info( + "Verified remote_as for dut %s's " + "neighbor %s, found expected: %s", + router, + bgp_neighbor, + remote_as, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=150) +def verify_bgp_convergence_from_running_config(tgen, dut=None, expected=True): + """ + API to verify BGP convergence b/w loopback and physical interface. + This API would be used when routers have BGP neighborship is loopback + to physical or vice-versa + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `expected` : expected results from API, by-default True + + Usage + ----- + results = verify_bgp_convergence_bw_lo_and_phy_intf(tgen, topo, + dut="r1") + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if dut is not None and dut != router: + continue + + logger.info("Verifying BGP Convergence on router %s:", router) + show_bgp_json = run_frr_cmd(rnode, "show bgp vrf all summary json", isjson=True) + # Verifying output dictionary show_bgp_json is empty or not + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + for vrf, addr_family_data in show_bgp_json.items(): + for address_family, neighborship_data in addr_family_data.items(): + total_peer = 0 + no_of_peer = 0 + + total_peer = len(neighborship_data["peers"].keys()) + + for peer, peer_data in neighborship_data["peers"].items(): + if peer_data["state"] == "Established": + no_of_peer += 1 + + if total_peer != no_of_peer: + errormsg = ( + "[DUT: %s] VRF: %s, BGP is not converged" + " for peer: %s" % (router, vrf, peer) + ) + return errormsg + + logger.info("[DUT: %s]: vrf: %s, BGP is Converged", router, vrf) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +def clear_bgp(tgen, addr_type, router, vrf=None, neighbor=None): + """ + This API is to clear bgp neighborship by running + clear ip bgp */clear bgp ipv6 * command, + + Parameters + ---------- + * `tgen`: topogen object + * `addr_type`: ip type ipv4/ipv6 + * `router`: device under test + * `vrf`: vrf name + * `neighbor`: Neighbor for which bgp needs to be cleared + + Usage + ----- + clear_bgp(tgen, addr_type, "r1") + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if router not in tgen.routers(): + return False + + rnode = tgen.routers()[router] + + if vrf: + if type(vrf) is not list: + vrf = [vrf] + + # Clearing BGP + logger.info("Clearing BGP neighborship for router %s..", router) + if addr_type == "ipv4": + if vrf: + for _vrf in vrf: + run_frr_cmd(rnode, "clear ip bgp vrf {} *".format(_vrf)) + elif neighbor: + run_frr_cmd(rnode, "clear bgp ipv4 {}".format(neighbor)) + else: + run_frr_cmd(rnode, "clear ip bgp *") + elif addr_type == "ipv6": + if vrf: + for _vrf in vrf: + run_frr_cmd(rnode, "clear bgp vrf {} ipv6 *".format(_vrf)) + elif neighbor: + run_frr_cmd(rnode, "clear bgp ipv6 {}".format(neighbor)) + else: + run_frr_cmd(rnode, "clear bgp ipv6 *") + else: + run_frr_cmd(rnode, "clear bgp *") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + +def clear_bgp_and_verify(tgen, topo, router): + """ + This API is to clear bgp neighborship and verify bgp neighborship + is coming up(BGP is converged) usinf "show bgp summary json" command + and also verifying for all bgp neighbors uptime before and after + clear bgp sessions is different as the uptime must be changed once + bgp sessions are cleared using "clear ip bgp */clear bgp ipv6 *" cmd. + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `router`: device under test + + Usage + ----- + result = clear_bgp_and_verify(tgen, topo, addr_type, dut) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if router not in tgen.routers(): + return False + + rnode = tgen.routers()[router] + + peer_uptime_before_clear_bgp = {} + sleeptime = 3 + + # Verifying BGP convergence before bgp clear command + for retry in range(50): + # Waiting for BGP to converge + logger.info( + "Waiting for %s sec for BGP to converge on router" " %s...", + sleeptime, + router, + ) + sleep(sleeptime) + + show_bgp_json = run_frr_cmd(rnode, "show bgp summary json", isjson=True) + # Verifying output dictionary show_bgp_json is empty or not + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + # To find neighbor ip type + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + total_peer = 0 + for addr_type in bgp_addr_type.keys(): + + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor in bgp_neighbors: + total_peer += len(bgp_neighbors[bgp_neighbor]["dest_link"]) + + no_of_peer = 0 + for addr_type in bgp_addr_type: + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + if addr_type == "ipv4": + ipv4_data = show_bgp_json["ipv4Unicast"]["peers"] + nh_state = ipv4_data[neighbor_ip]["state"] + + # Peer up time dictionary + peer_uptime_before_clear_bgp[bgp_neighbor] = ipv4_data[ + neighbor_ip + ]["peerUptimeEstablishedEpoch"] + else: + ipv6_data = show_bgp_json["ipv6Unicast"]["peers"] + nh_state = ipv6_data[neighbor_ip]["state"] + + # Peer up time dictionary + peer_uptime_before_clear_bgp[bgp_neighbor] = ipv6_data[ + neighbor_ip + ]["peerUptimeEstablishedEpoch"] + + if nh_state == "Established": + no_of_peer += 1 + + if no_of_peer == total_peer: + logger.info("BGP is Converged for router %s before bgp" " clear", router) + break + else: + logger.info( + "BGP is not yet Converged for router %s " "before bgp clear", router + ) + else: + errormsg = ( + "TIMEOUT!! BGP is not converged in {} seconds for" + " router {}".format(retry * sleeptime, router) + ) + return errormsg + + # Clearing BGP + logger.info("Clearing BGP neighborship for router %s..", router) + for addr_type in bgp_addr_type.keys(): + if addr_type == "ipv4": + run_frr_cmd(rnode, "clear ip bgp *") + elif addr_type == "ipv6": + run_frr_cmd(rnode, "clear bgp ipv6 *") + + peer_uptime_after_clear_bgp = {} + # Verifying BGP convergence after bgp clear command + for retry in range(50): + + # Waiting for BGP to converge + logger.info( + "Waiting for %s sec for BGP to converge on router" " %s...", + sleeptime, + router, + ) + sleep(sleeptime) + + show_bgp_json = run_frr_cmd(rnode, "show bgp summary json", isjson=True) + # Verifying output dictionary show_bgp_json is empty or not + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + # To find neighbor ip type + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + total_peer = 0 + for addr_type in bgp_addr_type.keys(): + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor in bgp_neighbors: + total_peer += len(bgp_neighbors[bgp_neighbor]["dest_link"]) + + no_of_peer = 0 + for addr_type in bgp_addr_type: + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + if addr_type == "ipv4": + ipv4_data = show_bgp_json["ipv4Unicast"]["peers"] + nh_state = ipv4_data[neighbor_ip]["state"] + peer_uptime_after_clear_bgp[bgp_neighbor] = ipv4_data[ + neighbor_ip + ]["peerUptimeEstablishedEpoch"] + else: + ipv6_data = show_bgp_json["ipv6Unicast"]["peers"] + nh_state = ipv6_data[neighbor_ip]["state"] + # Peer up time dictionary + peer_uptime_after_clear_bgp[bgp_neighbor] = ipv6_data[ + neighbor_ip + ]["peerUptimeEstablishedEpoch"] + + if nh_state == "Established": + no_of_peer += 1 + + if no_of_peer == total_peer: + logger.info("BGP is Converged for router %s after bgp clear", router) + break + else: + logger.info( + "BGP is not yet Converged for router %s after" " bgp clear", router + ) + else: + errormsg = ( + "TIMEOUT!! BGP is not converged in {} seconds for" + " router {}".format(retry * sleeptime, router) + ) + return errormsg + + # Comparing peerUptimeEstablishedEpoch dictionaries + if peer_uptime_before_clear_bgp != peer_uptime_after_clear_bgp: + logger.info("BGP neighborship is reset after clear BGP on router %s", router) + else: + errormsg = ( + "BGP neighborship is not reset after clear bgp on router" + " {}".format(router) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def verify_bgp_timers_and_functionality(tgen, topo, input_dict): + """ + To verify BGP timer config, execute "show ip bgp neighbor json" command + and verify bgp timers with input_dict data. + To veirfy bgp timers functonality, shutting down peer interface + and verify BGP neighborship status. + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type`: ip type, ipv4/ipv6 + * `input_dict`: defines for which router, bgp timers needs to be verified + + Usage: + # To verify BGP timers for neighbor r2 of router r1 + input_dict = { + "r1": { + "bgp": { + "bgp_neighbors":{ + "r2":{ + "keepalivetimer": 5, + "holddowntimer": 15, + }}}}} + result = verify_bgp_timers_and_functionality(tgen, topo, "ipv4", + input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + sleep(5) + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + + rnode = router_list[router] + + logger.info("Verifying bgp timers functionality, DUT is %s:", router) + + show_ip_bgp_neighbor_json = run_frr_cmd( + rnode, "show ip bgp neighbor json", isjson=True + ) + + bgp_addr_type = input_dict[router]["bgp"]["address_family"] + + for addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + for bgp_neighbor, peer_data in bgp_neighbors.items(): + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + keepalivetimer = peer_dict["keepalivetimer"] + holddowntimer = peer_dict["holddowntimer"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + neighbor_intf = data[dest_link]["interface"] + + # Verify HoldDownTimer for neighbor + bgpHoldTimeMsecs = show_ip_bgp_neighbor_json[neighbor_ip][ + "bgpTimerHoldTimeMsecs" + ] + if bgpHoldTimeMsecs != holddowntimer * 1000: + errormsg = ( + "Verifying holddowntimer for bgp " + "neighbor {} under dut {}, found: {} " + "but expected: {}".format( + neighbor_ip, + router, + bgpHoldTimeMsecs, + holddowntimer * 1000, + ) + ) + return errormsg + + # Verify KeepAliveTimer for neighbor + bgpKeepAliveTimeMsecs = show_ip_bgp_neighbor_json[neighbor_ip][ + "bgpTimerKeepAliveIntervalMsecs" + ] + if bgpKeepAliveTimeMsecs != keepalivetimer * 1000: + errormsg = ( + "Verifying keepalivetimer for bgp " + "neighbor {} under dut {}, found: {} " + "but expected: {}".format( + neighbor_ip, + router, + bgpKeepAliveTimeMsecs, + keepalivetimer * 1000, + ) + ) + return errormsg + + #################### + # Shutting down peer interface after keepalive time and + # after some time bringing up peer interface. + # verifying BGP neighborship in (hold down-keep alive) + # time, it should not go down + #################### + + # Wait till keep alive time + logger.info("=" * 20) + logger.info("Scenario 1:") + logger.info( + "Shutdown and bring up peer interface: %s " + "in keep alive time : %s sec and verify " + " BGP neighborship is intact in %s sec ", + neighbor_intf, + keepalivetimer, + (holddowntimer - keepalivetimer), + ) + logger.info("=" * 20) + logger.info("Waiting for %s sec..", keepalivetimer) + sleep(keepalivetimer) + + # Shutting down peer ineterface + logger.info( + "Shutting down interface %s on router %s", + neighbor_intf, + bgp_neighbor, + ) + topotest.interface_set_status( + router_list[bgp_neighbor], neighbor_intf, ifaceaction=False + ) + + # Bringing up peer interface + sleep(5) + logger.info( + "Bringing up interface %s on router %s..", + neighbor_intf, + bgp_neighbor, + ) + topotest.interface_set_status( + router_list[bgp_neighbor], neighbor_intf, ifaceaction=True + ) + + # Verifying BGP neighborship is intact in + # (holddown - keepalive) time + for timer in range( + keepalivetimer, holddowntimer, int(holddowntimer / 3) + ): + logger.info("Waiting for %s sec..", keepalivetimer) + sleep(keepalivetimer) + sleep(2) + show_bgp_json = run_frr_cmd( + rnode, "show bgp summary json", isjson=True + ) + + if addr_type == "ipv4": + ipv4_data = show_bgp_json["ipv4Unicast"]["peers"] + nh_state = ipv4_data[neighbor_ip]["state"] + else: + ipv6_data = show_bgp_json["ipv6Unicast"]["peers"] + nh_state = ipv6_data[neighbor_ip]["state"] + + if timer == (holddowntimer - keepalivetimer): + if nh_state != "Established": + errormsg = ( + "BGP neighborship has not gone " + "down in {} sec for neighbor {}".format( + timer, bgp_neighbor + ) + ) + return errormsg + else: + logger.info( + "BGP neighborship is intact in %s" + " sec for neighbor %s", + timer, + bgp_neighbor, + ) + + #################### + # Shutting down peer interface and verifying that BGP + # neighborship is going down in holddown time + #################### + logger.info("=" * 20) + logger.info("Scenario 2:") + logger.info( + "Shutdown peer interface: %s and verify BGP" + " neighborship has gone down in hold down " + "time %s sec", + neighbor_intf, + holddowntimer, + ) + logger.info("=" * 20) + + logger.info( + "Shutting down interface %s on router %s..", + neighbor_intf, + bgp_neighbor, + ) + topotest.interface_set_status( + router_list[bgp_neighbor], neighbor_intf, ifaceaction=False + ) + + # Verifying BGP neighborship is going down in holddown time + for timer in range( + keepalivetimer, + (holddowntimer + keepalivetimer), + int(holddowntimer / 3), + ): + logger.info("Waiting for %s sec..", keepalivetimer) + sleep(keepalivetimer) + sleep(2) + show_bgp_json = run_frr_cmd( + rnode, "show bgp summary json", isjson=True + ) + + if addr_type == "ipv4": + ipv4_data = show_bgp_json["ipv4Unicast"]["peers"] + nh_state = ipv4_data[neighbor_ip]["state"] + else: + ipv6_data = show_bgp_json["ipv6Unicast"]["peers"] + nh_state = ipv6_data[neighbor_ip]["state"] + + if timer == holddowntimer: + if nh_state == "Established": + errormsg = ( + "BGP neighborship has not gone " + "down in {} sec for neighbor {}".format( + timer, bgp_neighbor + ) + ) + return errormsg + else: + logger.info( + "BGP neighborship has gone down in" + " %s sec for neighbor %s", + timer, + bgp_neighbor, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=16) +def verify_bgp_attributes( + tgen, + addr_type, + dut, + static_routes, + rmap_name=None, + input_dict=None, + seq_id=None, + vrf=None, + nexthop=None, + expected=True, +): + """ + API will verify BGP attributes set by Route-map for given prefix and + DUT. it will run "show bgp ipv4/ipv6 {prefix_address} json" command + in DUT to verify BGP attributes set by route-map, Set attributes + values will be read from input_dict and verified with command output. + + * `tgen`: topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test + * `static_routes`: Static Routes for which BGP set attributes needs to be + verified + * `rmap_name`: route map name for which set criteria needs to be verified + * `input_dict`: defines for which router, AS numbers needs + * `seq_id`: sequence number of rmap, default is None + * `expected` : expected results from API, by-default True + + Usage + ----- + # To verify BGP attribute "localpref" set to 150 and "med" set to 30 + for prefix 10.0.20.1/32 in router r3. + input_dict = { + "r3": { + "route_maps": { + "rmap_match_pf_list1": [ + { + "action": "PERMIT", + "match": {"prefix_list": "pf_list_1"}, + "set": {"localpref": 150, "med": 30} + } + ], + }, + "as_path": "500 400" + } + } + static_routes (list) = ["10.0.20.1/32"] + + + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + logger.info("Verifying BGP set attributes for dut {}:".format(router)) + + for static_route in static_routes: + if vrf: + cmd = "show bgp vrf {} {} {} json".format(vrf, addr_type, static_route) + else: + cmd = "show bgp {} {} json".format(addr_type, static_route) + show_bgp_json = run_frr_cmd(rnode, cmd, isjson=True) + + dict_to_test = [] + tmp_list = [] + + dict_list = list(input_dict.values())[0] + + if "route_maps" in dict_list: + for rmap_router in input_dict.keys(): + for rmap, values in input_dict[rmap_router]["route_maps"].items(): + if rmap == rmap_name: + dict_to_test = values + for rmap_dict in values: + if seq_id is not None: + if type(seq_id) is not list: + seq_id = [seq_id] + + if "seq_id" in rmap_dict: + rmap_seq_id = rmap_dict["seq_id"] + for _seq_id in seq_id: + if _seq_id == rmap_seq_id: + tmp_list.append(rmap_dict) + if tmp_list: + dict_to_test = tmp_list + + value = None + for rmap_dict in dict_to_test: + if "set" in rmap_dict: + for criteria in rmap_dict["set"].keys(): + found = False + for path in show_bgp_json["paths"]: + if criteria not in path: + continue + + if criteria == "aspath": + value = path[criteria]["string"] + else: + value = path[criteria] + + if rmap_dict["set"][criteria] == value: + found = True + logger.info( + "Verifying BGP " + "attribute {} for" + " route: {} in " + "router: {}, found" + " expected value:" + " {}".format( + criteria, + static_route, + dut, + value, + ) + ) + break + + if not found: + errormsg = ( + "Failed: Verifying BGP " + "attribute {} for route:" + " {} in router: {}, " + " expected value: {} but" + " found: {}".format( + criteria, + static_route, + dut, + rmap_dict["set"][criteria], + value, + ) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=8) +def verify_best_path_as_per_bgp_attribute( + tgen, addr_type, router, input_dict, attribute, expected=True +): + """ + API is to verify best path according to BGP attributes for given routes. + "show bgp ipv4/6 json" command will be run and verify best path according + to shortest as-path, highest local-preference and med, lowest weight and + route origin IGP>EGP>INCOMPLETE. + Parameters + ---------- + * `tgen` : topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `tgen` : topogen object + * `attribute` : calculate best path using this attribute + * `input_dict`: defines different routes to calculate for which route + best path is selected + * `expected` : expected results from API, by-default True + + Usage + ----- + # To verify best path for routes 200.50.2.0/32 and 200.60.2.0/32 from + router r7 to router r1(DUT) as per shortest as-path attribute + input_dict = { + "r7": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "advertise_networks": [ + { + "network": "200.50.2.0/32" + }, + { + "network": "200.60.2.0/32" + } + ] + } + } + } + } + } + } + attribute = "locPrf" + result = verify_best_path_as_per_bgp_attribute(tgen, "ipv4", dut, \ + input_dict, attribute) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if router not in tgen.routers(): + return False + + rnode = tgen.routers()[router] + + # Verifying show bgp json + command = "show bgp" + + sleep(2) + logger.info("Verifying router %s RIB for best path:", router) + + static_route = False + advertise_network = False + for route_val in input_dict.values(): + if "static_routes" in route_val: + static_route = True + networks = route_val["static_routes"] + else: + advertise_network = True + net_data = route_val["bgp"]["address_family"][addr_type]["unicast"] + networks = net_data["advertise_networks"] + + for network in networks: + _network = network["network"] + no_of_ip = network.setdefault("no_of_ip", 1) + vrf = network.setdefault("vrf", None) + + if vrf: + cmd = "{} vrf {}".format(command, vrf) + else: + cmd = command + + cmd = "{} {}".format(cmd, addr_type) + cmd = "{} json".format(cmd) + sh_ip_bgp_json = run_frr_cmd(rnode, cmd, isjson=True) + + routes = generate_ips(_network, no_of_ip) + for route in routes: + route = str(ipaddress.ip_network(frr_unicode(route))) + + if route in sh_ip_bgp_json["routes"]: + route_attributes = sh_ip_bgp_json["routes"][route] + _next_hop = None + compare = None + attribute_dict = {} + for route_attribute in route_attributes: + next_hops = route_attribute["nexthops"] + for next_hop in next_hops: + next_hop_ip = next_hop["ip"] + attribute_dict[next_hop_ip] = route_attribute[attribute] + + # AS_PATH attribute + if attribute == "path": + # Find next_hop for the route have minimum as_path + _next_hop = min( + attribute_dict, key=lambda x: len(set(attribute_dict[x])) + ) + compare = "SHORTEST" + + # LOCAL_PREF attribute + elif attribute == "locPrf": + # Find next_hop for the route have highest local preference + _next_hop = max( + attribute_dict, key=(lambda k: attribute_dict[k]) + ) + compare = "HIGHEST" + + # WEIGHT attribute + elif attribute == "weight": + # Find next_hop for the route have highest weight + _next_hop = max( + attribute_dict, key=(lambda k: attribute_dict[k]) + ) + compare = "HIGHEST" + + # ORIGIN attribute + elif attribute == "origin": + # Find next_hop for the route have IGP as origin, - + # - rule is IGP>EGP>INCOMPLETE + _next_hop = [ + key + for (key, value) in attribute_dict.items() + if value == "IGP" + ][0] + compare = "" + + # MED attribute + elif attribute == "metric": + # Find next_hop for the route have LOWEST MED + _next_hop = min( + attribute_dict, key=(lambda k: attribute_dict[k]) + ) + compare = "LOWEST" + + # Show ip route + if addr_type == "ipv4": + command_1 = "show ip route" + else: + command_1 = "show ipv6 route" + + if vrf: + cmd = "{} vrf {} json".format(command_1, vrf) + else: + cmd = "{} json".format(command_1) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if not bool(rib_routes_json): + errormsg = "No route found in RIB of router {}..".format(router) + return errormsg + + st_found = False + nh_found = False + # Find best is installed in RIB + if route in rib_routes_json: + st_found = True + # Verify next_hop in rib_routes_json + if ( + rib_routes_json[route][0]["nexthops"][0]["ip"] + in attribute_dict + ): + nh_found = True + else: + errormsg = ( + "Incorrect Nexthop for BGP route {} in " + "RIB of router {}, Expected: {}, Found:" + " {}\n".format( + route, + router, + rib_routes_json[route][0]["nexthops"][0]["ip"], + _next_hop, + ) + ) + return errormsg + + if st_found and nh_found: + logger.info( + "Best path for prefix: %s with next_hop: %s is " + "installed according to %s %s: (%s) in RIB of " + "router %s", + route, + _next_hop, + compare, + attribute, + attribute_dict[_next_hop], + router, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=10) +def verify_best_path_as_per_admin_distance( + tgen, addr_type, router, input_dict, attribute, expected=True, vrf=None +): + """ + API is to verify best path according to admin distance for given + route. "show ip/ipv6 route json" command will be run and verify + best path accoring to shortest admin distanc. + + Parameters + ---------- + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test + * `tgen` : topogen object + * `attribute` : calculate best path using admin distance + * `input_dict`: defines different routes with different admin distance + to calculate for which route best path is selected + * `expected` : expected results from API, by-default True + * `vrf`: Pass vrf name check for perticular vrf. + + Usage + ----- + # To verify best path for route 200.50.2.0/32 from router r2 to + router r1(DUT) as per shortest admin distance which is 60. + input_dict = { + "r2": { + "static_routes": [{"network": "200.50.2.0/32", \ + "admin_distance": 80, "next_hop": "10.0.0.14"}, + {"network": "200.50.2.0/32", \ + "admin_distance": 60, "next_hop": "10.0.0.18"}] + }} + attribute = "locPrf" + result = verify_best_path_as_per_admin_distance(tgen, "ipv4", dut, \ + input_dict, attribute): + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + router_list = tgen.routers() + if router not in router_list: + return False + + rnode = tgen.routers()[router] + + sleep(5) + logger.info("Verifying router %s RIB for best path:", router) + + # Show ip route cmd + if addr_type == "ipv4": + command = "show ip route" + else: + command = "show ipv6 route" + + if vrf: + command = "{} vrf {} json".format(command, vrf) + else: + command = "{} json".format(command) + + for routes_from_router in input_dict.keys(): + sh_ip_route_json = router_list[routes_from_router].vtysh_cmd( + command, isjson=True + ) + networks = input_dict[routes_from_router]["static_routes"] + for network in networks: + route = network["network"] + + route_attributes = sh_ip_route_json[route] + _next_hop = None + compare = None + attribute_dict = {} + for route_attribute in route_attributes: + next_hops = route_attribute["nexthops"] + for next_hop in next_hops: + next_hop_ip = next_hop["ip"] + attribute_dict[next_hop_ip] = route_attribute["distance"] + + # Find next_hop for the route have LOWEST Admin Distance + _next_hop = min(attribute_dict, key=(lambda k: attribute_dict[k])) + compare = "LOWEST" + + # Show ip route + rib_routes_json = run_frr_cmd(rnode, command, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if not bool(rib_routes_json): + errormsg = "No route found in RIB of router {}..".format(router) + return errormsg + + st_found = False + nh_found = False + # Find best is installed in RIB + if route in rib_routes_json: + st_found = True + # Verify next_hop in rib_routes_json + if rib_routes_json[route][0]["nexthops"][0]["ip"] == _next_hop: + nh_found = True + else: + errormsg = ( + "Nexthop {} is Missing for BGP route {}" + " in RIB of router {}\n".format(_next_hop, route, router) + ) + return errormsg + + if st_found and nh_found: + logger.info( + "Best path for prefix: %s is installed according" + " to %s %s: (%s) in RIB of router %s", + route, + compare, + attribute, + attribute_dict[_next_hop], + router, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=30) +def verify_bgp_rib( + tgen, + addr_type, + dut, + input_dict, + next_hop=None, + aspath=None, + multi_nh=None, + expected=True, +): + """ + This API is to verify whether bgp rib has any + matching route for a nexthop. + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: input dut router name + * `addr_type` : ip type ipv4/ipv6 + * `input_dict` : input dict, has details of static routes + * `next_hop`[optional]: next_hop which needs to be verified, + default = static + * 'aspath'[optional]: aspath which needs to be verified + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = 'r1' + next_hop = "192.168.1.10" + input_dict = topo['routers'] + aspath = "100 200 300" + result = verify_bgp_rib(tgen, addr_type, dut, tgen, input_dict, + next_hop, aspath) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: verify_bgp_rib()") + + router_list = tgen.routers() + additional_nexthops_in_required_nhs = [] + list1 = [] + list2 = [] + found_hops = [] + for routerInput in input_dict.keys(): + for router, rnode in router_list.items(): + if router != dut: + continue + + # Verifying RIB routes + command = "show bgp" + + # Static routes + sleep(2) + logger.info("Checking router {} BGP RIB:".format(dut)) + + if "static_routes" in input_dict[routerInput]: + static_routes = input_dict[routerInput]["static_routes"] + + for static_route in static_routes: + found_routes = [] + missing_routes = [] + st_found = False + nh_found = False + + vrf = static_route.setdefault("vrf", None) + community = static_route.setdefault("community", None) + largeCommunity = static_route.setdefault("largeCommunity", None) + + if vrf: + cmd = "{} vrf {} {}".format(command, vrf, addr_type) + + if community: + cmd = "{} community {}".format(cmd, community) + + if largeCommunity: + cmd = "{} large-community {}".format(cmd, largeCommunity) + else: + cmd = "{} {}".format(command, addr_type) + + cmd = "{} json".format(cmd) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) == False: + errormsg = "No route found in rib of router {}..".format(router) + return errormsg + + network = static_route["network"] + + if "no_of_ip" in static_route: + no_of_ip = static_route["no_of_ip"] + else: + no_of_ip = 1 + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_ip) + + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt))) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json["routes"]: + st_found = True + found_routes.append(st_rt) + + if next_hop and multi_nh and st_found: + if type(next_hop) is not list: + next_hop = [next_hop] + list1 = next_hop + + for mnh in range( + 0, len(rib_routes_json["routes"][st_rt]) + ): + found_hops.append( + [ + rib_r["ip"] + for rib_r in rib_routes_json["routes"][ + st_rt + ][mnh]["nexthops"] + ] + ) + for mnh in found_hops: + for each_nh_in_multipath in mnh: + list2.append(each_nh_in_multipath) + if found_hops[0]: + missing_list_of_nexthops = set(list2).difference( + list1 + ) + additional_nexthops_in_required_nhs = set( + list1 + ).difference(list2) + + if list2: + if additional_nexthops_in_required_nhs: + logger.info( + "Missing nexthop %s for route" + " %s in RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + else: + nh_found = True + + elif next_hop and multi_nh is None: + if type(next_hop) is not list: + next_hop = [next_hop] + list1 = next_hop + found_hops = [ + rib_r["ip"] + for rib_r in rib_routes_json["routes"][st_rt][0][ + "nexthops" + ] + ] + list2 = found_hops + missing_list_of_nexthops = set(list2).difference(list1) + additional_nexthops_in_required_nhs = set( + list1 + ).difference(list2) + + if list2: + if additional_nexthops_in_required_nhs: + logger.info( + "Missing nexthop %s for route" + " %s in RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is Missing for " + "route {} in RIB of router {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + + if aspath: + found_paths = rib_routes_json["routes"][st_rt][0][ + "path" + ] + if aspath == found_paths: + aspath_found = True + logger.info( + "Found AS path {} for route" + " {} in RIB of router " + "{}\n".format(aspath, st_rt, dut) + ) + else: + errormsg = ( + "AS Path {} is missing for route" + "for route {} in RIB of router {}\n".format( + aspath, st_rt, dut + ) + ) + return errormsg + + else: + missing_routes.append(st_rt) + + if nh_found: + logger.info( + "Found next_hop {} for all bgp" + " routes in RIB of" + " router {}\n".format(next_hop, router) + ) + + if len(missing_routes) > 0: + errormsg = ( + "Missing route in RIB of router {}, " + "routes: {}\n".format(dut, missing_routes) + ) + return errormsg + + if found_routes: + logger.info( + "Verified routes in router {} BGP RIB, " + "found routes are: {} \n".format(dut, found_routes) + ) + continue + + if "bgp" not in input_dict[routerInput]: + continue + + # Advertise networks + bgp_data_list = input_dict[routerInput]["bgp"] + + if type(bgp_data_list) is not list: + bgp_data_list = [bgp_data_list] + + for bgp_data in bgp_data_list: + vrf_id = bgp_data.setdefault("vrf", None) + if vrf_id: + cmd = "{} vrf {} {}".format(command, vrf_id, addr_type) + else: + cmd = "{} {}".format(command, addr_type) + + cmd = "{} json".format(cmd) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) == False: + errormsg = "No route found in rib of router {}..".format(router) + return errormsg + + bgp_net_advertise = bgp_data["address_family"][addr_type]["unicast"] + advertise_network = bgp_net_advertise.setdefault( + "advertise_networks", [] + ) + + for advertise_network_dict in advertise_network: + found_routes = [] + missing_routes = [] + found = False + + network = advertise_network_dict["network"] + + if "no_of_network" in advertise_network_dict: + no_of_network = advertise_network_dict["no_of_network"] + else: + no_of_network = 1 + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_network) + + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt))) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json["routes"]: + found = True + found_routes.append(st_rt) + else: + found = False + missing_routes.append(st_rt) + + if len(missing_routes) > 0: + errormsg = ( + "Missing route in BGP RIB of router {}," + " are: {}\n".format(dut, missing_routes) + ) + return errormsg + + if found_routes: + logger.info( + "Verified routes in router {} BGP RIB, found " + "routes are: {}\n".format(dut, found_routes) + ) + + logger.debug("Exiting lib API: verify_bgp_rib()") + return True + + +@retry(retry_timeout=10) +def verify_graceful_restart( + tgen, topo, addr_type, input_dict, dut, peer, expected=True +): + """ + This API is to verify verify_graceful_restart configuration of DUT and + cross verify the same from the peer bgp routerrouter. + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `input_dict`: input dictionary, have details of Device Under Test, for + which user wants to test the data + * `dut`: input dut router name + * `peer`: input peer router name + * `expected` : expected results from API, by-default True + + Usage + ----- + "r1": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + }, + "ipv6": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + } + } + } + } + } + + result = verify_graceful_restart(tgen, topo, addr_type, input_dict, + dut = "r1", peer = 'r2') + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + try: + bgp_addr_type = topo["routers"][dut]["bgp"]["address_family"] + except TypeError: + bgp_addr_type = topo["routers"][dut]["bgp"][0]["address_family"] + + # bgp_addr_type = topo["routers"][dut]["bgp"]["address_family"] + + if addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + if bgp_neighbor != peer: + continue + + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + + logger.info( + "[DUT: {}]: Checking bgp graceful-restart show" + " o/p {}".format(dut, neighbor_ip) + ) + + show_bgp_graceful_json = None + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format( + addr_type, neighbor_ip + ), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info( + "[DUT: {}]: Neighbor ip matched {}".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Neighbor ip NOT a matched {}".format( + dut, neighbor_ip + ) + return errormsg + + lmode = None + rmode = None + # Local GR mode + if "address_family" in input_dict[dut]["bgp"]: + bgp_neighbors = input_dict[dut]["bgp"]["address_family"][addr_type][ + "unicast" + ]["neighbor"][peer]["dest_link"] + + for dest_link, data in bgp_neighbors.items(): + if ( + "graceful-restart-helper" in data + and data["graceful-restart-helper"] + ): + lmode = "Helper" + elif "graceful-restart" in data and data["graceful-restart"]: + lmode = "Restart" + elif ( + "graceful-restart-disable" in data + and data["graceful-restart-disable"] + ): + lmode = "Disable" + else: + lmode = None + + if lmode is None: + if "graceful-restart" in input_dict[dut]["bgp"]: + + if ( + "graceful-restart" in input_dict[dut]["bgp"]["graceful-restart"] + and input_dict[dut]["bgp"]["graceful-restart"][ + "graceful-restart" + ] + ): + lmode = "Restart*" + elif ( + "graceful-restart-disable" + in input_dict[dut]["bgp"]["graceful-restart"] + and input_dict[dut]["bgp"]["graceful-restart"][ + "graceful-restart-disable" + ] + ): + lmode = "Disable*" + else: + lmode = "Helper*" + else: + lmode = "Helper*" + + if lmode == "Disable" or lmode == "Disable*": + return True + + # Remote GR mode + if "address_family" in input_dict[peer]["bgp"]: + bgp_neighbors = input_dict[peer]["bgp"]["address_family"][addr_type][ + "unicast" + ]["neighbor"][dut]["dest_link"] + + for dest_link, data in bgp_neighbors.items(): + if ( + "graceful-restart-helper" in data + and data["graceful-restart-helper"] + ): + rmode = "Helper" + elif "graceful-restart" in data and data["graceful-restart"]: + rmode = "Restart" + elif ( + "graceful-restart-disable" in data + and data["graceful-restart-disable"] + ): + rmode = "Disable" + else: + rmode = None + + if rmode is None: + if "graceful-restart" in input_dict[peer]["bgp"]: + + if ( + "graceful-restart" + in input_dict[peer]["bgp"]["graceful-restart"] + and input_dict[peer]["bgp"]["graceful-restart"][ + "graceful-restart" + ] + ): + rmode = "Restart" + elif ( + "graceful-restart-disable" + in input_dict[peer]["bgp"]["graceful-restart"] + and input_dict[peer]["bgp"]["graceful-restart"][ + "graceful-restart-disable" + ] + ): + rmode = "Disable" + else: + rmode = "Helper" + else: + rmode = "Helper" + + if show_bgp_graceful_json_out["localGrMode"] == lmode: + logger.info( + "[DUT: {}]: localGrMode : {} ".format( + dut, show_bgp_graceful_json_out["localGrMode"] + ) + ) + else: + errormsg = ( + "[DUT: {}]: localGrMode is not correct" + " Expected: {}, Found: {}".format( + dut, lmode, show_bgp_graceful_json_out["localGrMode"] + ) + ) + return errormsg + + if show_bgp_graceful_json_out["remoteGrMode"] == rmode: + logger.info( + "[DUT: {}]: remoteGrMode : {} ".format( + dut, show_bgp_graceful_json_out["remoteGrMode"] + ) + ) + elif ( + show_bgp_graceful_json_out["remoteGrMode"] == "NotApplicable" + and rmode == "Disable" + ): + logger.info( + "[DUT: {}]: remoteGrMode : {} ".format( + dut, show_bgp_graceful_json_out["remoteGrMode"] + ) + ) + else: + errormsg = ( + "[DUT: {}]: remoteGrMode is not correct" + " Expected: {}, Found: {}".format( + dut, rmode, show_bgp_graceful_json_out["remoteGrMode"] + ) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=10) +def verify_r_bit(tgen, topo, addr_type, input_dict, dut, peer, expected=True): + """ + This API is to verify r_bit in the BGP gr capability advertised + by the neighbor router + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `input_dict`: input dictionary, have details of Device Under Test, for + which user wants to test the data + * `dut`: input dut router name + * `peer`: peer name + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "r1": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + }, + "ipv6": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + } + } + } + } + } + result = verify_r_bit(tgen, topo, addr_type, input_dict, dut, peer) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + + if addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + if bgp_neighbor != peer: + continue + + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + + logger.info( + "[DUT: {}]: Checking bgp graceful-restart show" + " o/p {}".format(dut, neighbor_ip) + ) + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format( + addr_type, neighbor_ip + ), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info( + "[DUT: {}]: Neighbor ip matched {}".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Neighbor ip NOT a matched {}".format( + dut, neighbor_ip + ) + return errormsg + + if "rBit" in show_bgp_graceful_json_out: + if show_bgp_graceful_json_out["rBit"]: + logger.info("[DUT: {}]: Rbit true {}".format(dut, neighbor_ip)) + else: + errormsg = "[DUT: {}]: Rbit false {}".format(dut, neighbor_ip) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=10) +def verify_eor(tgen, topo, addr_type, input_dict, dut, peer, expected=True): + """ + This API is to verify EOR + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `input_dict`: input dictionary, have details of DUT, for + which user wants to test the data + * `dut`: input dut router name + * `peer`: peer name + Usage + ----- + input_dict = { + input_dict = { + "r1": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + }, + "ipv6": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + } + } + } + } + } + + result = verify_eor(tgen, topo, addr_type, input_dict, dut, peer) + + Returns + ------- + errormsg(str) or True + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + + if addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + if bgp_neighbor != peer: + continue + + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + + logger.info( + "[DUT: %s]: Checking bgp graceful-restart" " show o/p %s", + dut, + neighbor_ip, + ) + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format( + addr_type, neighbor_ip + ), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info("[DUT: %s]: Neighbor ip matched %s", dut, neighbor_ip) + else: + errormsg = "[DUT: %s]: Neighbor ip is NOT matched %s" % ( + dut, + neighbor_ip, + ) + return errormsg + + if addr_type == "ipv4": + afi = "ipv4Unicast" + elif addr_type == "ipv6": + afi = "ipv6Unicast" + else: + errormsg = "Address type %s is not supported" % (addr_type) + return errormsg + + eor_json = show_bgp_graceful_json_out[afi]["endOfRibStatus"] + if "endOfRibSend" in eor_json: + + if eor_json["endOfRibSend"]: + logger.info( + "[DUT: %s]: EOR Send true for %s " "%s", dut, neighbor_ip, afi + ) + else: + errormsg = "[DUT: %s]: EOR Send false for %s" " %s" % ( + dut, + neighbor_ip, + afi, + ) + return errormsg + + if "endOfRibRecv" in eor_json: + if eor_json["endOfRibRecv"]: + logger.info( + "[DUT: %s]: EOR Recv true %s " "%s", dut, neighbor_ip, afi + ) + else: + errormsg = "[DUT: %s]: EOR Recv false %s " "%s" % ( + dut, + neighbor_ip, + afi, + ) + return errormsg + + if "endOfRibSentAfterUpdate" in eor_json: + if eor_json["endOfRibSentAfterUpdate"]: + logger.info( + "[DUT: %s]: EOR SendTime true for %s" " %s", + dut, + neighbor_ip, + afi, + ) + else: + errormsg = "[DUT: %s]: EOR SendTime false for " "%s %s" % ( + dut, + neighbor_ip, + afi, + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=8) +def verify_f_bit(tgen, topo, addr_type, input_dict, dut, peer, expected=True): + """ + This API is to verify f_bit in the BGP gr capability advertised + by the neighbor router + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `input_dict`: input dictionary, have details of Device Under Test, for + which user wants to test the data + * `dut`: input dut router name + * `peer`: peer name + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "r1": { + "bgp": { + "address_family": { + "ipv4": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + }, + "ipv6": { + "unicast": { + "neighbor": { + "r3": { + "dest_link":{ + "r1": { + "graceful-restart": True + } + } + } + } + } + } + } + } + } + } + + result = verify_f_bit(tgen, topo, 'ipv4', input_dict, dut, peer) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + bgp_addr_type = topo["routers"][router]["bgp"]["address_family"] + + if addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + if bgp_neighbor != peer: + continue + + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + + logger.info( + "[DUT: {}]: Checking bgp graceful-restart show" + " o/p {}".format(dut, neighbor_ip) + ) + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format( + addr_type, neighbor_ip + ), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info( + "[DUT: {}]: Neighbor ip matched {}".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Neighbor ip NOT a match {}".format( + dut, neighbor_ip + ) + return errormsg + + if "ipv4Unicast" in show_bgp_graceful_json_out: + if show_bgp_graceful_json_out["ipv4Unicast"]["fBit"]: + logger.info( + "[DUT: {}]: Fbit True for {} IPv4" + " Unicast".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Fbit False for {} IPv4" " Unicast".format( + dut, neighbor_ip + ) + return errormsg + + elif "ipv6Unicast" in show_bgp_graceful_json_out: + if show_bgp_graceful_json_out["ipv6Unicast"]["fBit"]: + logger.info( + "[DUT: {}]: Fbit True for {} IPv6" + " Unicast".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Fbit False for {} IPv6" " Unicast".format( + dut, neighbor_ip + ) + return errormsg + else: + show_bgp_graceful_json_out["ipv4Unicast"] + show_bgp_graceful_json_out["ipv6Unicast"] + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=10) +def verify_graceful_restart_timers(tgen, topo, addr_type, input_dict, dut, peer): + """ + This API is to verify graceful restart timers, configured and received + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `input_dict`: input dictionary, have details of Device Under Test, + for which user wants to test the data + * `dut`: input dut router name + * `peer`: peer name + * `expected` : expected results from API, by-default True + + Usage + ----- + # Configure graceful-restart + input_dict_1 = { + "r1": { + "bgp": { + "bgp_neighbors": { + "r3": { + "graceful-restart": "graceful-restart-helper" + } + }, + "gracefulrestart": ["restart-time 150"] + } + }, + "r3": { + "bgp": { + "bgp_neighbors": { + "r1": { + "graceful-restart": "graceful-restart" + } + } + } + } + } + + result = verify_graceful_restart_timers(tgen, topo, 'ipv4', input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router, rnode in tgen.routers().items(): + if router != dut: + continue + + bgp_addr_type = topo["routers"][dut]["bgp"]["address_family"] + + if addr_type in bgp_addr_type: + if not check_address_types(addr_type): + continue + + bgp_neighbors = bgp_addr_type[addr_type]["unicast"]["neighbor"] + + for bgp_neighbor, peer_data in bgp_neighbors.items(): + if bgp_neighbor != peer: + continue + + for dest_link, peer_dict in peer_data["dest_link"].items(): + data = topo["routers"][bgp_neighbor]["links"] + + if dest_link in data: + neighbor_ip = data[dest_link][addr_type].split("/")[0] + + logger.info( + "[DUT: {}]: Checking bgp graceful-restart show" + " o/p {}".format(dut, neighbor_ip) + ) + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format( + addr_type, neighbor_ip + ), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info( + "[DUT: {}]: Neighbor ip matched {}".format(dut, neighbor_ip) + ) + else: + errormsg = "[DUT: {}]: Neighbor ip is NOT matched {}".format( + dut, neighbor_ip + ) + return errormsg + + # Graceful-restart timer + if "graceful-restart" in input_dict[peer]["bgp"]: + if "timer" in input_dict[peer]["bgp"]["graceful-restart"]: + for rs_timer, value in input_dict[peer]["bgp"]["graceful-restart"][ + "timer" + ].items(): + if rs_timer == "restart-time": + + receivedTimer = value + if ( + show_bgp_graceful_json_out["timers"][ + "receivedRestartTimer" + ] + == receivedTimer + ): + logger.info( + "receivedRestartTimer is {}" + " on {} from peer {}".format( + receivedTimer, router, peer + ) + ) + else: + errormsg = ( + "receivedRestartTimer is not" + " as expected {}".format(receivedTimer) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=8) +def verify_gr_address_family( + tgen, topo, addr_type, addr_family, dut, peer, expected=True +): + """ + This API is to verify gr_address_family in the BGP gr capability advertised + by the neighbor router + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: input json file data + * `addr_type` : ip type ipv4/ipv6 + * `addr_type` : ip type IPV4 Unicast/IPV6 Unicast + * `dut`: input dut router name + * `peer`: input peer router to check + * `expected` : expected results from API, by-default True + + Usage + ----- + + result = verify_gr_address_family(tgen, topo, "ipv4", "ipv4Unicast", "r1", "r3") + + Returns + ------- + errormsg(str) or None + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if not check_address_types(addr_type): + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return + + routers = tgen.routers() + if dut not in routers: + return "{} not in routers".format(dut) + + rnode = routers[dut] + bgp_addr_type = topo["routers"][dut]["bgp"]["address_family"] + + if addr_type not in bgp_addr_type: + return "{} not in bgp_addr_types".format(addr_type) + + if peer not in bgp_addr_type[addr_type]["unicast"]["neighbor"]: + return "{} not a peer of {} over {}".format(peer, dut, addr_type) + + nbr_links = topo["routers"][peer]["links"] + if dut not in nbr_links or addr_type not in nbr_links[dut]: + return "peer {} missing back link to {} over {}".format(peer, dut, addr_type) + + neighbor_ip = nbr_links[dut][addr_type].split("/")[0] + + logger.info( + "[DUT: {}]: Checking bgp graceful-restart show o/p {} for {}".format( + dut, neighbor_ip, addr_family + ) + ) + + show_bgp_graceful_json = run_frr_cmd( + rnode, + "show bgp {} neighbor {} graceful-restart json".format(addr_type, neighbor_ip), + isjson=True, + ) + + show_bgp_graceful_json_out = show_bgp_graceful_json[neighbor_ip] + + if show_bgp_graceful_json_out["neighborAddr"] == neighbor_ip: + logger.info("Neighbor ip matched {}".format(neighbor_ip)) + else: + errormsg = "Neighbor ip NOT a match {}".format(neighbor_ip) + return errormsg + + if addr_family == "ipv4Unicast": + if "ipv4Unicast" in show_bgp_graceful_json_out: + logger.info("ipv4Unicast present for {} ".format(neighbor_ip)) + return True + else: + errormsg = "ipv4Unicast NOT present for {} ".format(neighbor_ip) + return errormsg + + elif addr_family == "ipv6Unicast": + if "ipv6Unicast" in show_bgp_graceful_json_out: + logger.info("ipv6Unicast present for {} ".format(neighbor_ip)) + return True + else: + errormsg = "ipv6Unicast NOT present for {} ".format(neighbor_ip) + return errormsg + else: + errormsg = "Aaddress family: {} present for {} ".format( + addr_family, neighbor_ip + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + +@retry(retry_timeout=12) +def verify_attributes_for_evpn_routes( + tgen, + topo, + dut, + input_dict, + rd=None, + rt=None, + ethTag=None, + ipLen=None, + rd_peer=None, + rt_peer=None, + expected=True, +): + """ + API to verify rd and rt value using "sh bgp l2vpn evpn 10.1.1.1" + command. + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : device under test + * `input_dict`: having details like - for which route, rd value + needs to be verified + * `rd` : route distinguisher + * `rt` : route target + * `ethTag` : Ethernet Tag + * `ipLen` : IP prefix length + * `rd_peer` : Peer name from which RD will be auto-generated + * `rt_peer` : Peer name from which RT will be auto-generated + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict_1 = { + "r1": { + "static_routes": [{ + "network": [NETWORK1_1[addr_type]], + "next_hop": NEXT_HOP_IP[addr_type], + "vrf": "RED" + }] + } + } + + result = verify_attributes_for_evpn_routes(tgen, topo, + input_dict, rd = "10.0.0.33:1") + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for router in input_dict.keys(): + rnode = tgen.routers()[dut] + + if "static_routes" in input_dict[router]: + for static_route in input_dict[router]["static_routes"]: + network = static_route["network"] + + if "vrf" in static_route: + vrf = static_route["vrf"] + + if type(network) is not list: + network = [network] + + for route in network: + route = route.split("/")[0] + _addr_type = validate_ip_address(route) + if "v4" in _addr_type: + input_afi = "v4" + elif "v6" in _addr_type: + input_afi = "v6" + + cmd = "show bgp l2vpn evpn {} json".format(route) + evpn_rd_value_json = run_frr_cmd(rnode, cmd, isjson=True) + if not bool(evpn_rd_value_json): + errormsg = "No output for '{}' cli".format(cmd) + return errormsg + + if rd is not None and rd != "auto": + logger.info( + "[DUT: %s]: Verifying rd value for " "evpn route %s:", + dut, + route, + ) + + if rd in evpn_rd_value_json: + rd_value_json = evpn_rd_value_json[rd] + if rd_value_json["rd"] != rd: + errormsg = ( + "[DUT: %s] Failed: Verifying" + " RD value for EVPN route: %s" + "[FAILED]!!, EXPECTED : %s " + " FOUND : %s" + % (dut, route, rd, rd_value_json["rd"]) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: Verifying RD value for" + " EVPN route: %s [PASSED]|| " + "Found Expected: %s", + dut, + route, + rd, + ) + return True + + else: + errormsg = ( + "[DUT: %s] RD : %s is not present" + " in cli json output" % (dut, rd) + ) + return errormsg + + if rd == "auto": + logger.info( + "[DUT: %s]: Verifying auto-rd value for " "evpn route %s:", + dut, + route, + ) + + if rd_peer: + index = 1 + vni_dict = {} + + rnode = tgen.routers()[rd_peer] + vrfs = topo["routers"][rd_peer]["vrfs"] + for vrf_dict in vrfs: + vni_dict[vrf_dict["name"]] = index + index += 1 + + show_bgp_json = run_frr_cmd( + rnode, "show bgp vrf all summary json", isjson=True + ) + + # Verifying output dictionary show_bgp_json is empty + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + show_bgp_json_vrf = show_bgp_json[vrf] + for afi, afi_data in show_bgp_json_vrf.items(): + if input_afi not in afi: + continue + router_id = afi_data["routerId"] + + found = False + rd = "{}:{}".format(router_id, vni_dict[vrf]) + for _rd, rd_value_json in evpn_rd_value_json.items(): + if ( + str(rd_value_json["rd"].split(":")[0]) + != rd.split(":")[0] + ): + continue + + if int(rd_value_json["rd"].split(":")[1]) > 0: + found = True + + if found: + logger.info( + "[DUT %s]: Verifying RD value for" + " EVPN route: %s " + "Found Expected: %s", + dut, + route, + rd_value_json["rd"], + ) + return True + else: + errormsg = ( + "[DUT: %s] Failed: Verifying" + " RD value for EVPN route: %s" + " FOUND : %s" % (dut, route, rd_value_json["rd"]) + ) + return errormsg + + if rt == "auto": + vni_dict = {} + logger.info( + "[DUT: %s]: Verifying auto-rt value for " "evpn route %s:", + dut, + route, + ) + + if rt_peer: + rnode = tgen.routers()[rt_peer] + show_bgp_json = run_frr_cmd( + rnode, "show bgp vrf all summary json", isjson=True + ) + + # Verifying output dictionary show_bgp_json is empty + if not bool(show_bgp_json): + errormsg = "BGP is not running" + return errormsg + + show_bgp_json_vrf = show_bgp_json[vrf] + for afi, afi_data in show_bgp_json_vrf.items(): + if input_afi not in afi: + continue + as_num = afi_data["as"] + + show_vrf_vni_json = run_frr_cmd( + rnode, "show vrf vni json", isjson=True + ) + + vrfs = show_vrf_vni_json["vrfs"] + for vrf_dict in vrfs: + if vrf_dict["vrf"] == vrf: + vni_dict[vrf_dict["vrf"]] = str(vrf_dict["vni"]) + + # If AS is 4 byte, FRR uses only the lower 2 bytes of ASN+VNI + # for auto derived RT value. + if as_num > 65535: + as_bin = bin(as_num) + as_bin = as_bin[-16:] + as_num = int(as_bin, 2) + + rt = "{}:{}".format(str(as_num), vni_dict[vrf]) + for _rd, route_data in evpn_rd_value_json.items(): + if route_data["ip"] == route: + for rt_data in route_data["paths"]: + if vni_dict[vrf] == rt_data["VNI"]: + rt_string = rt_data["extendedCommunity"][ + "string" + ] + rt_input = "RT:{}".format(rt) + if rt_input not in rt_string: + errormsg = ( + "[DUT: %s] Failed:" + " Verifying RT " + "value for EVPN " + " route: %s" + "[FAILED]!!," + " EXPECTED : %s " + " FOUND : %s" + % (dut, route, rt_input, rt_string) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: Verifying " + "RT value for EVPN " + "route: %s [PASSED]||" + "Found Expected: %s", + dut, + route, + rt_input, + ) + return True + + else: + errormsg = ( + "[DUT: %s] Route : %s is not" + " present in cli json output" % (dut, route) + ) + return errormsg + + if rt is not None and rt != "auto": + logger.info( + "[DUT: %s]: Verifying rt value for " "evpn route %s:", + dut, + route, + ) + + if type(rt) is not list: + rt = [rt] + + for _rt in rt: + for _rd, route_data in evpn_rd_value_json.items(): + if route_data["ip"] == route: + for rt_data in route_data["paths"]: + rt_string = rt_data["extendedCommunity"][ + "string" + ] + rt_input = "RT:{}".format(_rt) + if rt_input not in rt_string: + errormsg = ( + "[DUT: %s] Failed: " + "Verifying RT value " + "for EVPN route: %s" + "[FAILED]!!," + " EXPECTED : %s " + " FOUND : %s" + % (dut, route, rt_input, rt_string) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: Verifying RT" + " value for EVPN route:" + " %s [PASSED]|| " + "Found Expected: %s", + dut, + route, + rt_input, + ) + return True + + else: + errormsg = ( + "[DUT: %s] Route : %s is not" + " present in cli json output" % (dut, route) + ) + return errormsg + + if ethTag is not None: + logger.info( + "[DUT: %s]: Verifying ethTag value for " "evpn route :", dut + ) + + for _rd, route_data in evpn_rd_value_json.items(): + if route_data["ip"] == route: + if route_data["ethTag"] != ethTag: + errormsg = ( + "[DUT: %s] RD: %s, Failed: " + "Verifying ethTag value " + "for EVPN route: %s" + "[FAILED]!!," + " EXPECTED : %s " + " FOUND : %s" + % ( + dut, + _rd, + route, + ethTag, + route_data["ethTag"], + ) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: RD: %s, Verifying " + "ethTag value for EVPN route:" + " %s [PASSED]|| " + "Found Expected: %s", + dut, + _rd, + route, + ethTag, + ) + return True + + else: + errormsg = ( + "[DUT: %s] RD: %s, Route : %s " + "is not present in cli json " + "output" % (dut, _rd, route) + ) + return errormsg + + if ipLen is not None: + logger.info( + "[DUT: %s]: Verifying ipLen value for " "evpn route :", dut + ) + + for _rd, route_data in evpn_rd_value_json.items(): + if route_data["ip"] == route: + if route_data["ipLen"] != int(ipLen): + errormsg = ( + "[DUT: %s] RD: %s, Failed: " + "Verifying ipLen value " + "for EVPN route: %s" + "[FAILED]!!," + " EXPECTED : %s " + " FOUND : %s" + % (dut, _rd, route, ipLen, route_data["ipLen"]) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: RD: %s, Verifying " + "ipLen value for EVPN route:" + " %s [PASSED]|| " + "Found Expected: %s", + dut, + _rd, + route, + ipLen, + ) + return True + + else: + errormsg = ( + "[DUT: %s] RD: %s, Route : %s " + "is not present in cli json " + "output " % (dut, _rd, route) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return False + + +@retry(retry_timeout=10) +def verify_evpn_routes( + tgen, topo, dut, input_dict, routeType=5, EthTag=0, next_hop=None, expected=True +): + """ + API to verify evpn routes using "sh bgp l2vpn evpn" + command. + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : device under test + * `input_dict`: having details like - for which route, rd value + needs to be verified + * `route_type` : Route type 5 is supported as of now + * `EthTag` : Ethernet tag, by-default is 0 + * `next_hop` : Prefered nexthop for the evpn routes + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict_1 = { + "r1": { + "static_routes": [{ + "network": [NETWORK1_1[addr_type]], + "next_hop": NEXT_HOP_IP[addr_type], + "vrf": "RED" + }] + } + } + result = verify_evpn_routes(tgen, topo, input_dict) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in input_dict.keys(): + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying evpn routes: ", dut) + + if "static_routes" in input_dict[router]: + for static_route in input_dict[router]["static_routes"]: + network = static_route["network"] + + if type(network) is not list: + network = [network] + + missing_routes = {} + for route in network: + rd_keys = 0 + ip_len = route.split("/")[1] + route = route.split("/")[0] + + prefix = "[{}]:[{}]:[{}]:[{}]".format( + routeType, EthTag, ip_len, route + ) + + cmd = "show bgp l2vpn evpn route json" + evpn_value_json = run_frr_cmd(rnode, cmd, isjson=True) + + if not bool(evpn_value_json): + errormsg = "No output for '{}' cli".format(cmd) + return errormsg + + if evpn_value_json["numPrefix"] == 0: + errormsg = "[DUT: %s]: No EVPN prefixes exist" % (dut) + return errormsg + + for key, route_data_json in evpn_value_json.items(): + if type(route_data_json) is dict: + rd_keys += 1 + if prefix not in route_data_json: + missing_routes[key] = prefix + + if rd_keys == len(missing_routes.keys()): + errormsg = ( + "[DUT: %s]: " + "Missing EVPN routes: " + "%s [FAILED]!!" % (dut, list(set(missing_routes.values()))) + ) + return errormsg + + for key, route_data_json in evpn_value_json.items(): + if type(route_data_json) is dict: + if prefix not in route_data_json: + continue + + for paths in route_data_json[prefix]["paths"]: + for path in paths: + if path["routeType"] != routeType: + errormsg = ( + "[DUT: %s]: " + "Verifying routeType " + "for EVPN route: %s " + "[FAILED]!! " + "Expected: %s, " + "Found: %s" + % ( + dut, + prefix, + routeType, + path["routeType"], + ) + ) + return errormsg + + elif next_hop: + for nh_dict in path["nexthops"]: + if nh_dict["ip"] != next_hop: + errormsg = ( + "[DUT: %s]: " + "Verifying " + "nexthop for " + "EVPN route: %s" + "[FAILED]!! " + "Expected: %s," + " Found: %s" + % ( + dut, + prefix, + next_hop, + nh_dict["ip"], + ) + ) + return errormsg + + else: + logger.info( + "[DUT %s]: Verifying " + "EVPN route : %s, " + "routeType: %s is " + "installed " + "[PASSED]|| ", + dut, + prefix, + routeType, + ) + return True + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return False + + +@retry(retry_timeout=10) +def verify_bgp_bestpath(tgen, addr_type, input_dict): + """ + Verifies bgp next hop values in best-path output + + * `dut` : device under test + * `addr_type` : Address type ipv4/ipv6 + * `input_dict`: having details like multipath and bestpath + + Usage + ----- + input_dict_1 = { + "r1": { + "ipv4" : { + "bestpath": "50.0.0.1", + "multipath": ["50.0.0.1", "50.0.0.2"], + "network": "100.0.0.0/24" + } + "ipv6" : { + "bestpath": "1000::1", + "multipath": ["1000::1", "1000::2"] + "network": "2000::1/128" + } + } + } + + result = verify_bgp_bestpath(tgen, input_dict) + + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for dut in input_dict.keys(): + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying bgp bestpath and multipath " "routes:", dut) + result = False + for network_dict in input_dict[dut][addr_type]: + nw_addr = network_dict.setdefault("network", None) + vrf = network_dict.setdefault("vrf", None) + bestpath = network_dict.setdefault("bestpath", None) + + if vrf: + cmd = "show bgp vrf {} {} {} bestpath json".format( + vrf, addr_type, nw_addr + ) + else: + cmd = "show bgp {} {} bestpath json".format(addr_type, nw_addr) + + data = run_frr_cmd(rnode, cmd, isjson=True) + route = data["paths"][0] + + if "bestpath" in route: + if route["bestpath"]["overall"] is True: + _bestpath = route["nexthops"][0]["ip"] + + if _bestpath != bestpath: + return ( + "DUT:[{}] Bestpath do not match for" + " network: {}, Expected " + " {} as bgp bestpath found {}".format( + dut, nw_addr, bestpath, _bestpath + ) + ) + + logger.info( + "DUT:[{}] Found expected bestpath: " + " {} for network: {}".format(dut, _bestpath, nw_addr) + ) + result = True + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def verify_tcp_mss(tgen, dut, neighbour, configured_tcp_mss, vrf=None): + """ + This api is used to verify the tcp-mss value assigned to a neigbour of DUT + + Parameters + ---------- + * `tgen` : topogen object + * `dut`: device under test + * `neighbour`:neigbout IP address + * `configured_tcp_mss`:The TCP-MSS value to be verified + * `vrf`:vrf + + Usage + ----- + result = verify_tcp_mss(tgen, dut,neighbour,configured_tcp_mss) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + rnode = tgen.routers()[dut] + if vrf: + cmd = "show bgp vrf {} neighbors {} json".format(vrf, neighbour) + else: + cmd = "show bgp neighbors {} json".format(neighbour) + + # Execute the command + show_vrf_stats = run_frr_cmd(rnode, cmd, isjson=True) + + # Verify TCP-MSS on router + logger.info("Verify that no core is observed") + if tgen.routers_have_failure(): + errormsg = "Core observed while running CLI: %s" % (cmd) + return errormsg + else: + if configured_tcp_mss == show_vrf_stats.get(neighbour).get( + "bgpTcpMssConfigured" + ): + logger.debug( + "Configured TCP - MSS Found: {}".format(sys._getframe().f_code.co_name) + ) + return True + else: + logger.debug( + "TCP-MSS Mismatch ,configured {} expecting {}".format( + show_vrf_stats.get(neighbour).get("bgpTcpMssConfigured"), + configured_tcp_mss, + ) + ) + return "TCP-MSS Mismatch" + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return False + + +def get_dut_as_number(tgen, dut): + """ + API to get the Autonomous Number of the given DUT + + params: + ======= + dut : Device Under test + + returns : + ======= + Success : DUT Autonomous number + Fail : Error message with Boolean False + """ + tgen = get_topogen() + for router, rnode in tgen.routers().items(): + if router == dut: + show_bgp_json = run_frr_cmd(rnode, "sh ip bgp summary json ", isjson=True) + as_number = show_bgp_json["ipv4Unicast"]["as"] + if as_number: + logger.info( + "[dut {}] DUT contains Automnomous number :: {} ".format( + dut, as_number + ) + ) + return as_number + else: + logger.error( + "[dut {}] ERROR....! DUT doesnot contain any Automnomous number ".format( + dut + ) + ) + return False + + +def get_prefix_count_route( + tgen, topo, dut, peer, vrf=None, link=None, sent=None, received=None +): + """ + API to return the prefix count of default originate the given DUT + dut : Device under test + peer : neigbor on which you are expecting the route to be received + + returns : + prefix_count as dict with ipv4 and ipv6 value + """ + # the neighbor IP address can be accessable by finding the neigborship (vice-versa) + + if link: + neighbor_ipv4_address = topo["routers"][peer]["links"][link]["ipv4"] + neighbor_ipv6_address = topo["routers"][peer]["links"][link]["ipv6"] + else: + neighbor_ipv4_address = topo["routers"][peer]["links"][dut]["ipv4"] + neighbor_ipv6_address = topo["routers"][peer]["links"][dut]["ipv6"] + + neighbor_ipv4_address = neighbor_ipv4_address.split("/")[0] + neighbor_ipv6_address = neighbor_ipv6_address.split("/")[0] + prefix_count = {} + tgen = get_topogen() + for router, rnode in tgen.routers().items(): + if router == dut: + + if vrf: + ipv4_cmd = "sh ip bgp vrf {} summary json".format(vrf) + show_bgp_json_ipv4 = run_frr_cmd(rnode, ipv4_cmd, isjson=True) + ipv6_cmd = "sh ip bgp vrf {} ipv6 unicast summary json".format(vrf) + show_bgp_json_ipv6 = run_frr_cmd(rnode, ipv6_cmd, isjson=True) + + prefix_count["ipv4_count"] = show_bgp_json_ipv4["ipv4Unicast"]["peers"][ + neighbor_ipv4_address + ]["pfxRcd"] + prefix_count["ipv6_count"] = show_bgp_json_ipv6["peers"][ + neighbor_ipv6_address + ]["pfxRcd"] + + logger.info( + "The Prefix Count of the [DUT:{} : vrf [{}] ] towards neighbor ipv4 : {} and ipv6 : {} is : {}".format( + dut, + vrf, + neighbor_ipv4_address, + neighbor_ipv6_address, + prefix_count, + ) + ) + return prefix_count + + else: + show_bgp_json_ipv4 = run_frr_cmd( + rnode, "sh ip bgp summary json ", isjson=True + ) + show_bgp_json_ipv6 = run_frr_cmd( + rnode, "sh ip bgp ipv6 unicast summary json ", isjson=True + ) + if received: + prefix_count["ipv4_count"] = show_bgp_json_ipv4["ipv4Unicast"][ + "peers" + ][neighbor_ipv4_address]["pfxRcd"] + prefix_count["ipv6_count"] = show_bgp_json_ipv6["peers"][ + neighbor_ipv6_address + ]["pfxRcd"] + + elif sent: + prefix_count["ipv4_count"] = show_bgp_json_ipv4["ipv4Unicast"][ + "peers" + ][neighbor_ipv4_address]["pfxSnt"] + prefix_count["ipv6_count"] = show_bgp_json_ipv6["peers"][ + neighbor_ipv6_address + ]["pfxSnt"] + + else: + prefix_count["ipv4_count"] = show_bgp_json_ipv4["ipv4Unicast"][ + "peers" + ][neighbor_ipv4_address]["pfxRcd"] + prefix_count["ipv6_count"] = show_bgp_json_ipv6["peers"][ + neighbor_ipv6_address + ]["pfxRcd"] + + logger.info( + "The Prefix Count of the DUT:{} towards neighbor ipv4 : {} and ipv6 : {} is : {}".format( + dut, neighbor_ipv4_address, neighbor_ipv6_address, prefix_count + ) + ) + return prefix_count + else: + logger.error("ERROR...! Unknown dut {} in topolgy".format(dut)) + + +@retry(retry_timeout=5) +def verify_rib_default_route( + tgen, + topo, + dut, + routes, + expected_nexthop, + metric=None, + origin=None, + locPrf=None, + expected_aspath=None, +): + """ + API to verify the the 'Default route" in BGP RIB with the attributes the rout carries (metric , local preference, ) + + param + ===== + dut : device under test + routes : default route with expected nexthop + expected_nexthop : the nexthop that is expected the deafult route + + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + connected_routes = {} + for router, rnode in tgen.routers().items(): + if router == dut: + + ipv4_routes = run_frr_cmd(rnode, "sh ip bgp json", isjson=True) + ipv6_routes = run_frr_cmd(rnode, "sh ip bgp ipv6 unicast json", isjson=True) + is_ipv4_default_attrib_found = False + is_ipv6_default_attrib_found = False + + default_ipv4_route = routes["ipv4"] + default_ipv6_route = "::/0" + ipv4_route_Origin = False + ipv4_route_local_pref = False + ipv4_route_metric = False + + if default_ipv4_route in ipv4_routes["routes"].keys(): + nxt_hop_count = len(ipv4_routes["routes"][default_ipv4_route]) + rib_next_hops = [] + for index in range(nxt_hop_count): + rib_next_hops.append( + ipv4_routes["routes"][default_ipv4_route][index]["nexthops"][0]["ip"] + ) + + for nxt_hop in expected_nexthop.items(): + if nxt_hop[0] == "ipv4": + if nxt_hop[1] in rib_next_hops: + logger.info( + "Default routes [{}] obtained from {} .....PASSED".format( + default_ipv4_route, nxt_hop[1] + ) + ) + else: + logger.error( + "ERROR ...! Default routes [{}] expected is missing {}".format( + default_ipv4_route, nxt_hop[1] + ) + ) + return False + + else: + pass + + if "origin" in ipv4_routes["routes"][default_ipv4_route][0].keys(): + ipv4_route_Origin = ipv4_routes["routes"][default_ipv4_route][0]["origin"] + if "locPrf" in ipv4_routes["routes"][default_ipv4_route][0].keys(): + ipv4_route_local_pref = ipv4_routes["routes"][default_ipv4_route][0][ + "locPrf" + ] + if "metric" in ipv4_routes["routes"][default_ipv4_route][0].keys(): + ipv4_route_metric = ipv4_routes["routes"][default_ipv4_route][0]["metric"] + else: + logger.error("ERROR [ DUT {}] : The Default Route Not found in RIB".format(dut)) + return False + + origin_found = False + locPrf_found = False + metric_found = False + as_path_found = False + + if origin: + if origin == ipv4_route_Origin: + logger.info( + "Dafault Route {} expected origin {} Found in RIB....PASSED".format( + default_ipv4_route, origin + ) + ) + origin_found = True + else: + logger.error( + "ERROR... IPV4::! Expected Origin is {} obtained {}".format( + origin, ipv4_route_Origin + ) + ) + return False + else: + origin_found = True + + if locPrf: + if locPrf == ipv4_route_local_pref: + logger.info( + "Dafault Route {} expected local preference {} Found in RIB....PASSED".format( + default_ipv4_route, locPrf + ) + ) + locPrf_found = True + else: + logger.error( + "ERROR... IPV4::! Expected Local preference is {} obtained {}".format( + locPrf, ipv4_route_local_pref + ) + ) + return False + else: + locPrf_found = True + + if metric: + if metric == ipv4_route_metric: + logger.info( + "Dafault Route {} expected metric {} Found in RIB....PASSED".format( + default_ipv4_route, metric + ) + ) + + metric_found = True + else: + logger.error( + "ERROR... IPV4::! Expected metric is {} obtained {}".format( + metric, ipv4_route_metric + ) + ) + return False + else: + metric_found = True + + if expected_aspath: + obtained_aspath = ipv4_routes["routes"]["0.0.0.0/0"][0]["path"] + if expected_aspath in obtained_aspath: + as_path_found = True + logger.info( + "Dafault Route {} expected AS path {} Found in RIB....PASSED".format( + default_ipv4_route, expected_aspath + ) + ) + else: + logger.error( + "ERROR.....! Expected AS path {} obtained {}..... FAILED ".format( + expected_aspath, obtained_aspath + ) + ) + return False + else: + as_path_found = True + + if origin_found and locPrf_found and metric_found and as_path_found: + is_ipv4_default_attrib_found = True + logger.info( + "IPV4:: Expected origin ['{}'] , Local Preference ['{}'] , Metric ['{}'] and AS path [{}] is found in RIB".format( + origin, locPrf, metric, expected_aspath + ) + ) + else: + is_ipv4_default_attrib_found = False + logger.error( + "IPV4:: Expected origin ['{}'] Obtained [{}]".format( + origin, ipv4_route_Origin + ) + ) + logger.error( + "IPV4:: Expected locPrf ['{}'] Obtained [{}]".format( + locPrf, ipv4_route_local_pref + ) + ) + logger.error( + "IPV4:: Expected metric ['{}'] Obtained [{}]".format( + metric, ipv4_route_metric + ) + ) + logger.error( + "IPV4:: Expected metric ['{}'] Obtained [{}]".format( + expected_aspath, obtained_aspath + ) + ) + + route_Origin = False + route_local_pref = False + route_local_metric = False + default_ipv6_route = "" + try: + ipv6_routes["routes"]["0::0/0"] + default_ipv6_route = "0::0/0" + except: + ipv6_routes["routes"]["::/0"] + default_ipv6_route = "::/0" + if default_ipv6_route in ipv6_routes["routes"].keys(): + nxt_hop_count = len(ipv6_routes["routes"][default_ipv6_route]) + rib_next_hops = [] + for index in range(nxt_hop_count): + rib_next_hops.append( + ipv6_routes["routes"][default_ipv6_route][index]["nexthops"][0]["ip"] + ) + try: + rib_next_hops.append( + ipv6_routes["routes"][default_ipv6_route][index]["nexthops"][1][ + "ip" + ] + ) + except (KeyError, IndexError) as e: + logger.error("NO impact ..! Global IPV6 Address not found ") + + for nxt_hop in expected_nexthop.items(): + if nxt_hop[0] == "ipv6": + if nxt_hop[1] in rib_next_hops: + logger.info( + "Default routes [{}] obtained from {} .....PASSED".format( + default_ipv6_route, nxt_hop[1] + ) + ) + else: + logger.error( + "ERROR ...! Default routes [{}] expected from {} obtained {}".format( + default_ipv6_route, nxt_hop[1], rib_next_hops + ) + ) + return False + + else: + pass + if "origin" in ipv6_routes["routes"][default_ipv6_route][0].keys(): + route_Origin = ipv6_routes["routes"][default_ipv6_route][0]["origin"] + if "locPrf" in ipv6_routes["routes"][default_ipv6_route][0].keys(): + route_local_pref = ipv6_routes["routes"][default_ipv6_route][0]["locPrf"] + if "metric" in ipv6_routes["routes"][default_ipv6_route][0].keys(): + route_local_metric = ipv6_routes["routes"][default_ipv6_route][0]["metric"] + + origin_found = False + locPrf_found = False + metric_found = False + as_path_found = False + + if origin: + if origin == route_Origin: + logger.info( + "Dafault Route {} expected origin {} Found in RIB....PASSED".format( + default_ipv6_route, route_Origin + ) + ) + origin_found = True + else: + logger.error( + "ERROR... IPV6::! Expected Origin is {} obtained {}".format( + origin, route_Origin + ) + ) + return False + else: + origin_found = True + + if locPrf: + if locPrf == route_local_pref: + logger.info( + "Dafault Route {} expected Local Preference {} Found in RIB....PASSED".format( + default_ipv6_route, route_local_pref + ) + ) + locPrf_found = True + else: + logger.error( + "ERROR... IPV6::! Expected Local Preference is {} obtained {}".format( + locPrf, route_local_pref + ) + ) + return False + else: + locPrf_found = True + + if metric: + if metric == route_local_metric: + logger.info( + "Dafault Route {} expected metric {} Found in RIB....PASSED".format( + default_ipv4_route, metric + ) + ) + + metric_found = True + else: + logger.error( + "ERROR... IPV6::! Expected metric is {} obtained {}".format( + metric, route_local_metric + ) + ) + return False + else: + metric_found = True + + if expected_aspath: + obtained_aspath = ipv6_routes["routes"]["::/0"][0]["path"] + if expected_aspath in obtained_aspath: + as_path_found = True + logger.info( + "Dafault Route {} expected AS path {} Found in RIB....PASSED".format( + default_ipv4_route, expected_aspath + ) + ) + else: + logger.error( + "ERROR.....! Expected AS path {} obtained {}..... FAILED ".format( + expected_aspath, obtained_aspath + ) + ) + return False + else: + as_path_found = True + + if origin_found and locPrf_found and metric_found and as_path_found: + is_ipv6_default_attrib_found = True + logger.info( + "IPV6:: Expected origin ['{}'] , Local Preference ['{}'] , Metric ['{}'] and AS path [{}] is found in RIB".format( + origin, locPrf, metric, expected_aspath + ) + ) + else: + is_ipv6_default_attrib_found = False + logger.error( + "IPV6:: Expected origin ['{}'] Obtained [{}]".format(origin, route_Origin) + ) + logger.error( + "IPV6:: Expected locPrf ['{}'] Obtained [{}]".format( + locPrf, route_local_pref + ) + ) + logger.error( + "IPV6:: Expected metric ['{}'] Obtained [{}]".format( + metric, route_local_metric + ) + ) + logger.error( + "IPV6:: Expected metric ['{}'] Obtained [{}]".format( + expected_aspath, obtained_aspath + ) + ) + + if is_ipv4_default_attrib_found and is_ipv6_default_attrib_found: + logger.info("The attributes are found for default route in RIB ") + return True + else: + return False + + +@retry(retry_timeout=5) +def verify_fib_default_route(tgen, topo, dut, routes, expected_nexthop): + """ + API to verify the the 'Default route" in FIB + + param + ===== + dut : device under test + routes : default route with expected nexthop + expected_nexthop : the nexthop that is expected the deafult route + + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + connected_routes = {} + for router, rnode in tgen.routers().items(): + if router == dut: + ipv4_routes = run_frr_cmd(rnode, "sh ip route json", isjson=True) + ipv6_routes = run_frr_cmd(rnode, "sh ipv6 route json", isjson=True) + + is_ipv4_default_route_found = False + is_ipv6_default_route_found = False + if routes["ipv4"] in ipv4_routes.keys(): + rib_ipv4_nxt_hops = [] + ipv4_default_route = routes["ipv4"] + nxt_hop_count = len(ipv4_routes[ipv4_default_route][0]["nexthops"]) + for index in range(nxt_hop_count): + rib_ipv4_nxt_hops.append( + ipv4_routes[ipv4_default_route][0]["nexthops"][index]["ip"] + ) + + if expected_nexthop["ipv4"] in rib_ipv4_nxt_hops: + is_ipv4_default_route_found = True + logger.info( + "{} default route with next hop {} is found in FIB ".format( + ipv4_default_route, expected_nexthop + ) + ) + else: + logger.error( + "ERROR .. ! {} default route with next hop {} is not found in FIB ".format( + ipv4_default_route, expected_nexthop + ) + ) + return False + + if routes["ipv6"] in ipv6_routes.keys() or "::/0" in ipv6_routes.keys(): + rib_ipv6_nxt_hops = [] + if "::/0" in ipv6_routes.keys(): + ipv6_default_route = "::/0" + elif routes["ipv6"] in ipv6_routes.keys(): + ipv6_default_route = routes["ipv6"] + + nxt_hop_count = len(ipv6_routes[ipv6_default_route][0]["nexthops"]) + for index in range(nxt_hop_count): + rib_ipv6_nxt_hops.append( + ipv6_routes[ipv6_default_route][0]["nexthops"][index]["ip"] + ) + + if expected_nexthop["ipv6"] in rib_ipv6_nxt_hops: + is_ipv6_default_route_found = True + logger.info( + "{} default route with next hop {} is found in FIB ".format( + ipv6_default_route, expected_nexthop + ) + ) + else: + logger.error( + "ERROR .. ! {} default route with next hop {} is not found in FIB ".format( + ipv6_default_route, expected_nexthop + ) + ) + return False + + if is_ipv4_default_route_found and is_ipv6_default_route_found: + return True + else: + logger.error( + "Default Route for ipv4 and ipv6 address family is not found in FIB " + ) + return False + + +@retry(retry_timeout=5) +def verify_bgp_advertised_routes_from_neighbor(tgen, topo, dut, peer, expected_routes): + """ + APi is verifies the the routes that are advertised from dut to peer + + command used : + "sh ip bgp neighbor <x.x.x.x> advertised-routes" and + "sh ip bgp ipv6 unicast neighbor<x::x> advertised-routes" + + dut : Device Under Tests + Peer : Peer on which the routs is expected + expected_routes : dual stack IPV4-and IPv6 routes to be verified + expected_routes + + returns: True / False + + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + + peer_ipv4_neighbor_ip = topo["routers"][peer]["links"][dut]["ipv4"].split("/")[0] + peer_ipv6_neighbor_ip = topo["routers"][peer]["links"][dut]["ipv6"].split("/")[0] + + for router, rnode in tgen.routers().items(): + if router == dut: + ipv4_receieved_routes = run_frr_cmd( + rnode, + "sh ip bgp neighbor {} advertised-routes json".format( + peer_ipv4_neighbor_ip + ), + isjson=True, + ) + ipv6_receieved_routes = run_frr_cmd( + rnode, + "sh ip bgp ipv6 unicast neighbor {} advertised-routes json".format( + peer_ipv6_neighbor_ip + ), + isjson=True, + ) + ipv4_route_count = 0 + ipv6_route_count = 0 + if ipv4_receieved_routes: + for index in range(len(expected_routes["ipv4"])): + if ( + expected_routes["ipv4"][index]["network"] + in ipv4_receieved_routes["advertisedRoutes"].keys() + ): + ipv4_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is advertised to {} ".format( + dut, expected_routes["ipv4"][index]["network"], peer + ) + ) + + elif ( + expected_routes["ipv4"][index]["network"] + in ipv4_receieved_routes["bgpOriginatingDefaultNetwork"] + ): + ipv4_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is advertised to {} ".format( + dut, expected_routes["ipv4"][index]["network"], peer + ) + ) + + else: + logger.error( + "ERROR....![DUT : {}] The Expected Route {} is not advertised to {} ".format( + dut, expected_routes["ipv4"][index]["network"], peer + ) + ) + else: + logger.error(ipv4_receieved_routes) + logger.error( + "ERROR...! [DUT : {}] No IPV4 Routes are advertised to the peer {}".format( + dut, peer + ) + ) + return False + + if ipv6_receieved_routes: + for index in range(len(expected_routes["ipv6"])): + if ( + expected_routes["ipv6"][index]["network"] + in ipv6_receieved_routes["advertisedRoutes"].keys() + ): + ipv6_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is advertised to {} ".format( + dut, expected_routes["ipv6"][index]["network"], peer + ) + ) + elif ( + expected_routes["ipv6"][index]["network"] + in ipv6_receieved_routes["bgpOriginatingDefaultNetwork"] + ): + ipv6_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is advertised to {} ".format( + dut, expected_routes["ipv6"][index]["network"], peer + ) + ) + else: + logger.error( + "ERROR....![DUT : {}] The Expected Route {} is not advertised to {} ".format( + dut, expected_routes["ipv6"][index]["network"], peer + ) + ) + else: + logger.error(ipv6_receieved_routes) + logger.error( + "ERROR...! [DUT : {}] No IPV6 Routes are advertised to the peer {}".format( + dut, peer + ) + ) + return False + + if ipv4_route_count == len(expected_routes["ipv4"]) and ipv6_route_count == len( + expected_routes["ipv6"] + ): + return True + else: + logger.error( + "ERROR ....! IPV4 : Expected Routes -> {} obtained ->{} ".format( + expected_routes["ipv4"], ipv4_receieved_routes["advertisedRoutes"] + ) + ) + logger.error( + "ERROR ....! IPV6 : Expected Routes -> {} obtained ->{} ".format( + expected_routes["ipv6"], ipv6_receieved_routes["advertisedRoutes"] + ) + ) + return False + + +@retry(retry_timeout=5) +def verify_bgp_received_routes_from_neighbor(tgen, topo, dut, peer, expected_routes): + """ + API to verify the bgp received routes + + commad used : + ============= + show ip bgp neighbor <x.x.x.x> received-routes + show ip bgp ipv6 unicast neighbor <x::x> received-routes + + params + ======= + dut : Device Under Tests + Peer : Peer on which the routs is expected + expected_routes : dual stack IPV4-and IPv6 routes to be verified + expected_routes + + returns: + ======== + True / False + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + tgen = get_topogen() + + peer_ipv4_neighbor_ip = topo["routers"][peer]["links"][dut]["ipv4"].split("/")[0] + peer_ipv6_neighbor_ip = topo["routers"][peer]["links"][dut]["ipv6"].split("/")[0] + + logger.info("Enabling Soft configuration to neighbor INBOUND ") + neigbor_dict = {"ipv4": peer_ipv4_neighbor_ip, "ipv6": peer_ipv6_neighbor_ip} + result = configure_bgp_soft_configuration( + tgen, dut, neigbor_dict, direction="inbound" + ) + assert ( + result is True + ), " Failed to configure the soft configuration \n Error: {}".format(result) + + """sleep of 10 sec is required to get the routes on peer after soft configuration""" + sleep(10) + for router, rnode in tgen.routers().items(): + if router == dut: + ipv4_receieved_routes = run_frr_cmd( + rnode, + "sh ip bgp neighbor {} received-routes json".format( + peer_ipv4_neighbor_ip + ), + isjson=True, + ) + ipv6_receieved_routes = run_frr_cmd( + rnode, + "sh ip bgp ipv6 unicast neighbor {} received-routes json".format( + peer_ipv6_neighbor_ip + ), + isjson=True, + ) + ipv4_route_count = 0 + ipv6_route_count = 0 + if ipv4_receieved_routes: + for index in range(len(expected_routes["ipv4"])): + if ( + expected_routes["ipv4"][index]["network"] + in ipv4_receieved_routes["receivedRoutes"].keys() + ): + ipv4_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is received from {} ".format( + dut, expected_routes["ipv4"][index]["network"], peer + ) + ) + else: + logger.error( + "ERROR....![DUT : {}] The Expected Route {} is not received from {} ".format( + dut, expected_routes["ipv4"][index]["network"], peer + ) + ) + else: + logger.error(ipv4_receieved_routes) + logger.error( + "ERROR...! [DUT : {}] No IPV4 Routes are received from the peer {}".format( + dut, peer + ) + ) + return False + + if ipv6_receieved_routes: + for index in range(len(expected_routes["ipv6"])): + if ( + expected_routes["ipv6"][index]["network"] + in ipv6_receieved_routes["receivedRoutes"].keys() + ): + ipv6_route_count += 1 + logger.info( + "Success [DUT : {}] The Expected Route {} is received from {} ".format( + dut, expected_routes["ipv6"][index]["network"], peer + ) + ) + else: + logger.error( + "ERROR....![DUT : {}] The Expected Route {} is not received from {} ".format( + dut, expected_routes["ipv6"][index]["network"], peer + ) + ) + else: + logger.error(ipv6_receieved_routes) + logger.error( + "ERROR...! [DUT : {}] No IPV6 Routes are received from the peer {}".format( + dut, peer + ) + ) + return False + + if ipv4_route_count == len(expected_routes["ipv4"]) and ipv6_route_count == len( + expected_routes["ipv6"] + ): + return True + else: + logger.error( + "ERROR ....! IPV4 : Expected Routes -> {} obtained ->{} ".format( + expected_routes["ipv4"], ipv4_receieved_routes["advertisedRoutes"] + ) + ) + logger.error( + "ERROR ....! IPV6 : Expected Routes -> {} obtained ->{} ".format( + expected_routes["ipv6"], ipv6_receieved_routes["advertisedRoutes"] + ) + ) + return False + + +def configure_bgp_soft_configuration(tgen, dut, neighbor_dict, direction): + """ + Api to configure the bgp soft configuration to show the received routes from peer + params + ====== + dut : device under test route on which the sonfiguration to be applied + neighbor_dict : dict element contains ipv4 and ipv6 neigbor ip + direction : Directionon which it should be applied in/out + + returns: + ======== + boolean + """ + logger.info("Enabling Soft configuration to neighbor INBOUND ") + local_as = get_dut_as_number(tgen, dut) + ipv4_neighbor = neighbor_dict["ipv4"] + ipv6_neighbor = neighbor_dict["ipv6"] + direction = direction.lower() + if ipv4_neighbor and ipv4_neighbor: + raw_config = { + dut: { + "raw_config": [ + "router bgp {}".format(local_as), + "address-family ipv4 unicast", + "neighbor {} soft-reconfiguration {} ".format( + ipv4_neighbor, direction + ), + "exit-address-family", + "address-family ipv6 unicast", + "neighbor {} soft-reconfiguration {} ".format( + ipv6_neighbor, direction + ), + "exit-address-family", + ] + } + } + result = apply_raw_config(tgen, raw_config) + logger.info( + "Success... [DUT : {}] The soft configuration onis applied on neighbors {} ".format( + dut, neighbor_dict + ) + ) + return True diff --git a/tests/topotests/lib/bgprib.py b/tests/topotests/lib/bgprib.py new file mode 100644 index 0000000..35a57d0 --- /dev/null +++ b/tests/topotests/lib/bgprib.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python + +# Copyright 2018, 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 + +# +# want_rd_routes = [ +# {'rd':'10:1', 'p':'5.1.0.0/24', 'n':'1.1.1.1', 'bp': True}, +# {'rd':'10:1', 'p':'5.1.0.0/24', 'n':'1.1.1.1', 'bp': False}, +# +# {'rd':'10:3', 'p':'5.1.0.0/24', 'n':'3.3.3.3'}, +# ] +# +# ribRequireVpnRoutes('r2','Customer routes',want_rd_routes) +# +# want_unicast_routes = [ +# {'p':'5.1.0.0/24', 'n':'1.1.1.1'}, +# ] +# +# ribRequireUnicastRoutes('r1','ipv4','r1-cust1','Customer routes in vrf',want_unicast_routes) +# ribRequireUnicastRoutes('r1','ipv4','','Customer routes in default',want_unicast_routes) +# + +from lib.lutil import luCommand, luResult, LUtil +import json +import re + +# gpz: get rib in json form and compare against desired routes +class BgpRib: + def log(self, str): + LUtil.log("BgpRib: " + str) + + def routes_include_wanted(self, pfxtbl, want, debug): + # helper function to RequireVpnRoutes + for pfx in pfxtbl.keys(): + if debug: + self.log("trying pfx %s" % pfx) + if pfx != want["p"]: + if debug: + self.log("want pfx=" + want["p"] + ", not " + pfx) + continue + if debug: + self.log("have pfx=%s" % pfx) + for r in pfxtbl[pfx]: + bp = r.get("bestpath", False) + if debug: + self.log("trying route %s bp=%s" % (r, bp)) + nexthops = r["nexthops"] + for nh in nexthops: + if debug: + self.log("trying nh %s" % nh["ip"]) + if nh["ip"] == want["n"]: + if debug: + self.log("found %s" % want["n"]) + if bp == want.get("bp", bp): + return 1 + elif debug: + self.log("bestpath mismatch %s != %s" % (bp, want["bp"])) + else: + if debug: + self.log("want nh=" + want["n"] + ", not " + nh["ip"]) + if debug: + self.log("missing route: pfx=" + want["p"] + ", nh=" + want["n"]) + return 0 + + def RequireVpnRoutes(self, target, title, wantroutes, debug=0): + import json + + logstr = "RequireVpnRoutes " + str(wantroutes) + # non json form for humans + luCommand( + target, + 'vtysh -c "show bgp ipv4 vpn"', + ".", + "None", + "Get VPN RIB (non-json)", + ) + ret = luCommand( + target, + 'vtysh -c "show bgp ipv4 vpn json"', + ".*", + "None", + "Get VPN RIB (json)", + ) + if re.search(r"^\s*$", ret): + # degenerate case: empty json means no routes + if len(wantroutes) > 0: + luResult(target, False, title, logstr) + return + luResult(target, True, title, logstr) + rib = json.loads(ret) + rds = rib["routes"]["routeDistinguishers"] + for want in wantroutes: + found = 0 + if debug: + self.log("want rd %s" % want["rd"]) + for rd in rds.keys(): + if rd != want["rd"]: + continue + if debug: + self.log("found rd %s" % rd) + table = rds[rd] + if self.routes_include_wanted(table, want, debug): + found = 1 + break + if not found: + luResult(target, False, title, logstr) + return + luResult(target, True, title, logstr) + + def RequireUnicastRoutes(self, target, afi, vrf, title, wantroutes, debug=0): + logstr = "RequireUnicastRoutes %s" % str(wantroutes) + vrfstr = "" + if vrf != "": + vrfstr = "vrf %s" % (vrf) + + if (afi != "ipv4") and (afi != "ipv6"): + self.log("ERROR invalid afi") + + cmdstr = "show bgp %s %s unicast" % (vrfstr, afi) + # non json form for humans + cmd = 'vtysh -c "%s"' % cmdstr + luCommand(target, cmd, ".", "None", "Get %s %s RIB (non-json)" % (vrfstr, afi)) + cmd = 'vtysh -c "%s json"' % cmdstr + ret = luCommand( + target, cmd, ".*", "None", "Get %s %s RIB (json)" % (vrfstr, afi) + ) + if re.search(r"^\s*$", ret): + # degenerate case: empty json means no routes + if len(wantroutes) > 0: + luResult(target, False, title, logstr) + return + luResult(target, True, title, logstr) + rib = json.loads(ret) + try: + table = rib["routes"] + # KeyError: 'routes' probably means missing/bad VRF + except KeyError as err: + if vrf != "": + errstr = "-script ERROR: check if wrong vrf (%s)" % (vrf) + else: + errstr = "-script ERROR: check if vrf missing" + luResult(target, False, title + errstr, logstr) + return + # if debug: + # self.log("table=%s" % table) + for want in wantroutes: + if debug: + self.log("want=%s" % want) + if not self.routes_include_wanted(table, want, debug): + luResult(target, False, title, logstr) + return + luResult(target, True, title, logstr) + + +BgpRib = BgpRib() + + +def bgpribRequireVpnRoutes(target, title, wantroutes, debug=0): + BgpRib.RequireVpnRoutes(target, title, wantroutes, debug) + + +def bgpribRequireUnicastRoutes(target, afi, vrf, title, wantroutes, debug=0): + BgpRib.RequireUnicastRoutes(target, afi, vrf, title, wantroutes, debug) diff --git a/tests/topotests/lib/common_config.py b/tests/topotests/lib/common_config.py new file mode 100644 index 0000000..5f4c280 --- /dev/null +++ b/tests/topotests/lib/common_config.py @@ -0,0 +1,5130 @@ +# +# Copyright (c) 2019 by VMware, Inc. ("VMware") +# Used Copyright (c) 2018 by Network Device Education Foundation, Inc. +# ("NetDEF") in this file. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import ipaddress +import json +import os +import platform +import socket +import subprocess +import sys +import traceback +import functools +from collections import OrderedDict +from copy import deepcopy +from datetime import datetime, timedelta +from functools import wraps +from re import search as re_search +from time import sleep + +try: + # Imports from python2 + import ConfigParser as configparser +except ImportError: + # Imports from python3 + import configparser + +from lib.micronet import comm_error +from lib.topogen import TopoRouter, get_topogen +from lib.topolog import get_logger, logger +from lib.topotest import frr_unicode, interface_set_status, version_cmp +from lib import topotest + +FRRCFG_FILE = "frr_json.conf" +FRRCFG_BKUP_FILE = "frr_json_initial.conf" + +ERROR_LIST = ["Malformed", "Failure", "Unknown", "Incomplete"] + +#### +CD = os.path.dirname(os.path.realpath(__file__)) +PYTESTINI_PATH = os.path.join(CD, "../pytest.ini") + +# NOTE: to save execution logs to log file frrtest_log_dir must be configured +# in `pytest.ini`. +config = configparser.ConfigParser() +config.read(PYTESTINI_PATH) + +config_section = "topogen" + +# Debug logs for daemons +DEBUG_LOGS = { + "pimd": [ + "debug msdp events", + "debug msdp packets", + "debug igmp events", + "debug igmp trace", + "debug mroute", + "debug mroute detail", + "debug pim events", + "debug pim packets", + "debug pim trace", + "debug pim zebra", + "debug pim bsm", + "debug pim packets joins", + "debug pim packets register", + "debug pim nht", + ], + "bgpd": [ + "debug bgp neighbor-events", + "debug bgp updates", + "debug bgp zebra", + "debug bgp nht", + "debug bgp neighbor-events", + "debug bgp graceful-restart", + "debug bgp update-groups", + "debug bgp vpn leak-from-vrf", + "debug bgp vpn leak-to-vrf", + "debug bgp zebr", + "debug bgp updates", + "debug bgp nht", + "debug bgp neighbor-events", + "debug vrf", + ], + "zebra": [ + "debug zebra events", + "debug zebra rib", + "debug zebra vxlan", + "debug zebra nht", + ], + "ospf": [ + "debug ospf event", + "debug ospf ism", + "debug ospf lsa", + "debug ospf nsm", + "debug ospf nssa", + "debug ospf packet all", + "debug ospf sr", + "debug ospf te", + "debug ospf zebra", + ], + "ospf6": [ + "debug ospf6 event", + "debug ospf6 ism", + "debug ospf6 lsa", + "debug ospf6 nsm", + "debug ospf6 nssa", + "debug ospf6 packet all", + "debug ospf6 sr", + "debug ospf6 te", + "debug ospf6 zebra", + ], +} + +g_iperf_client_procs = {} +g_iperf_server_procs = {} + + +def is_string(value): + try: + return isinstance(value, basestring) + except NameError: + return isinstance(value, str) + + +if config.has_option("topogen", "verbosity"): + loglevel = config.get("topogen", "verbosity") + loglevel = loglevel.lower() +else: + loglevel = "info" + +if config.has_option("topogen", "frrtest_log_dir"): + frrtest_log_dir = config.get("topogen", "frrtest_log_dir") + time_stamp = datetime.time(datetime.now()) + logfile_name = "frr_test_bgp_" + frrtest_log_file = frrtest_log_dir + logfile_name + str(time_stamp) + print("frrtest_log_file..", frrtest_log_file) + + logger = get_logger( + "test_execution_logs", log_level=loglevel, target=frrtest_log_file + ) + print("Logs will be sent to logfile: {}".format(frrtest_log_file)) + +if config.has_option("topogen", "show_router_config"): + show_router_config = config.get("topogen", "show_router_config") +else: + show_router_config = False + +# env variable for setting what address type to test +ADDRESS_TYPES = os.environ.get("ADDRESS_TYPES") + + +# Saves sequence id numbers +SEQ_ID = {"prefix_lists": {}, "route_maps": {}} + + +def get_seq_id(obj_type, router, obj_name): + """ + Generates and saves sequence number in interval of 10 + Parameters + ---------- + * `obj_type`: prefix_lists or route_maps + * `router`: router name + *` obj_name`: name of the prefix-list or route-map + Returns + -------- + Sequence number generated + """ + + router_data = SEQ_ID[obj_type].setdefault(router, {}) + obj_data = router_data.setdefault(obj_name, {}) + seq_id = obj_data.setdefault("seq_id", 0) + + seq_id = int(seq_id) + 10 + obj_data["seq_id"] = seq_id + + return seq_id + + +def set_seq_id(obj_type, router, id, obj_name): + """ + Saves sequence number if not auto-generated and given by user + Parameters + ---------- + * `obj_type`: prefix_lists or route_maps + * `router`: router name + *` obj_name`: name of the prefix-list or route-map + """ + router_data = SEQ_ID[obj_type].setdefault(router, {}) + obj_data = router_data.setdefault(obj_name, {}) + seq_id = obj_data.setdefault("seq_id", 0) + + seq_id = int(seq_id) + int(id) + obj_data["seq_id"] = seq_id + + +class InvalidCLIError(Exception): + """Raise when the CLI command is wrong""" + + +def run_frr_cmd(rnode, cmd, isjson=False): + """ + Execute frr show commands in privileged mode + * `rnode`: router node on which command needs to be executed + * `cmd`: Command to be executed on frr + * `isjson`: If command is to get json data or not + :return str: + """ + + if cmd: + ret_data = rnode.vtysh_cmd(cmd, isjson=isjson) + + if isjson: + rnode.vtysh_cmd(cmd.rstrip("json"), isjson=False) + + return ret_data + + else: + raise InvalidCLIError("No actual cmd passed") + + +def apply_raw_config(tgen, input_dict): + + """ + API to configure raw configuration on device. This can be used for any cli + which has not been implemented in JSON. + + Parameters + ---------- + * `tgen`: tgen object + * `input_dict`: configuration that needs to be applied + + Usage + ----- + input_dict = { + "r2": { + "raw_config": [ + "router bgp", + "no bgp update-group-split-horizon" + ] + } + } + Returns + ------- + True or errormsg + """ + + rlist = [] + + for router_name in input_dict.keys(): + config_cmd = input_dict[router_name]["raw_config"] + + if not isinstance(config_cmd, list): + config_cmd = [config_cmd] + + frr_cfg_file = "{}/{}/{}".format(tgen.logdir, router_name, FRRCFG_FILE) + with open(frr_cfg_file, "w") as cfg: + for cmd in config_cmd: + cfg.write("{}\n".format(cmd)) + + rlist.append(router_name) + + # Load config on all routers + return load_config_to_routers(tgen, rlist) + + +def create_common_configurations( + tgen, config_dict, config_type=None, build=False, load_config=True +): + """ + API to create object of class FRRConfig and also create frr_json.conf + file. It will create interface and common configurations and save it to + frr_json.conf and load to router + Parameters + ---------- + * `tgen`: tgen object + * `config_dict`: Configuration data saved in a dict of { router: config-list } + * `routers` : list of router id to be configured. + * `config_type` : Syntactic information while writing configuration. Should + be one of the value as mentioned in the config_map below. + * `build` : Only for initial setup phase this is set as True + Returns + ------- + True or False + """ + + config_map = OrderedDict( + { + "general_config": "! FRR General Config\n", + "debug_log_config": "! Debug log Config\n", + "interface_config": "! Interfaces Config\n", + "static_route": "! Static Route Config\n", + "prefix_list": "! Prefix List Config\n", + "bgp_community_list": "! Community List Config\n", + "route_maps": "! Route Maps Config\n", + "bgp": "! BGP Config\n", + "vrf": "! VRF Config\n", + "ospf": "! OSPF Config\n", + "ospf6": "! OSPF Config\n", + "pim": "! PIM Config\n", + } + ) + + if build: + mode = "a" + elif not load_config: + mode = "a" + else: + mode = "w" + + routers = config_dict.keys() + for router in routers: + fname = "{}/{}/{}".format(tgen.logdir, router, FRRCFG_FILE) + try: + frr_cfg_fd = open(fname, mode) + if config_type: + frr_cfg_fd.write(config_map[config_type]) + for line in config_dict[router]: + frr_cfg_fd.write("{} \n".format(str(line))) + frr_cfg_fd.write("\n") + + except IOError as err: + logger.error("Unable to open FRR Config '%s': %s" % (fname, str(err))) + return False + finally: + frr_cfg_fd.close() + + # If configuration applied from build, it will done at last + result = True + if not build and load_config: + result = load_config_to_routers(tgen, routers) + + return result + + +def create_common_configuration( + tgen, router, data, config_type=None, build=False, load_config=True +): + """ + API to create object of class FRRConfig and also create frr_json.conf + file. It will create interface and common configurations and save it to + frr_json.conf and load to router + Parameters + ---------- + * `tgen`: tgen object + * `data`: Configuration data saved in a list. + * `router` : router id to be configured. + * `config_type` : Syntactic information while writing configuration. Should + be one of the value as mentioned in the config_map below. + * `build` : Only for initial setup phase this is set as True + Returns + ------- + True or False + """ + return create_common_configurations( + tgen, {router: data}, config_type, build, load_config + ) + + +def kill_router_daemons(tgen, router, daemons, save_config=True): + """ + Router's current config would be saved to /etc/frr/ for each daemon + and daemon would be killed forcefully using SIGKILL. + * `tgen` : topogen object + * `router`: Device under test + * `daemons`: list of daemons to be killed + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + router_list = tgen.routers() + + if save_config: + # Saving router config to /etc/frr, which will be loaded to router + # when it starts + router_list[router].vtysh_cmd("write memory") + + # Kill Daemons + result = router_list[router].killDaemons(daemons) + if len(result) > 0: + assert "Errors found post shutdown - details follow:" == 0, result + return result + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + +def start_router_daemons(tgen, router, daemons): + """ + Daemons defined by user would be started + * `tgen` : topogen object + * `router`: Device under test + * `daemons`: list of daemons to be killed + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + router_list = tgen.routers() + + # Start daemons + res = router_list[router].startDaemons(daemons) + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + res = errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return res + + +def check_router_status(tgen): + """ + Check if all daemons are running for all routers in topology + * `tgen` : topogen object + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + router_list = tgen.routers() + for router, rnode in router_list.items(): + + result = rnode.check_router_running() + if result != "": + daemons = [] + if "bgpd" in result: + daemons.append("bgpd") + if "zebra" in result: + daemons.append("zebra") + if "pimd" in result: + daemons.append("pimd") + if "pim6d" in result: + daemons.append("pim6d") + if "ospfd" in result: + daemons.append("ospfd") + if "ospf6d" in result: + daemons.append("ospf6d") + rnode.startDaemons(daemons) + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def save_initial_config_on_routers(tgen): + """Save current configuration on routers to FRRCFG_BKUP_FILE. + + FRRCFG_BKUP_FILE is the file that will be restored when `reset_config_on_routers()` + is called. + + Parameters + ---------- + * `tgen` : Topogen object + """ + router_list = tgen.routers() + target_cfg_fmt = tgen.logdir + "/{}/frr_json_initial.conf" + + # Get all running configs in parallel + procs = {} + for rname in router_list: + logger.info("Fetching running config for router %s", rname) + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-c", "show running-config no-header"], + stdin=None, + stdout=open(target_cfg_fmt.format(rname), "w"), + stderr=subprocess.PIPE, + ) + for rname, p in procs.items(): + _, error = p.communicate() + if p.returncode: + logger.error( + "Get running config for %s failed %d: %s", rname, p.returncode, error + ) + raise InvalidCLIError( + "vtysh show running error on {}: {}".format(rname, error) + ) + + +def reset_config_on_routers(tgen, routerName=None): + """ + Resets configuration on routers to the snapshot created using input JSON + file. It replaces existing router configuration with FRRCFG_BKUP_FILE + + Parameters + ---------- + * `tgen` : Topogen object + * `routerName` : router config is to be reset + """ + + logger.debug("Entering API: reset_config_on_routers") + + tgen.cfg_gen += 1 + gen = tgen.cfg_gen + + # Trim the router list if needed + router_list = tgen.routers() + if routerName: + if routerName not in router_list: + logger.warning( + "Exiting API: reset_config_on_routers: no router %s", + routerName, + exc_info=True, + ) + return True + router_list = {routerName: router_list[routerName]} + + delta_fmt = tgen.logdir + "/{}/delta-{}.conf" + # FRRCFG_BKUP_FILE + target_cfg_fmt = tgen.logdir + "/{}/frr_json_initial.conf" + run_cfg_fmt = tgen.logdir + "/{}/frr-{}.sav" + + # + # Get all running configs in parallel + # + procs = {} + for rname in router_list: + logger.info("Fetching running config for router %s", rname) + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-c", "show running-config no-header"], + stdin=None, + stdout=open(run_cfg_fmt.format(rname, gen), "w"), + stderr=subprocess.PIPE, + ) + for rname, p in procs.items(): + _, error = p.communicate() + if p.returncode: + logger.error( + "Get running config for %s failed %d: %s", rname, p.returncode, error + ) + raise InvalidCLIError( + "vtysh show running error on {}: {}".format(rname, error) + ) + + # + # Get all delta's in parallel + # + procs = {} + for rname in router_list: + logger.info( + "Generating delta for router %s to new configuration (gen %d)", rname, gen + ) + procs[rname] = tgen.net.popen( + [ + "/usr/lib/frr/frr-reload.py", + "--test-reset", + "--input", + run_cfg_fmt.format(rname, gen), + "--test", + target_cfg_fmt.format(rname), + ], + stdin=None, + stdout=open(delta_fmt.format(rname, gen), "w"), + stderr=subprocess.PIPE, + ) + for rname, p in procs.items(): + _, error = p.communicate() + if p.returncode: + logger.error( + "Delta file creation for %s failed %d: %s", rname, p.returncode, error + ) + raise InvalidCLIError("frr-reload error for {}: {}".format(rname, error)) + + # + # Apply all the deltas in parallel + # + procs = {} + for rname in router_list: + logger.info("Applying delta config on router %s", rname) + + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-f", delta_fmt.format(rname, gen)], + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + for rname, p in procs.items(): + output, _ = p.communicate() + vtysh_command = "vtysh -f {}".format(delta_fmt.format(rname, gen)) + if not p.returncode: + router_list[rname].logger.info( + '\nvtysh config apply => "{}"\nvtysh output <= "{}"'.format( + vtysh_command, output + ) + ) + else: + router_list[rname].logger.warning( + '\nvtysh config apply failed => "{}"\nvtysh output <= "{}"'.format( + vtysh_command, output + ) + ) + logger.error( + "Delta file apply for %s failed %d: %s", rname, p.returncode, output + ) + + # We really need to enable this failure; however, currently frr-reload.py + # producing invalid "no" commands as it just preprends "no", but some of the + # command forms lack matching values (e.g., final values). Until frr-reload + # is fixed to handle this (or all the CLI no forms are adjusted) we can't + # fail tests. + # raise InvalidCLIError("frr-reload error for {}: {}".format(rname, output)) + + # + # Optionally log all new running config if "show_router_config" is defined in + # "pytest.ini" + # + if show_router_config: + procs = {} + for rname in router_list: + logger.info("Fetching running config for router %s", rname) + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-c", "show running-config no-header"], + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + for rname, p in procs.items(): + output, _ = p.communicate() + if p.returncode: + logger.warning( + "Get running config for %s failed %d: %s", + rname, + p.returncode, + output, + ) + else: + logger.info( + "Configuration on router %s after reset:\n%s", rname, output + ) + + logger.debug("Exiting API: reset_config_on_routers") + return True + + +def prep_load_config_to_routers(tgen, *config_name_list): + """Create common config for `load_config_to_routers`. + + The common config file is constructed from the list of sub-config files passed as + position arguments to this function. Each entry in `config_name_list` is looked for + under the router sub-directory in the test directory and those files are + concatenated together to create the common config. e.g., + + # Routers are "r1" and "r2", test file is `example/test_example_foo.py` + prepare_load_config_to_routers(tgen, "bgpd.conf", "ospfd.conf") + + When the above call is made the files in + + example/r1/bgpd.conf + example/r1/ospfd.conf + + Are concat'd together into a single config file that will be loaded on r1, and + + example/r2/bgpd.conf + example/r2/ospfd.conf + + Are concat'd together into a single config file that will be loaded on r2 when + the call to `load_config_to_routers` is made. + """ + + routers = tgen.routers() + for rname, router in routers.items(): + destname = "{}/{}/{}".format(tgen.logdir, rname, FRRCFG_FILE) + wmode = "w" + for cfbase in config_name_list: + script_dir = os.environ["PYTEST_TOPOTEST_SCRIPTDIR"] + confname = os.path.join(script_dir, "{}/{}".format(rname, cfbase)) + with open(confname, "r") as cf: + with open(destname, wmode) as df: + df.write(cf.read()) + wmode = "a" + + +def load_config_to_routers(tgen, routers, save_bkup=False): + """ + Loads configuration on routers from the file FRRCFG_FILE. + + Parameters + ---------- + * `tgen` : Topogen object + * `routers` : routers for which configuration is to be loaded + * `save_bkup` : If True, Saves snapshot of FRRCFG_FILE to FRRCFG_BKUP_FILE + Returns + ------- + True or False + """ + + logger.debug("Entering API: load_config_to_routers") + + tgen.cfg_gen += 1 + gen = tgen.cfg_gen + + base_router_list = tgen.routers() + router_list = {} + for router in routers: + if router not in base_router_list: + continue + router_list[router] = base_router_list[router] + + frr_cfg_file_fmt = tgen.logdir + "/{}/" + FRRCFG_FILE + frr_cfg_save_file_fmt = tgen.logdir + "/{}/{}-" + FRRCFG_FILE + frr_cfg_bkup_fmt = tgen.logdir + "/{}/" + FRRCFG_BKUP_FILE + + procs = {} + for rname in router_list: + router = router_list[rname] + try: + frr_cfg_file = frr_cfg_file_fmt.format(rname) + frr_cfg_save_file = frr_cfg_save_file_fmt.format(rname, gen) + frr_cfg_bkup = frr_cfg_bkup_fmt.format(rname) + with open(frr_cfg_file, "r+") as cfg: + data = cfg.read() + logger.info( + "Applying following configuration on router %s (gen: %d):\n%s", + rname, + gen, + data, + ) + # Always save a copy of what we just did + with open(frr_cfg_save_file, "w") as bkup: + bkup.write(data) + if save_bkup: + with open(frr_cfg_bkup, "w") as bkup: + bkup.write(data) + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-f", frr_cfg_file], + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + except IOError as err: + logger.error( + "Unable to open config File. error(%s): %s", err.errno, err.strerror + ) + return False + except Exception as error: + logger.error("Unable to apply config on %s: %s", rname, str(error)) + return False + + errors = [] + for rname, p in procs.items(): + output, _ = p.communicate() + frr_cfg_file = frr_cfg_file_fmt.format(rname) + vtysh_command = "vtysh -f " + frr_cfg_file + if not p.returncode: + router_list[rname].logger.info( + '\nvtysh config apply => "{}"\nvtysh output <= "{}"'.format( + vtysh_command, output + ) + ) + else: + router_list[rname].logger.error( + '\nvtysh config apply failed => "{}"\nvtysh output <= "{}"'.format( + vtysh_command, output + ) + ) + logger.error( + "Config apply for %s failed %d: %s", rname, p.returncode, output + ) + # We can't thorw an exception here as we won't clear the config file. + errors.append( + InvalidCLIError( + "load_config_to_routers error for {}: {}".format(rname, output) + ) + ) + + # Empty the config file or we append to it next time through. + with open(frr_cfg_file, "r+") as cfg: + cfg.truncate(0) + + # Router current configuration to log file or console if + # "show_router_config" is defined in "pytest.ini" + if show_router_config: + procs = {} + for rname in router_list: + procs[rname] = router_list[rname].popen( + ["/usr/bin/env", "vtysh", "-c", "show running-config no-header"], + stdin=None, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + ) + for rname, p in procs.items(): + output, _ = p.communicate() + if p.returncode: + logger.warning( + "Get running config for %s failed %d: %s", + rname, + p.returncode, + output, + ) + else: + logger.info("New configuration for router %s:\n%s", rname, output) + + logger.debug("Exiting API: load_config_to_routers") + return not errors + + +def load_config_to_router(tgen, routerName, save_bkup=False): + """ + Loads configuration on router from the file FRRCFG_FILE. + + Parameters + ---------- + * `tgen` : Topogen object + * `routerName` : router for which configuration to be loaded + * `save_bkup` : If True, Saves snapshot of FRRCFG_FILE to FRRCFG_BKUP_FILE + """ + return load_config_to_routers(tgen, [routerName], save_bkup) + + +def reset_with_new_configs(tgen, *cflist): + """Reset the router to initial config, then load new configs. + + Resets routers to the initial config state (see `save_initial_config_on_routers() + and `reset_config_on_routers()` `), then concat list of router sub-configs together + and load onto the routers (see `prep_load_config_to_routers()` and + `load_config_to_routers()`) + """ + routers = tgen.routers() + + reset_config_on_routers(tgen) + prep_load_config_to_routers(tgen, *cflist) + load_config_to_routers(tgen, tgen.routers(), save_bkup=False) + + +def get_frr_ipv6_linklocal(tgen, router, intf=None, vrf=None): + """ + API to get the link local ipv6 address of a particular interface using + FRR command 'show interface' + + * `tgen`: tgen object + * `router` : router for which highest interface should be + calculated + * `intf` : interface for which link-local address needs to be taken + * `vrf` : VRF name + + Usage + ----- + linklocal = get_frr_ipv6_linklocal(tgen, router, "intf1", RED_A) + + Returns + ------- + 1) array of interface names to link local ips. + """ + + router_list = tgen.routers() + for rname, rnode in router_list.items(): + if rname != router: + continue + + linklocal = [] + + if vrf: + cmd = "show interface vrf {}".format(vrf) + else: + cmd = "show interface" + + linklocal = [] + if vrf: + cmd = "show interface vrf {}".format(vrf) + else: + cmd = "show interface" + for chk_ll in range(0, 60): + sleep(1 / 4) + ifaces = router_list[router].run('vtysh -c "{}"'.format(cmd)) + # Fix newlines (make them all the same) + ifaces = ("\n".join(ifaces.splitlines()) + "\n").splitlines() + + interface = None + ll_per_if_count = 0 + for line in ifaces: + # Interface name + m = re_search("Interface ([a-zA-Z0-9-]+) is", line) + if m: + interface = m.group(1).split(" ")[0] + ll_per_if_count = 0 + + # Interface ip + m1 = re_search("inet6 (fe80[:a-fA-F0-9]+/[0-9]+)", line) + if m1: + local = m1.group(1) + ll_per_if_count += 1 + if ll_per_if_count > 1: + linklocal += [["%s-%s" % (interface, ll_per_if_count), local]] + else: + linklocal += [[interface, local]] + + try: + if linklocal: + if intf: + return [ + _linklocal[1] + for _linklocal in linklocal + if _linklocal[0] == intf + ][0].split("/")[0] + return linklocal + except IndexError: + continue + + errormsg = "Link local ip missing on router {}".format(router) + return errormsg + + +def generate_support_bundle(): + """ + API to generate support bundle on any verification ste failure. + it runs a python utility, /usr/lib/frr/generate_support_bundle.py, + which basically runs defined CLIs and dumps the data to specified location + """ + + tgen = get_topogen() + router_list = tgen.routers() + test_name = os.environ.get("PYTEST_CURRENT_TEST").split(":")[-1].split(" ")[0] + + bundle_procs = {} + for rname, rnode in router_list.items(): + logger.info("Spawn collection of support bundle for %s", rname) + dst_bundle = "{}/{}/support_bundles/{}".format(tgen.logdir, rname, test_name) + rnode.run("mkdir -p " + dst_bundle) + + gen_sup_cmd = [ + "/usr/lib/frr/generate_support_bundle.py", + "--log-dir=" + dst_bundle, + ] + bundle_procs[rname] = tgen.net[rname].popen(gen_sup_cmd, stdin=None) + + for rname, rnode in router_list.items(): + logger.info("Waiting on support bundle for %s", rname) + output, error = bundle_procs[rname].communicate() + if output: + logger.info( + "Output from collecting support bundle for %s:\n%s", rname, output + ) + if error: + logger.warning( + "Error from collecting support bundle for %s:\n%s", rname, error + ) + + return True + + +def start_topology(tgen, daemon=None): + """ + Starting topology, create tmp files which are loaded to routers + to start daemons and then start routers + * `tgen` : topogen object + """ + + # Starting topology + tgen.start_topology() + + # Starting daemons + + router_list = tgen.routers() + routers_sorted = sorted( + router_list.keys(), key=lambda x: int(re_search("[0-9]+", x).group(0)) + ) + + linux_ver = "" + router_list = tgen.routers() + for rname in routers_sorted: + router = router_list[rname] + + # It will help in debugging the failures, will give more details on which + # specific kernel version tests are failing + if linux_ver == "": + linux_ver = router.run("uname -a") + logger.info("Logging platform related details: \n %s \n", linux_ver) + + try: + os.chdir(tgen.logdir) + + # # Creating router named dir and empty zebra.conf bgpd.conf files + # # inside the current directory + # if os.path.isdir("{}".format(rname)): + # os.system("rm -rf {}".format(rname)) + # os.mkdir("{}".format(rname)) + # os.system("chmod -R go+rw {}".format(rname)) + # os.chdir("{}/{}".format(tgen.logdir, rname)) + # os.system("touch zebra.conf bgpd.conf") + # else: + # os.mkdir("{}".format(rname)) + # os.system("chmod -R go+rw {}".format(rname)) + # os.chdir("{}/{}".format(tgen.logdir, rname)) + # os.system("touch zebra.conf bgpd.conf") + + except IOError as err: + logger.error("I/O error({0}): {1}".format(err.errno, err.strerror)) + + # Loading empty zebra.conf file to router, to start the zebra daemon + router.load_config( + TopoRouter.RD_ZEBRA, "{}/{}/zebra.conf".format(tgen.logdir, rname) + ) + + # Loading empty bgpd.conf file to router, to start the bgp daemon + router.load_config( + TopoRouter.RD_BGP, "{}/{}/bgpd.conf".format(tgen.logdir, rname) + ) + + if daemon and "ospfd" in daemon: + # Loading empty ospf.conf file to router, to start the bgp daemon + router.load_config( + TopoRouter.RD_OSPF, "{}/{}/ospfd.conf".format(tgen.logdir, rname) + ) + + if daemon and "ospf6d" in daemon: + # Loading empty ospf.conf file to router, to start the bgp daemon + router.load_config( + TopoRouter.RD_OSPF6, "{}/{}/ospf6d.conf".format(tgen.logdir, rname) + ) + + if daemon and "pimd" in daemon: + # Loading empty pimd.conf file to router, to start the pim deamon + router.load_config( + TopoRouter.RD_PIM, "{}/{}/pimd.conf".format(tgen.logdir, rname) + ) + + if daemon and "pim6d" in daemon: + # Loading empty pimd.conf file to router, to start the pim6d deamon + router.load_config( + TopoRouter.RD_PIM6, "{}/{}/pim6d.conf".format(tgen.logdir, rname) + ) + + # Starting routers + logger.info("Starting all routers once topology is created") + tgen.start_router() + + +def stop_router(tgen, router): + """ + Router"s current config would be saved to /tmp/topotest/<suite>/<router> for each daemon + and router and its daemons would be stopped. + + * `tgen` : topogen object + * `router`: Device under test + """ + + router_list = tgen.routers() + + # Saving router config to /etc/frr, which will be loaded to router + # when it starts + router_list[router].vtysh_cmd("write memory") + + # Stop router + router_list[router].stop() + + +def start_router(tgen, router): + """ + Router will be started and config would be loaded from /tmp/topotest/<suite>/<router> for each + daemon + + * `tgen` : topogen object + * `router`: Device under test + """ + + logger.debug("Entering lib API: start_router") + + try: + router_list = tgen.routers() + + # Router and its daemons would be started and config would + # be loaded to router for each daemon from /etc/frr + router_list[router].start() + + # Waiting for router to come up + sleep(5) + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: start_router()") + return True + + +def number_to_row(routerName): + """ + Returns the number for the router. + Calculation based on name a0 = row 0, a1 = row 1, b2 = row 2, z23 = row 23 + etc + """ + return int(routerName[1:]) + + +def number_to_column(routerName): + """ + Returns the number for the router. + Calculation based on name a0 = columnn 0, a1 = column 0, b2= column 1, + z23 = column 26 etc + """ + return ord(routerName[0]) - 97 + + +def topo_daemons(tgen, topo=None): + """ + Returns daemon list required for the suite based on topojson. + """ + daemon_list = [] + + if topo is None: + topo = tgen.json_topo + + router_list = tgen.routers() + routers_sorted = sorted( + router_list.keys(), key=lambda x: int(re_search("[0-9]+", x).group(0)) + ) + + for rtr in routers_sorted: + if "ospf" in topo["routers"][rtr] and "ospfd" not in daemon_list: + daemon_list.append("ospfd") + + if "ospf6" in topo["routers"][rtr] and "ospf6d" not in daemon_list: + daemon_list.append("ospf6d") + + for val in topo["routers"][rtr]["links"].values(): + if "pim" in val and "pimd" not in daemon_list: + daemon_list.append("pimd") + if "pim6" in val and "pim6d" not in daemon_list: + daemon_list.append("pim6d") + if "ospf" in val and "ospfd" not in daemon_list: + daemon_list.append("ospfd") + if "ospf6" in val and "ospf6d" not in daemon_list: + daemon_list.append("ospf6d") + break + + return daemon_list + + +def add_interfaces_to_vlan(tgen, input_dict): + """ + Add interfaces to VLAN, we need vlan pakcage to be installed on machine + + * `tgen`: tgen onject + * `input_dict` : interfaces to be added to vlans + + input_dict= { + "r1":{ + "vlan":{ + VLAN_1: [{ + intf_r1_s1: { + "ip": "10.1.1.1", + "subnet": "255.255.255.0 + } + }] + } + } + } + + add_interfaces_to_vlan(tgen, input_dict) + + """ + + router_list = tgen.routers() + for dut in input_dict.keys(): + rnode = router_list[dut] + + if "vlan" in input_dict[dut]: + for vlan, interfaces in input_dict[dut]["vlan"].items(): + for intf_dict in interfaces: + for interface, data in intf_dict.items(): + # Adding interface to VLAN + vlan_intf = "{}.{}".format(interface, vlan) + cmd = "ip link add link {} name {} type vlan id {}".format( + interface, vlan_intf, vlan + ) + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + result = rnode.run(cmd) + logger.info("result %s", result) + + # Bringing interface up + cmd = "ip link set {} up".format(vlan_intf) + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + result = rnode.run(cmd) + logger.info("result %s", result) + + # Assigning IP address + ifaddr = ipaddress.ip_interface( + "{}/{}".format( + frr_unicode(data["ip"]), frr_unicode(data["subnet"]) + ) + ) + + cmd = "ip -{0} a flush {1} scope global && ip a add {2} dev {1} && ip l set {1} up".format( + ifaddr.version, vlan_intf, ifaddr + ) + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + result = rnode.run(cmd) + logger.info("result %s", result) + + +def tcpdump_capture_start( + tgen, + router, + intf, + protocol=None, + grepstr=None, + timeout=0, + options=None, + cap_file=None, + background=True, +): + """ + API to capture network packets using tcp dump. + + Packages used : + + Parameters + ---------- + * `tgen`: topogen object. + * `router`: router on which ping has to be performed. + * `intf` : interface for capture. + * `protocol` : protocol for which packet needs to be captured. + * `grepstr` : string to filter out tcp dump output. + * `timeout` : Time for which packet needs to be captured. + * `options` : options for TCP dump, all tcpdump options can be used. + * `cap_file` : filename to store capture dump. + * `background` : Make tcp dump run in back ground. + + Usage + ----- + tcpdump_result = tcpdump_dut(tgen, 'r2', intf, protocol='tcp', timeout=20, + options='-A -vv -x > r2bgp.txt ') + Returns + ------- + 1) True for successful capture + 2) errormsg - when tcp dump fails + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + rnode = tgen.gears[router] + + if timeout > 0: + cmd = "timeout {}".format(timeout) + else: + cmd = "" + + cmdargs = "{} tcpdump".format(cmd) + + if intf: + cmdargs += " -i {}".format(str(intf)) + if protocol: + cmdargs += " {}".format(str(protocol)) + if options: + cmdargs += " -s 0 {}".format(str(options)) + + if cap_file: + file_name = os.path.join(tgen.logdir, router, cap_file) + cmdargs += " -w {}".format(str(file_name)) + # Remove existing capture file + rnode.run("rm -rf {}".format(file_name)) + + if grepstr: + cmdargs += ' | grep "{}"'.format(str(grepstr)) + + logger.info("Running tcpdump command: [%s]", cmdargs) + if not background: + rnode.run(cmdargs) + else: + # XXX this & is bogus doesn't work + # rnode.run("nohup {} & /dev/null 2>&1".format(cmdargs)) + rnode.run("nohup {} > /dev/null 2>&1".format(cmdargs)) + + # Check if tcpdump process is running + if background: + result = rnode.run("pgrep tcpdump") + logger.debug("ps -ef | grep tcpdump \n {}".format(result)) + + if not result: + errormsg = "tcpdump is not running {}".format("tcpdump") + return errormsg + else: + logger.info("Packet capture started on %s: interface %s", router, intf) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def tcpdump_capture_stop(tgen, router): + """ + API to capture network packets using tcp dump. + + Packages used : + + Parameters + ---------- + * `tgen`: topogen object. + * `router`: router on which ping has to be performed. + * `intf` : interface for capture. + * `protocol` : protocol for which packet needs to be captured. + * `grepstr` : string to filter out tcp dump output. + * `timeout` : Time for which packet needs to be captured. + * `options` : options for TCP dump, all tcpdump options can be used. + * `cap2file` : filename to store capture dump. + * `bakgrnd` : Make tcp dump run in back ground. + + Usage + ----- + tcpdump_result = tcpdump_dut(tgen, 'r2', intf, protocol='tcp', timeout=20, + options='-A -vv -x > r2bgp.txt ') + Returns + ------- + 1) True for successful capture + 2) errormsg - when tcp dump fails + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + rnode = tgen.gears[router] + + # Check if tcpdump process is running + result = rnode.run("ps -ef | grep tcpdump") + logger.debug("ps -ef | grep tcpdump \n {}".format(result)) + + if not re_search(r"{}".format("tcpdump"), result): + errormsg = "tcpdump is not running {}".format("tcpdump") + return errormsg + else: + # XXX this doesn't work with micronet + ppid = tgen.net.nameToNode[rnode.name].pid + rnode.run("set +m; pkill -P %s tcpdump &> /dev/null" % ppid) + logger.info("Stopped tcpdump capture") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def create_debug_log_config(tgen, input_dict, build=False): + """ + Enable/disable debug logs for any protocol with defined debug + options and logs would be saved to created log file + + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : details to enable debug logs for protocols + * `build` : Only for initial setup phase this is set as True. + + + Usage: + ------ + input_dict = { + "r2": { + "debug":{ + "log_file" : "debug.log", + "enable": ["pimd", "zebra"], + "disable": { + "bgpd":[ + 'debug bgp neighbor-events', + 'debug bgp updates', + 'debug bgp zebra', + ] + } + } + } + } + + result = create_debug_log_config(tgen, input_dict) + + Returns + ------- + True or False + """ + + result = False + try: + debug_config_dict = {} + + for router in input_dict.keys(): + debug_config = [] + if "debug" in input_dict[router]: + debug_dict = input_dict[router]["debug"] + + disable_logs = debug_dict.setdefault("disable", None) + enable_logs = debug_dict.setdefault("enable", None) + log_file = debug_dict.setdefault("log_file", None) + + if log_file: + _log_file = os.path.join(tgen.logdir, log_file) + debug_config.append("log file {} \n".format(_log_file)) + + if type(enable_logs) is list: + for daemon in enable_logs: + for debug_log in DEBUG_LOGS[daemon]: + debug_config.append("{}".format(debug_log)) + elif type(enable_logs) is dict: + for daemon, debug_logs in enable_logs.items(): + for debug_log in debug_logs: + debug_config.append("{}".format(debug_log)) + + if type(disable_logs) is list: + for daemon in disable_logs: + for debug_log in DEBUG_LOGS[daemon]: + debug_config.append("no {}".format(debug_log)) + elif type(disable_logs) is dict: + for daemon, debug_logs in disable_logs.items(): + for debug_log in debug_logs: + debug_config.append("no {}".format(debug_log)) + if debug_config: + debug_config_dict[router] = debug_config + + result = create_common_configurations( + tgen, debug_config_dict, "debug_log_config", build=build + ) + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +############################################# +# Common APIs, will be used by all protocols +############################################# + + +def create_vrf_cfg(tgen, topo, input_dict=None, build=False): + """ + Create vrf configuration for created topology. VRF + configuration is provided in input json file. + + VRF config is done in Linux Kernel: + * Create VRF + * Attach interface to VRF + * Bring up VRF + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring + from testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict={ + "r3": { + "links": { + "r2-link1": {"ipv4": "auto", "ipv6": "auto", "vrf": "RED_A"}, + "r2-link2": {"ipv4": "auto", "ipv6": "auto", "vrf": "RED_B"}, + "r2-link3": {"ipv4": "auto", "ipv6": "auto", "vrf": "BLUE_A"}, + "r2-link4": {"ipv4": "auto", "ipv6": "auto", "vrf": "BLUE_B"}, + }, + "vrfs":[ + { + "name": "RED_A", + "id": "1" + }, + { + "name": "RED_B", + "id": "2" + }, + { + "name": "BLUE_A", + "id": "3", + "delete": True + }, + { + "name": "BLUE_B", + "id": "4" + } + ] + } + } + result = create_vrf_cfg(tgen, topo, input_dict) + + Returns + ------- + True or False + """ + result = True + if not input_dict: + input_dict = deepcopy(topo) + else: + input_dict = deepcopy(input_dict) + + try: + config_data_dict = {} + + for c_router, c_data in input_dict.items(): + rnode = tgen.gears[c_router] + config_data = [] + if "vrfs" in c_data: + for vrf in c_data["vrfs"]: + name = vrf.setdefault("name", None) + table_id = vrf.setdefault("id", None) + del_action = vrf.setdefault("delete", False) + + if del_action: + # Kernel cmd- Add VRF and table + cmd = "ip link del {} type vrf table {}".format( + vrf["name"], vrf["id"] + ) + + logger.info("[DUT: %s]: Running kernel cmd [%s]", c_router, cmd) + rnode.run(cmd) + + # Kernel cmd - Bring down VRF + cmd = "ip link set dev {} down".format(name) + logger.info("[DUT: %s]: Running kernel cmd [%s]", c_router, cmd) + rnode.run(cmd) + + else: + if name and table_id: + # Kernel cmd- Add VRF and table + cmd = "ip link add {} type vrf table {}".format( + name, table_id + ) + logger.info( + "[DUT: %s]: Running kernel cmd " "[%s]", c_router, cmd + ) + rnode.run(cmd) + + # Kernel cmd - Bring up VRF + cmd = "ip link set dev {} up".format(name) + logger.info( + "[DUT: %s]: Running kernel " "cmd [%s]", c_router, cmd + ) + rnode.run(cmd) + + for vrf in c_data["vrfs"]: + vni = vrf.setdefault("vni", None) + del_vni = vrf.setdefault("no_vni", None) + + if "links" in c_data: + for destRouterLink, data in sorted(c_data["links"].items()): + # Loopback interfaces + if "type" in data and data["type"] == "loopback": + interface_name = destRouterLink + else: + interface_name = data["interface"] + + if "vrf" in data: + vrf_list = data["vrf"] + + if type(vrf_list) is not list: + vrf_list = [vrf_list] + + for _vrf in vrf_list: + cmd = "ip link set {} master {}".format( + interface_name, _vrf + ) + + logger.info( + "[DUT: %s]: Running" " kernel cmd [%s]", + c_router, + cmd, + ) + rnode.run(cmd) + + if vni: + config_data.append("vrf {}".format(vrf["name"])) + cmd = "vni {}".format(vni) + config_data.append(cmd) + + if del_vni: + config_data.append("vrf {}".format(vrf["name"])) + cmd = "no vni {}".format(del_vni) + config_data.append(cmd) + + if config_data: + config_data_dict[c_router] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "vrf", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + return result + + +def create_interface_in_kernel( + tgen, dut, name, ip_addr, vrf=None, netmask=None, create=True +): + """ + Cretae interfaces in kernel for ipv4/ipv6 + Config is done in Linux Kernel: + + Parameters + ---------- + * `tgen` : Topogen object + * `dut` : Device for which interfaces to be added + * `name` : interface name + * `ip_addr` : ip address for interface + * `vrf` : VRF name, to which interface will be associated + * `netmask` : netmask value, default is None + * `create`: Create interface in kernel, if created then no need + to create + """ + + rnode = tgen.gears[dut] + + if create: + cmd = "ip link show {0} >/dev/null || ip link add {0} type dummy".format(name) + rnode.run(cmd) + + if not netmask: + ifaddr = ipaddress.ip_interface(frr_unicode(ip_addr)) + else: + ifaddr = ipaddress.ip_interface( + "{}/{}".format(frr_unicode(ip_addr), frr_unicode(netmask)) + ) + cmd = "ip -{0} a flush {1} scope global && ip a add {2} dev {1} && ip l set {1} up".format( + ifaddr.version, name, ifaddr + ) + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + rnode.run(cmd) + + if vrf: + cmd = "ip link set {} master {}".format(name, vrf) + rnode.run(cmd) + + +def shutdown_bringup_interface_in_kernel(tgen, dut, intf_name, ifaceaction=False): + """ + Cretae interfaces in kernel for ipv4/ipv6 + Config is done in Linux Kernel: + + Parameters + ---------- + * `tgen` : Topogen object + * `dut` : Device for which interfaces to be added + * `intf_name` : interface name + * `ifaceaction` : False to shutdown and True to bringup the + ineterface + """ + + rnode = tgen.gears[dut] + + cmd = "ip link set dev" + if ifaceaction: + action = "up" + cmd = "{} {} {}".format(cmd, intf_name, action) + else: + action = "down" + cmd = "{} {} {}".format(cmd, intf_name, action) + + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + rnode.run(cmd) + + +def validate_ip_address(ip_address): + """ + Validates the type of ip address + Parameters + ---------- + * `ip_address`: IPv4/IPv6 address + Returns + ------- + Type of address as string + """ + + if "/" in ip_address: + ip_address = ip_address.split("/")[0] + + v4 = True + v6 = True + try: + socket.inet_aton(ip_address) + except socket.error as error: + logger.debug("Not a valid IPv4 address") + v4 = False + else: + return "ipv4" + + try: + socket.inet_pton(socket.AF_INET6, ip_address) + except socket.error as error: + logger.debug("Not a valid IPv6 address") + v6 = False + else: + return "ipv6" + + if not v4 and not v6: + raise Exception( + "InvalidIpAddr", "%s is neither valid IPv4 or IPv6" " address" % ip_address + ) + + +def check_address_types(addr_type=None): + """ + Checks environment variable set and compares with the current address type + """ + + addr_types_env = os.environ.get("ADDRESS_TYPES") + if not addr_types_env: + addr_types_env = "dual" + + if addr_types_env == "dual": + addr_types = ["ipv4", "ipv6"] + elif addr_types_env == "ipv4": + addr_types = ["ipv4"] + elif addr_types_env == "ipv6": + addr_types = ["ipv6"] + + if addr_type is None: + return addr_types + + if addr_type not in addr_types: + logger.debug( + "{} not in supported/configured address types {}".format( + addr_type, addr_types + ) + ) + return False + + return True + + +def generate_ips(network, no_of_ips): + """ + Returns list of IPs. + based on start_ip and no_of_ips + + * `network` : from here the ip will start generating, + start_ip will be + * `no_of_ips` : these many IPs will be generated + """ + ipaddress_list = [] + if type(network) is not list: + network = [network] + + for start_ipaddr in network: + if "/" in start_ipaddr: + start_ip = start_ipaddr.split("/")[0] + mask = int(start_ipaddr.split("/")[1]) + else: + logger.debug("start_ipaddr {} must have a / in it".format(start_ipaddr)) + assert 0 + + addr_type = validate_ip_address(start_ip) + if addr_type == "ipv4": + if start_ip == "0.0.0.0" and mask == 0 and no_of_ips == 1: + ipaddress_list.append("{}/{}".format(start_ip, mask)) + return ipaddress_list + start_ip = ipaddress.IPv4Address(frr_unicode(start_ip)) + step = 2 ** (32 - mask) + elif addr_type == "ipv6": + if start_ip == "0::0" and mask == 0 and no_of_ips == 1: + ipaddress_list.append("{}/{}".format(start_ip, mask)) + return ipaddress_list + start_ip = ipaddress.IPv6Address(frr_unicode(start_ip)) + step = 2 ** (128 - mask) + else: + return [] + + next_ip = start_ip + count = 0 + while count < no_of_ips: + ipaddress_list.append("{}/{}".format(next_ip, mask)) + if addr_type == "ipv6": + next_ip = ipaddress.IPv6Address(int(next_ip) + step) + else: + next_ip += step + count += 1 + + return ipaddress_list + + +def find_interface_with_greater_ip(topo, router, loopback=True, interface=True): + """ + Returns highest interface ip for ipv4/ipv6. If loopback is there then + it will return highest IP from loopback IPs otherwise from physical + interface IPs. + * `topo` : json file data + * `router` : router for which highest interface should be calculated + """ + + link_data = topo["routers"][router]["links"] + lo_list = [] + interfaces_list = [] + lo_exists = False + for destRouterLink, data in sorted(link_data.items()): + if loopback: + if "type" in data and data["type"] == "loopback": + lo_exists = True + ip_address = topo["routers"][router]["links"][destRouterLink][ + "ipv4" + ].split("/")[0] + lo_list.append(ip_address) + if interface: + ip_address = topo["routers"][router]["links"][destRouterLink]["ipv4"].split( + "/" + )[0] + interfaces_list.append(ip_address) + + if lo_exists: + return sorted(lo_list)[-1] + + return sorted(interfaces_list)[-1] + + +def write_test_header(tc_name): + """Display message at beginning of test case""" + count = 20 + logger.info("*" * (len(tc_name) + count)) + step("START -> Testcase : %s" % tc_name, reset=True) + logger.info("*" * (len(tc_name) + count)) + + +def write_test_footer(tc_name): + """Display message at end of test case""" + count = 21 + logger.info("=" * (len(tc_name) + count)) + logger.info("Testcase : %s -> PASSED", tc_name) + logger.info("=" * (len(tc_name) + count)) + + +def interface_status(tgen, topo, input_dict): + """ + Delete ip route maps from device + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : for which router, route map has to be deleted + Usage + ----- + input_dict = { + "r3": { + "interface_list": ['eth1-r1-r2', 'eth2-r1-r3'], + "status": "down" + } + } + Returns + ------- + errormsg(str) or True + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + rlist = [] + + for router in input_dict.keys(): + + interface_list = input_dict[router]["interface_list"] + status = input_dict[router].setdefault("status", "up") + for intf in interface_list: + rnode = tgen.gears[router] + interface_set_status(rnode, intf, status) + + rlist.append(router) + + # Load config to routers + load_config_to_routers(tgen, rlist) + + except Exception as e: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def retry(retry_timeout, initial_wait=0, expected=True, diag_pct=0.75): + """ + Fixture: Retries function while it's return value is an errormsg (str), False, or it raises an exception. + + * `retry_timeout`: Retry for at least this many seconds; after waiting initial_wait seconds + * `initial_wait`: Sleeps for this many seconds before first executing function + * `expected`: if False then the return logic is inverted, except for exceptions, + (i.e., a False or errmsg (str) function return ends the retry loop, + and returns that False or str value) + * `diag_pct`: Percentage of `retry_timeout` to keep testing after negative result would have + been returned in order to see if a positive result comes after. This is an + important diagnostic tool, and normally should not be disabled. Calls to wrapped + functions though, can override the `diag_pct` value to make it larger in case more + diagnostic retrying is appropriate. + """ + + def _retry(func): + @wraps(func) + def func_retry(*args, **kwargs): + # We will continue to retry diag_pct of the timeout value to see if test would have passed with a + # longer retry timeout value. + saved_failure = None + + retry_sleep = 2 + + # Allow the wrapped function's args to override the fixtures + _retry_timeout = kwargs.pop("retry_timeout", retry_timeout) + _expected = kwargs.pop("expected", expected) + _initial_wait = kwargs.pop("initial_wait", initial_wait) + _diag_pct = kwargs.pop("diag_pct", diag_pct) + + start_time = datetime.now() + retry_until = datetime.now() + timedelta( + seconds=_retry_timeout + _initial_wait + ) + + if initial_wait > 0: + logger.info("Waiting for [%s]s as initial delay", initial_wait) + sleep(initial_wait) + + invert_logic = not _expected + while True: + seconds_left = (retry_until - datetime.now()).total_seconds() + try: + ret = func(*args, **kwargs) + logger.debug("Function returned %s", ret) + + negative_result = ret is False or is_string(ret) + if negative_result == invert_logic: + # Simple case, successful result in time + if not saved_failure: + return ret + + # Positive result, but happened after timeout failure, very important to + # note for fixing tests. + logger.warning( + "RETRY DIAGNOSTIC: SUCCEED after FAILED with requested timeout of %.1fs; however, succeeded in %.1fs, investigate timeout timing", + _retry_timeout, + (datetime.now() - start_time).total_seconds(), + ) + if isinstance(saved_failure, Exception): + raise saved_failure # pylint: disable=E0702 + return saved_failure + + except Exception as error: + logger.info("Function raised exception: %s", str(error)) + ret = error + + if seconds_left < 0 and saved_failure: + logger.info( + "RETRY DIAGNOSTIC: Retry timeout reached, still failing" + ) + if isinstance(saved_failure, Exception): + raise saved_failure # pylint: disable=E0702 + return saved_failure + + if seconds_left < 0: + logger.info("Retry timeout of %ds reached", _retry_timeout) + + saved_failure = ret + retry_extra_delta = timedelta( + seconds=seconds_left + _retry_timeout * _diag_pct + ) + retry_until = datetime.now() + retry_extra_delta + seconds_left = retry_extra_delta.total_seconds() + + # Generate bundle after setting remaining diagnostic retry time + generate_support_bundle() + + # If user has disabled diagnostic retries return now + if not _diag_pct: + if isinstance(saved_failure, Exception): + raise saved_failure + return saved_failure + + if saved_failure: + logger.info( + "RETRY DIAG: [failure] Sleeping %ds until next retry with %.1f retry time left - too see if timeout was too short", + retry_sleep, + seconds_left, + ) + else: + logger.info( + "Sleeping %ds until next retry with %.1f retry time left", + retry_sleep, + seconds_left, + ) + sleep(retry_sleep) + + func_retry._original = func + return func_retry + + return _retry + + +class Stepper: + """ + Prints step number for the test case step being executed + """ + + count = 1 + + def __call__(self, msg, reset): + if reset: + Stepper.count = 1 + logger.info(msg) + else: + logger.info("STEP %s: '%s'", Stepper.count, msg) + Stepper.count += 1 + + +def step(msg, reset=False): + """ + Call Stepper to print test steps. Need to reset at the beginning of test. + * ` msg` : Step message body. + * `reset` : Reset step count to 1 when set to True. + """ + _step = Stepper() + _step(msg, reset) + + +def do_countdown(secs): + """ + Countdown timer display + """ + for i in range(secs, 0, -1): + sys.stdout.write("{} ".format(str(i))) + sys.stdout.flush() + sleep(1) + return + + +############################################# +# These APIs, will used by testcase +############################################# +def create_interfaces_cfg(tgen, topo, build=False): + """ + Create interface configuration for created topology. Basic Interface + configuration is provided in input json file. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `build` : Only for initial setup phase this is set as True. + + Returns + ------- + True or False + """ + + def _create_interfaces_ospf_cfg(ospf, c_data, data, ospf_keywords): + interface_data = [] + ip_ospf = "ipv6 ospf6" if ospf == "ospf6" else "ip ospf" + for keyword in ospf_keywords: + if keyword in data[ospf]: + intf_ospf_value = c_data["links"][destRouterLink][ospf][keyword] + if "delete" in data and data["delete"]: + interface_data.append( + "no {} {}".format(ip_ospf, keyword.replace("_", "-")) + ) + else: + interface_data.append( + "{} {} {}".format( + ip_ospf, keyword.replace("_", "-"), intf_ospf_value + ) + ) + return interface_data + + result = False + topo = deepcopy(topo) + + try: + interface_data_dict = {} + + for c_router, c_data in topo.items(): + interface_data = [] + for destRouterLink, data in sorted(c_data["links"].items()): + # Loopback interfaces + if "type" in data and data["type"] == "loopback": + interface_name = destRouterLink + else: + interface_name = data["interface"] + + interface_data.append("interface {}".format(str(interface_name))) + + if "ipv4" in data: + intf_addr = c_data["links"][destRouterLink]["ipv4"] + + if "delete" in data and data["delete"]: + interface_data.append("no ip address {}".format(intf_addr)) + else: + interface_data.append("ip address {}".format(intf_addr)) + if "ipv6" in data: + intf_addr = c_data["links"][destRouterLink]["ipv6"] + + if "delete" in data and data["delete"]: + interface_data.append("no ipv6 address {}".format(intf_addr)) + else: + interface_data.append("ipv6 address {}".format(intf_addr)) + + # Wait for vrf interfaces to get link local address once they are up + if ( + not destRouterLink == "lo" + and "vrf" in topo[c_router]["links"][destRouterLink] + ): + vrf = topo[c_router]["links"][destRouterLink]["vrf"] + intf = topo[c_router]["links"][destRouterLink]["interface"] + ll = get_frr_ipv6_linklocal(tgen, c_router, intf=intf, vrf=vrf) + + if "ipv6-link-local" in data: + intf_addr = c_data["links"][destRouterLink]["ipv6-link-local"] + + if "delete" in data and data["delete"]: + interface_data.append("no ipv6 address {}".format(intf_addr)) + else: + interface_data.append("ipv6 address {}\n".format(intf_addr)) + + ospf_keywords = [ + "hello_interval", + "dead_interval", + "network", + "priority", + "cost", + "mtu_ignore", + ] + if "ospf" in data: + interface_data += _create_interfaces_ospf_cfg( + "ospf", c_data, data, ospf_keywords + ["area"] + ) + if "ospf6" in data: + interface_data += _create_interfaces_ospf_cfg( + "ospf6", c_data, data, ospf_keywords + ["area"] + ) + if interface_data: + interface_data_dict[c_router] = interface_data + + result = create_common_configurations( + tgen, interface_data_dict, "interface_config", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + return result + + +def create_static_routes(tgen, input_dict, build=False): + """ + Create static routes for given router as defined in input_dict + + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict should be in the format below: + # static_routes: list of all routes + # network: network address + # no_of_ip: number of next-hop address that will be configured + # admin_distance: admin distance for route/routes. + # next_hop: starting next-hop address + # tag: tag id for static routes + # vrf: VRF name in which static routes needs to be created + # delete: True if config to be removed. Default False. + + Example: + "routers": { + "r1": { + "static_routes": [ + { + "network": "100.0.20.1/32", + "no_of_ip": 9, + "admin_distance": 100, + "next_hop": "10.0.0.1", + "tag": 4001, + "vrf": "RED_A" + "delete": true + } + ] + } + } + + Returns + ------- + errormsg(str) or True + """ + result = False + logger.debug("Entering lib API: create_static_routes()") + input_dict = deepcopy(input_dict) + + try: + static_routes_list_dict = {} + + for router in input_dict.keys(): + if "static_routes" not in input_dict[router]: + errormsg = "static_routes not present in input_dict" + logger.info(errormsg) + continue + + static_routes_list = [] + + static_routes = input_dict[router]["static_routes"] + for static_route in static_routes: + del_action = static_route.setdefault("delete", False) + no_of_ip = static_route.setdefault("no_of_ip", 1) + network = static_route.setdefault("network", []) + if type(network) is not list: + network = [network] + + admin_distance = static_route.setdefault("admin_distance", None) + tag = static_route.setdefault("tag", None) + vrf = static_route.setdefault("vrf", None) + interface = static_route.setdefault("interface", None) + next_hop = static_route.setdefault("next_hop", None) + nexthop_vrf = static_route.setdefault("nexthop_vrf", None) + + ip_list = generate_ips(network, no_of_ip) + for ip in ip_list: + addr_type = validate_ip_address(ip) + + if addr_type == "ipv4": + cmd = "ip route {}".format(ip) + else: + cmd = "ipv6 route {}".format(ip) + + if interface: + cmd = "{} {}".format(cmd, interface) + + if next_hop: + cmd = "{} {}".format(cmd, next_hop) + + if nexthop_vrf: + cmd = "{} nexthop-vrf {}".format(cmd, nexthop_vrf) + + if vrf: + cmd = "{} vrf {}".format(cmd, vrf) + + if tag: + cmd = "{} tag {}".format(cmd, str(tag)) + + if admin_distance: + cmd = "{} {}".format(cmd, admin_distance) + + if del_action: + cmd = "no {}".format(cmd) + + static_routes_list.append(cmd) + + if static_routes_list: + static_routes_list_dict[router] = static_routes_list + + result = create_common_configurations( + tgen, static_routes_list_dict, "static_route", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: create_static_routes()") + return result + + +def create_prefix_lists(tgen, input_dict, build=False): + """ + Create ip prefix lists as per the config provided in input + JSON or input_dict + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + Usage + ----- + # pf_lists_1: name of prefix-list, user defined + # seqid: prefix-list seqid, auto-generated if not given by user + # network: criteria for applying prefix-list + # action: permit/deny + # le: less than or equal number of bits + # ge: greater than or equal number of bits + Example + ------- + input_dict = { + "r1": { + "prefix_lists":{ + "ipv4": { + "pf_list_1": [ + { + "seqid": 10, + "network": "any", + "action": "permit", + "le": "32", + "ge": "30", + "delete": True + } + ] + } + } + } + } + Returns + ------- + errormsg or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + try: + config_data_dict = {} + + for router in input_dict.keys(): + if "prefix_lists" not in input_dict[router]: + errormsg = "prefix_lists not present in input_dict" + logger.debug(errormsg) + continue + + config_data = [] + prefix_lists = input_dict[router]["prefix_lists"] + for addr_type, prefix_data in prefix_lists.items(): + if not check_address_types(addr_type): + continue + + for prefix_name, prefix_list in prefix_data.items(): + for prefix_dict in prefix_list: + if "action" not in prefix_dict or "network" not in prefix_dict: + errormsg = "'action' or network' missing in" " input_dict" + return errormsg + + network_addr = prefix_dict["network"] + action = prefix_dict["action"] + le = prefix_dict.setdefault("le", None) + ge = prefix_dict.setdefault("ge", None) + seqid = prefix_dict.setdefault("seqid", None) + del_action = prefix_dict.setdefault("delete", False) + if seqid is None: + seqid = get_seq_id("prefix_lists", router, prefix_name) + else: + set_seq_id("prefix_lists", router, seqid, prefix_name) + + if addr_type == "ipv4": + protocol = "ip" + else: + protocol = "ipv6" + + cmd = "{} prefix-list {} seq {} {} {}".format( + protocol, prefix_name, seqid, action, network_addr + ) + if le: + cmd = "{} le {}".format(cmd, le) + if ge: + cmd = "{} ge {}".format(cmd, ge) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + if config_data: + config_data_dict[router] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "prefix_list", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def create_route_maps(tgen, input_dict, build=False): + """ + Create route-map on the devices as per the arguments passed + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + Usage + ----- + # route_maps: key, value pair for route-map name and its attribute + # rmap_match_prefix_list_1: user given name for route-map + # action: PERMIT/DENY + # match: key,value pair for match criteria. prefix_list, community-list, + large-community-list or tag. Only one option at a time. + # prefix_list: name of prefix list + # large-community-list: name of large community list + # community-ist: name of community list + # tag: tag id for static routes + # set: key, value pair for modifying route attributes + # localpref: preference value for the network + # med: metric value advertised for AS + # aspath: set AS path value + # weight: weight for the route + # community: standard community value to be attached + # large_community: large community value to be attached + # community_additive: if set to "additive", adds community/large-community + value to the existing values of the network prefix + Example: + -------- + input_dict = { + "r1": { + "route_maps": { + "rmap_match_prefix_list_1": [ + { + "action": "PERMIT", + "match": { + "ipv4": { + "prefix_list": "pf_list_1" + } + "ipv6": { + "prefix_list": "pf_list_1" + } + "large-community-list": { + "id": "community_1", + "exact_match": True + } + "community_list": { + "id": "community_2", + "exact_match": True + } + "tag": "tag_id" + }, + "set": { + "locPrf": 150, + "metric": 30, + "path": { + "num": 20000, + "action": "prepend", + }, + "weight": 500, + "community": { + "num": "1:2 2:3", + "action": additive + } + "large_community": { + "num": "1:2:3 4:5;6", + "action": additive + }, + } + } + ] + } + } + } + Returns + ------- + errormsg(str) or True + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + input_dict = deepcopy(input_dict) + try: + rmap_data_dict = {} + + for router in input_dict.keys(): + if "route_maps" not in input_dict[router]: + logger.debug("route_maps not present in input_dict") + continue + rmap_data = [] + for rmap_name, rmap_value in input_dict[router]["route_maps"].items(): + + for rmap_dict in rmap_value: + del_action = rmap_dict.setdefault("delete", False) + + if del_action: + rmap_data.append("no route-map {}".format(rmap_name)) + continue + + if "action" not in rmap_dict: + errormsg = "action not present in input_dict" + logger.error(errormsg) + return False + + rmap_action = rmap_dict.setdefault("action", "deny") + + seq_id = rmap_dict.setdefault("seq_id", None) + if seq_id is None: + seq_id = get_seq_id("route_maps", router, rmap_name) + else: + set_seq_id("route_maps", router, seq_id, rmap_name) + + rmap_data.append( + "route-map {} {} {}".format(rmap_name, rmap_action, seq_id) + ) + + if "continue" in rmap_dict: + continue_to = rmap_dict["continue"] + if continue_to: + rmap_data.append("on-match goto {}".format(continue_to)) + else: + logger.error( + "In continue, 'route-map entry " + "sequence number' is not provided" + ) + return False + + if "goto" in rmap_dict: + go_to = rmap_dict["goto"] + if go_to: + rmap_data.append("on-match goto {}".format(go_to)) + else: + logger.error( + "In goto, 'Goto Clause number' is not" " provided" + ) + return False + + if "call" in rmap_dict: + call_rmap = rmap_dict["call"] + if call_rmap: + rmap_data.append("call {}".format(call_rmap)) + else: + logger.error( + "In call, 'destination Route-Map' is" " not provided" + ) + return False + + # Verifying if SET criteria is defined + if "set" in rmap_dict: + set_data = rmap_dict["set"] + ipv4_data = set_data.setdefault("ipv4", {}) + ipv6_data = set_data.setdefault("ipv6", {}) + local_preference = set_data.setdefault("locPrf", None) + metric = set_data.setdefault("metric", None) + metric_type = set_data.setdefault("metric-type", None) + as_path = set_data.setdefault("path", {}) + weight = set_data.setdefault("weight", None) + community = set_data.setdefault("community", {}) + large_community = set_data.setdefault("large_community", {}) + large_comm_list = set_data.setdefault("large_comm_list", {}) + set_action = set_data.setdefault("set_action", None) + nexthop = set_data.setdefault("nexthop", None) + origin = set_data.setdefault("origin", None) + ext_comm_list = set_data.setdefault("extcommunity", {}) + metrictype = set_data.setdefault("metric-type", {}) + + # Local Preference + if local_preference: + rmap_data.append( + "set local-preference {}".format(local_preference) + ) + + # Metric-Type + if metrictype: + rmap_data.append("set metric-type {}\n".format(metrictype)) + + # Metric + if metric: + del_comm = set_data.setdefault("delete", None) + if del_comm: + rmap_data.append("no set metric {}".format(metric)) + else: + rmap_data.append("set metric {}".format(metric)) + + # Origin + if origin: + rmap_data.append("set origin {} \n".format(origin)) + + # AS Path Prepend + if as_path: + as_num = as_path.setdefault("as_num", None) + as_action = as_path.setdefault("as_action", None) + if as_action and as_num: + rmap_data.append( + "set as-path {} {}".format(as_action, as_num) + ) + + # Community + if community: + num = community.setdefault("num", None) + comm_action = community.setdefault("action", None) + if num: + cmd = "set community {}".format(num) + if comm_action: + cmd = "{} {}".format(cmd, comm_action) + rmap_data.append(cmd) + else: + logger.error("In community, AS Num not" " provided") + return False + + if large_community: + num = large_community.setdefault("num", None) + comm_action = large_community.setdefault("action", None) + if num: + cmd = "set large-community {}".format(num) + if comm_action: + cmd = "{} {}".format(cmd, comm_action) + + rmap_data.append(cmd) + else: + logger.error( + "In large_community, AS Num not" " provided" + ) + return False + if large_comm_list: + id = large_comm_list.setdefault("id", None) + del_comm = large_comm_list.setdefault("delete", None) + if id: + cmd = "set large-comm-list {}".format(id) + if del_comm: + cmd = "{} delete".format(cmd) + + rmap_data.append(cmd) + else: + logger.error("In large_comm_list 'id' not" " provided") + return False + + if ext_comm_list: + rt = ext_comm_list.setdefault("rt", None) + del_comm = ext_comm_list.setdefault("delete", None) + if rt: + cmd = "set extcommunity rt {}".format(rt) + if del_comm: + cmd = "{} delete".format(cmd) + + rmap_data.append(cmd) + else: + logger.debug("In ext_comm_list 'rt' not" " provided") + return False + + # Weight + if weight: + rmap_data.append("set weight {}".format(weight)) + if ipv6_data: + nexthop = ipv6_data.setdefault("nexthop", None) + if nexthop: + rmap_data.append("set ipv6 next-hop {}".format(nexthop)) + + # Adding MATCH and SET sequence to RMAP if defined + if "match" in rmap_dict: + match_data = rmap_dict["match"] + ipv4_data = match_data.setdefault("ipv4", {}) + ipv6_data = match_data.setdefault("ipv6", {}) + community = match_data.setdefault("community_list", {}) + large_community = match_data.setdefault("large_community", {}) + large_community_list = match_data.setdefault( + "large_community_list", {} + ) + + metric = match_data.setdefault("metric", None) + source_vrf = match_data.setdefault("source-vrf", None) + + if ipv4_data: + # fetch prefix list data from rmap + prefix_name = ipv4_data.setdefault("prefix_lists", None) + if prefix_name: + rmap_data.append( + "match ip address" + " prefix-list {}".format(prefix_name) + ) + + # fetch tag data from rmap + tag = ipv4_data.setdefault("tag", None) + if tag: + rmap_data.append("match tag {}".format(tag)) + + # fetch large community data from rmap + large_community_list = ipv4_data.setdefault( + "large_community_list", {} + ) + large_community = match_data.setdefault( + "large_community", {} + ) + + if ipv6_data: + prefix_name = ipv6_data.setdefault("prefix_lists", None) + if prefix_name: + rmap_data.append( + "match ipv6 address" + " prefix-list {}".format(prefix_name) + ) + + # fetch tag data from rmap + tag = ipv6_data.setdefault("tag", None) + if tag: + rmap_data.append("match tag {}".format(tag)) + + # fetch large community data from rmap + large_community_list = ipv6_data.setdefault( + "large_community_list", {} + ) + large_community = match_data.setdefault( + "large_community", {} + ) + + if community: + if "id" not in community: + logger.error( + "'id' is mandatory for " + "community-list in match" + " criteria" + ) + return False + cmd = "match community {}".format(community["id"]) + exact_match = community.setdefault("exact_match", False) + if exact_match: + cmd = "{} exact-match".format(cmd) + + rmap_data.append(cmd) + if large_community: + if "id" not in large_community: + logger.error( + "'id' is mandatory for " + "large-community-list in match " + "criteria" + ) + return False + cmd = "match large-community {}".format( + large_community["id"] + ) + exact_match = large_community.setdefault( + "exact_match", False + ) + if exact_match: + cmd = "{} exact-match".format(cmd) + rmap_data.append(cmd) + if large_community_list: + if "id" not in large_community_list: + logger.error( + "'id' is mandatory for " + "large-community-list in match " + "criteria" + ) + return False + cmd = "match large-community {}".format( + large_community_list["id"] + ) + exact_match = large_community_list.setdefault( + "exact_match", False + ) + if exact_match: + cmd = "{} exact-match".format(cmd) + rmap_data.append(cmd) + + if source_vrf: + cmd = "match source-vrf {}".format(source_vrf) + rmap_data.append(cmd) + + if metric: + cmd = "match metric {}".format(metric) + rmap_data.append(cmd) + + if rmap_data: + rmap_data_dict[router] = rmap_data + + result = create_common_configurations( + tgen, rmap_data_dict, "route_maps", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def delete_route_maps(tgen, input_dict): + """ + Delete ip route maps from device + * `tgen` : Topogen object + * `input_dict` : for which router, + route map has to be deleted + Usage + ----- + # Delete route-map rmap_1 and rmap_2 from router r1 + input_dict = { + "r1": { + "route_maps": ["rmap_1", "rmap__2"] + } + } + result = delete_route_maps("ipv4", input_dict) + Returns + ------- + errormsg(str) or True + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in input_dict.keys(): + route_maps = input_dict[router]["route_maps"][:] + rmap_data = input_dict[router] + rmap_data["route_maps"] = {} + for route_map_name in route_maps: + rmap_data["route_maps"].update({route_map_name: [{"delete": True}]}) + + return create_route_maps(tgen, input_dict) + + +def create_bgp_community_lists(tgen, input_dict, build=False): + """ + Create bgp community-list or large-community-list on the devices as per + the arguments passed. Takes list of communities in input. + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + Usage + ----- + input_dict_1 = { + "r3": { + "bgp_community_lists": [ + { + "community_type": "standard", + "action": "permit", + "name": "rmap_lcomm_{}".format(addr_type), + "value": "1:1:1 1:2:3 2:1:1 2:2:2", + "large": True + } + ] + } + } + } + result = create_bgp_community_lists(tgen, input_dict_1) + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + input_dict = deepcopy(input_dict) + try: + config_data_dict = {} + + for router in input_dict.keys(): + if "bgp_community_lists" not in input_dict[router]: + errormsg = "bgp_community_lists not present in input_dict" + logger.debug(errormsg) + continue + + config_data = [] + + community_list = input_dict[router]["bgp_community_lists"] + for community_dict in community_list: + del_action = community_dict.setdefault("delete", False) + community_type = community_dict.setdefault("community_type", None) + action = community_dict.setdefault("action", None) + value = community_dict.setdefault("value", "") + large = community_dict.setdefault("large", None) + name = community_dict.setdefault("name", None) + if large: + cmd = "bgp large-community-list" + else: + cmd = "bgp community-list" + + if not large and not (community_type and action and value): + errormsg = ( + "community_type, action and value are " + "required in bgp_community_list" + ) + logger.error(errormsg) + return False + + cmd = "{} {} {} {} {}".format(cmd, community_type, name, action, value) + + if del_action: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + + if config_data: + config_data_dict[router] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "bgp_community_list", build=build + ) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def shutdown_bringup_interface(tgen, dut, intf_name, ifaceaction=False): + """ + Shutdown or bringup router's interface " + * `tgen` : Topogen object + * `dut` : Device under test + * `intf_name` : Interface name to be shut/no shut + * `ifaceaction` : Action, to shut/no shut interface, + by default is False + Usage + ----- + dut = "r3" + intf = "r3-r1-eth0" + # Shut down interface + shutdown_bringup_interface(tgen, dut, intf, False) + # Bring up interface + shutdown_bringup_interface(tgen, dut, intf, True) + Returns + ------- + errormsg(str) or True + """ + + router_list = tgen.routers() + if ifaceaction: + logger.info("Bringing up interface {} : {}".format(dut, intf_name)) + else: + logger.info("Shutting down interface {} : {}".format(dut, intf_name)) + + interface_set_status(router_list[dut], intf_name, ifaceaction) + + +def addKernelRoute( + tgen, router, intf, group_addr_range, next_hop=None, src=None, del_action=None +): + """ + Add route to kernel + + Parameters: + ----------- + * `tgen` : Topogen object + * `router`: router for which kernel routes needs to be added + * `intf`: interface name, for which kernel routes needs to be added + * `bindToAddress`: bind to <host>, an interface or multicast + address + + returns: + -------- + errormsg or True + """ + + logger.debug("Entering lib API: addKernelRoute()") + + rnode = tgen.gears[router] + + if type(group_addr_range) is not list: + group_addr_range = [group_addr_range] + + for grp_addr in group_addr_range: + + addr_type = validate_ip_address(grp_addr) + if addr_type == "ipv4": + if next_hop is not None: + cmd = "ip route add {} via {}".format(grp_addr, next_hop) + else: + cmd = "ip route add {} dev {}".format(grp_addr, intf) + if del_action: + cmd = "ip route del {}".format(grp_addr) + verify_cmd = "ip route" + elif addr_type == "ipv6": + if intf and src: + cmd = "ip -6 route add {} dev {} src {}".format(grp_addr, intf, src) + else: + cmd = "ip -6 route add {} via {}".format(grp_addr, next_hop) + verify_cmd = "ip -6 route" + if del_action: + cmd = "ip -6 route del {}".format(grp_addr) + + logger.info("[DUT: {}]: Running command: [{}]".format(router, cmd)) + output = rnode.run(cmd) + + def check_in_kernel(rnode, verify_cmd, grp_addr, router): + # Verifying if ip route added to kernel + errormsg = None + result = rnode.run(verify_cmd) + logger.debug("{}\n{}".format(verify_cmd, result)) + if "/" in grp_addr: + ip, mask = grp_addr.split("/") + if mask == "32" or mask == "128": + grp_addr = ip + else: + mask = "32" if addr_type == "ipv4" else "128" + + if not re_search(r"{}".format(grp_addr), result) and mask != "0": + errormsg = ( + "[DUT: {}]: Kernal route is not added for group" + " address {} Config output: {}".format( + router, grp_addr, output + ) + ) + + return errormsg + + test_func = functools.partial( + check_in_kernel, rnode, verify_cmd, grp_addr, router + ) + (result, out) = topotest.run_and_expect(test_func, None, count=20, wait=1) + assert result, out + + logger.debug("Exiting lib API: addKernelRoute()") + return True + + +def configure_vxlan(tgen, input_dict): + """ + Add and configure vxlan + + * `tgen`: tgen object + * `input_dict` : data for vxlan config + + Usage: + ------ + input_dict= { + "dcg2":{ + "vxlan":[{ + "vxlan_name": "vxlan75100", + "vxlan_id": "75100", + "dstport": 4789, + "local_addr": "120.0.0.1", + "learning": "no", + "delete": True + }] + } + } + + configure_vxlan(tgen, input_dict) + + Returns: + ------- + True or errormsg + + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for dut in input_dict.keys(): + rnode = router_list[dut] + + if "vxlan" in input_dict[dut]: + for vxlan_dict in input_dict[dut]["vxlan"]: + cmd = "ip link " + + del_vxlan = vxlan_dict.setdefault("delete", None) + vxlan_names = vxlan_dict.setdefault("vxlan_name", []) + vxlan_ids = vxlan_dict.setdefault("vxlan_id", []) + dstport = vxlan_dict.setdefault("dstport", None) + local_addr = vxlan_dict.setdefault("local_addr", None) + learning = vxlan_dict.setdefault("learning", None) + + config_data = [] + if vxlan_names and vxlan_ids: + for vxlan_name, vxlan_id in zip(vxlan_names, vxlan_ids): + cmd = "ip link" + + if del_vxlan: + cmd = "{} del {} type vxlan id {}".format( + cmd, vxlan_name, vxlan_id + ) + else: + cmd = "{} add {} type vxlan id {}".format( + cmd, vxlan_name, vxlan_id + ) + + if dstport: + cmd = "{} dstport {}".format(cmd, dstport) + + if local_addr: + ip_cmd = "ip addr add {} dev {}".format( + local_addr, vxlan_name + ) + if del_vxlan: + ip_cmd = "ip addr del {} dev {}".format( + local_addr, vxlan_name + ) + + config_data.append(ip_cmd) + + cmd = "{} local {}".format(cmd, local_addr) + + if learning == "no": + cmd = "{} nolearning".format(cmd) + + elif learning == "yes": + cmd = "{} learning".format(cmd) + + config_data.append(cmd) + + try: + for _cmd in config_data: + logger.info("[DUT: %s]: Running command: %s", dut, _cmd) + rnode.run(_cmd) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +def configure_brctl(tgen, topo, input_dict): + """ + Add and configure brctl + + * `tgen`: tgen object + * `input_dict` : data for brctl config + + Usage: + ------ + input_dict= { + dut:{ + "brctl": [{ + "brctl_name": "br100", + "addvxlan": "vxlan75100", + "vrf": "RED", + "stp": "off" + }] + } + } + + configure_brctl(tgen, topo, input_dict) + + Returns: + ------- + True or errormsg + + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for dut in input_dict.keys(): + rnode = router_list[dut] + + if "brctl" in input_dict[dut]: + for brctl_dict in input_dict[dut]["brctl"]: + + brctl_names = brctl_dict.setdefault("brctl_name", []) + addvxlans = brctl_dict.setdefault("addvxlan", []) + stp_values = brctl_dict.setdefault("stp", []) + vrfs = brctl_dict.setdefault("vrf", []) + + ip_cmd = "ip link set" + for brctl_name, vxlan, vrf, stp in zip( + brctl_names, addvxlans, vrfs, stp_values + ): + + ip_cmd_list = [] + cmd = "ip link add name {} type bridge stp_state {}".format( + brctl_name, stp + ) + + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + rnode.run(cmd) + + ip_cmd_list.append("{} up dev {}".format(ip_cmd, brctl_name)) + + if vxlan: + cmd = "{} dev {} master {}".format(ip_cmd, vxlan, brctl_name) + + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + rnode.run(cmd) + + ip_cmd_list.append("{} up dev {}".format(ip_cmd, vxlan)) + + if vrf: + ip_cmd_list.append( + "{} dev {} master {}".format(ip_cmd, brctl_name, vrf) + ) + + for intf_name, data in topo["routers"][dut]["links"].items(): + if "vrf" not in data: + continue + + if data["vrf"] == vrf: + ip_cmd_list.append( + "{} up dev {}".format(ip_cmd, data["interface"]) + ) + + try: + for _ip_cmd in ip_cmd_list: + logger.info("[DUT: %s]: Running command: %s", dut, _ip_cmd) + rnode.run(_ip_cmd) + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def configure_interface_mac(tgen, input_dict): + """ + Add and configure brctl + + * `tgen`: tgen object + * `input_dict` : data for mac config + + input_mac= { + "edge1":{ + "br75100": "00:80:48:BA:d1:00, + "br75200": "00:80:48:BA:d1:00 + } + } + + configure_interface_mac(tgen, input_mac) + + Returns: + ------- + True or errormsg + + """ + + router_list = tgen.routers() + for dut in input_dict.keys(): + rnode = router_list[dut] + + for intf, mac in input_dict[dut].items(): + cmd = "ip link set {} address {}".format(intf, mac) + logger.info("[DUT: %s]: Running command: %s", dut, cmd) + + try: + result = rnode.run(cmd) + if len(result) != 0: + return result + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + return True + + +def socat_send_igmp_join_traffic( + tgen, + server, + protocol_option, + igmp_groups, + send_from_intf, + send_from_intf_ip=None, + port=12345, + reuseaddr=True, + join=False, + traffic=False, +): + """ + API to send IGMP join using SOCAT tool + + Parameters: + ----------- + * `tgen` : Topogen object + * `server`: iperf server, from where IGMP join would be sent + * `protocol_option`: Protocol options, ex: UDP6-RECV + * `igmp_groups`: IGMP group for which join has to be sent + * `send_from_intf`: Interface from which join would be sent + * `send_from_intf_ip`: Interface IP, default is None + * `port`: Port to be used, default is 12345 + * `reuseaddr`: True|False, bydefault True + * `join`: If join needs to be sent + * `traffic`: If traffic needs to be sent + + returns: + -------- + errormsg or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + rnode = tgen.routers()[server] + socat_cmd = "socat -u " + + # UDP4/TCP4/UDP6/UDP6-RECV + if protocol_option: + socat_cmd += "{}".format(protocol_option) + + if port: + socat_cmd += ":{},".format(port) + + if reuseaddr: + socat_cmd += "{},".format("reuseaddr") + + # Group address range to cover + if igmp_groups: + if not isinstance(igmp_groups, list): + igmp_groups = [igmp_groups] + + for igmp_group in igmp_groups: + if join: + join_traffic_option = "ipv6-join-group" + elif traffic: + join_traffic_option = "ipv6-join-group-source" + + if send_from_intf and not send_from_intf_ip: + socat_cmd += "{}='[{}]:{}'".format( + join_traffic_option, igmp_group, send_from_intf + ) + else: + socat_cmd += "{}='[{}]:{}:[{}]'".format( + join_traffic_option, igmp_group, send_from_intf, send_from_intf_ip + ) + + socat_cmd += " STDOUT" + + socat_cmd += " &>{}/socat.logs &".format(tgen.logdir) + + # Run socat command to send IGMP join + logger.info("[DUT: {}]: Running command: [{}]".format(server, socat_cmd)) + output = rnode.run("set +m; {} sleep 0.5".format(socat_cmd)) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +############################################# +# Verification APIs +############################################# +@retry(retry_timeout=40) +def verify_rib( + tgen, + addr_type, + dut, + input_dict, + next_hop=None, + protocol=None, + tag=None, + metric=None, + fib=None, + count_only=False, + admin_distance=None, +): + """ + Data will be read from input_dict or input JSON file, API will generate + same prefixes, which were redistributed by either create_static_routes() or + advertise_networks_using_network_command() and do will verify next_hop and + each prefix/routes is present in "show ip/ipv6 route {bgp/stataic} json" + command o/p. + + Parameters + ---------- + * `tgen` : topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test, for which user wants to test the data + * `input_dict` : input dict, has details of static routes + * `next_hop`[optional]: next_hop which needs to be verified, + default: static + * `protocol`[optional]: protocol, default = None + * `count_only`[optional]: count of nexthops only, not specific addresses, + default = False + + Usage + ----- + # RIB can be verified for static routes OR network advertised using + network command. Following are input_dicts to create static routes + and advertise networks using network command. Any one of the input_dict + can be passed to verify_rib() to verify routes in DUT"s RIB. + + # Creating static routes for r1 + input_dict = { + "r1": { + "static_routes": [{"network": "10.0.20.1/32", "no_of_ip": 9, \ + "admin_distance": 100, "next_hop": "10.0.0.2", "tag": 4001}] + }} + # Advertising networks using network command in router r1 + input_dict = { + "r1": { + "advertise_networks": [{"start_ip": "20.0.0.0/32", + "no_of_network": 10}, + {"start_ip": "30.0.0.0/32"}] + }} + # Verifying ipv4 routes in router r1 learned via BGP + dut = "r2" + protocol = "bgp" + result = verify_rib(tgen, "ipv4", dut, input_dict, protocol = protocol) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + additional_nexthops_in_required_nhs = [] + found_hops = [] + for routerInput in input_dict.keys(): + for router, rnode in router_list.items(): + if router != dut: + continue + + logger.info("Checking router %s RIB:", router) + + # Verifying RIB routes + if addr_type == "ipv4": + command = "show ip route" + else: + command = "show ipv6 route" + + found_routes = [] + missing_routes = [] + + if "static_routes" in input_dict[routerInput]: + static_routes = input_dict[routerInput]["static_routes"] + + for static_route in static_routes: + if "vrf" in static_route and static_route["vrf"] is not None: + + logger.info( + "[DUT: {}]: Verifying routes for VRF:" + " {}".format(router, static_route["vrf"]) + ) + + cmd = "{} vrf {}".format(command, static_route["vrf"]) + + else: + cmd = "{}".format(command) + + if protocol: + cmd = "{} {}".format(cmd, protocol) + + cmd = "{} json".format(cmd) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) is False: + errormsg = "No route found in rib of router {}..".format(router) + return errormsg + + network = static_route["network"] + if "no_of_ip" in static_route: + no_of_ip = static_route["no_of_ip"] + else: + no_of_ip = 1 + + if "tag" in static_route: + _tag = static_route["tag"] + else: + _tag = None + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_ip) + st_found = False + nh_found = False + + for st_rt in ip_list: + st_rt = str( + ipaddress.ip_network(frr_unicode(st_rt), strict=False) + ) + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json: + st_found = True + found_routes.append(st_rt) + + if "queued" in rib_routes_json[st_rt][0]: + errormsg = "Route {} is queued\n".format(st_rt) + return errormsg + + if fib and next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + for mnh in range(0, len(rib_routes_json[st_rt])): + if not "selected" in rib_routes_json[st_rt][mnh]: + continue + + if ( + "fib" + in rib_routes_json[st_rt][mnh]["nexthops"][0] + ): + found_hops.append( + [ + rib_r["ip"] + for rib_r in rib_routes_json[st_rt][ + mnh + ]["nexthops"] + ] + ) + + if found_hops[0]: + missing_list_of_nexthops = set( + found_hops[0] + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops[0]) + + if additional_nexthops_in_required_nhs: + logger.info( + "Nexthop " + "%s is not active for route %s in " + "RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is not active" + " for route {} in RIB of router" + " {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + + elif next_hop and fib is None: + if type(next_hop) is not list: + next_hop = [next_hop] + found_hops = [ + rib_r["ip"] + for rib_r in rib_routes_json[st_rt][0]["nexthops"] + if "ip" in rib_r + ] + + # If somehow key "ip" is not found in nexthops JSON + # then found_hops would be 0, this particular + # situation will be handled here + if not len(found_hops): + errormsg = ( + "Nexthop {} is Missing for " + "route {} in RIB of router {}\n".format( + next_hop, + st_rt, + dut, + ) + ) + return errormsg + + # Check only the count of nexthops + if count_only: + if len(next_hop) == len(found_hops): + nh_found = True + else: + errormsg = ( + "Nexthops are missing for " + "route {} in RIB of router {}: " + "expected {}, found {}\n".format( + st_rt, + dut, + len(next_hop), + len(found_hops), + ) + ) + return errormsg + + # Check the actual nexthops + elif found_hops: + missing_list_of_nexthops = set( + found_hops + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops) + + if additional_nexthops_in_required_nhs: + logger.info( + "Missing nexthop %s for route" + " %s in RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is Missing for " + "route {} in RIB of router {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + + if tag: + if "tag" not in rib_routes_json[st_rt][0]: + errormsg = ( + "[DUT: {}]: tag is not" + " present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if _tag != rib_routes_json[st_rt][0]["tag"]: + errormsg = ( + "[DUT: {}]: tag value {}" + " is not matched for" + " route {} in RIB \n".format( + dut, + _tag, + st_rt, + ) + ) + return errormsg + + if admin_distance is not None: + if "distance" not in rib_routes_json[st_rt][0]: + errormsg = ( + "[DUT: {}]: admin distance is" + " not present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if ( + admin_distance + != rib_routes_json[st_rt][0]["distance"] + ): + errormsg = ( + "[DUT: {}]: admin distance value " + "{} is not matched for " + "route {} in RIB \n".format( + dut, + admin_distance, + st_rt, + ) + ) + return errormsg + + if metric is not None: + if "metric" not in rib_routes_json[st_rt][0]: + errormsg = ( + "[DUT: {}]: metric is" + " not present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if metric != rib_routes_json[st_rt][0]["metric"]: + errormsg = ( + "[DUT: {}]: metric value " + "{} is not matched for " + "route {} in RIB \n".format( + dut, + metric, + st_rt, + ) + ) + return errormsg + + else: + missing_routes.append(st_rt) + + if nh_found: + logger.info( + "[DUT: {}]: Found next_hop {} for" + " RIB routes: {}".format(router, next_hop, found_routes) + ) + + if len(missing_routes) > 0: + errormsg = "[DUT: {}]: Missing route in RIB, " "routes: {}".format( + dut, missing_routes + ) + return errormsg + + if found_routes: + logger.info( + "[DUT: %s]: Verified routes in RIB, found" " routes are: %s\n", + dut, + found_routes, + ) + + continue + + if "bgp" in input_dict[routerInput]: + if ( + "advertise_networks" + not in input_dict[routerInput]["bgp"]["address_family"][addr_type][ + "unicast" + ] + ): + continue + + found_routes = [] + missing_routes = [] + advertise_network = input_dict[routerInput]["bgp"]["address_family"][ + addr_type + ]["unicast"]["advertise_networks"] + + # Continue if there are no network advertise + if len(advertise_network) == 0: + continue + + for advertise_network_dict in advertise_network: + if "vrf" in advertise_network_dict: + cmd = "{} vrf {} json".format( + command, advertise_network_dict["vrf"] + ) + else: + cmd = "{} json".format(command) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) is False: + errormsg = "No route found in rib of router {}..".format(router) + return errormsg + + start_ip = advertise_network_dict["network"] + if "no_of_network" in advertise_network_dict: + no_of_network = advertise_network_dict["no_of_network"] + else: + no_of_network = 1 + + # Generating IPs for verification + ip_list = generate_ips(start_ip, no_of_network) + st_found = False + nh_found = False + + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt), strict=False)) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json: + st_found = True + found_routes.append(st_rt) + + if "queued" in rib_routes_json[st_rt][0]: + errormsg = "Route {} is queued\n".format(st_rt) + return errormsg + + if next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + count = 0 + for nh in next_hop: + for nh_dict in rib_routes_json[st_rt][0]["nexthops"]: + if nh_dict["ip"] != nh: + continue + else: + count += 1 + + if count == len(next_hop): + nh_found = True + else: + errormsg = ( + "Nexthop {} is Missing" + " for route {} in " + "RIB of router {}\n".format(next_hop, st_rt, dut) + ) + return errormsg + else: + missing_routes.append(st_rt) + + if nh_found: + logger.info( + "Found next_hop {} for all routes in RIB" + " of router {}\n".format(next_hop, dut) + ) + + if len(missing_routes) > 0: + errormsg = ( + "Missing {} route in RIB of router {}, " + "routes: {} \n".format(addr_type, dut, missing_routes) + ) + return errormsg + + if found_routes: + logger.info( + "Verified {} routes in router {} RIB, found" + " routes are: {}\n".format(addr_type, dut, found_routes) + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=12) +def verify_fib_routes(tgen, addr_type, dut, input_dict, next_hop=None, protocol=None): + """ + Data will be read from input_dict or input JSON file, API will generate + same prefixes, which were redistributed by either create_static_routes() or + advertise_networks_using_network_command() and will verify next_hop and + each prefix/routes is present in "show ip/ipv6 fib json" + command o/p. + + Parameters + ---------- + * `tgen` : topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test, for which user wants to test the data + * `input_dict` : input dict, has details of static routes + * `next_hop`[optional]: next_hop which needs to be verified, + default: static + + Usage + ----- + input_routes_r1 = { + "r1": { + "static_routes": [{ + "network": ["1.1.1.1/32], + "next_hop": "Null0", + "vrf": "RED" + }] + } + } + result = result = verify_fib_routes(tgen, "ipv4, "r1", input_routes_r1) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + if dut not in router_list: + return + + for routerInput in input_dict.keys(): + # XXX replace with router = dut; rnode = router_list[dut] + for router, rnode in router_list.items(): + if router != dut: + continue + + logger.info("Checking router %s FIB routes:", router) + + # Verifying RIB routes + if addr_type == "ipv4": + command = "show ip fib" + else: + command = "show ipv6 fib" + + found_routes = [] + missing_routes = [] + + if protocol: + command = "{} {}".format(command, protocol) + + if "static_routes" in input_dict[routerInput]: + static_routes = input_dict[routerInput]["static_routes"] + + for static_route in static_routes: + if "vrf" in static_route and static_route["vrf"] is not None: + + logger.info( + "[DUT: {}]: Verifying routes for VRF:" + " {}".format(router, static_route["vrf"]) + ) + + cmd = "{} vrf {}".format(command, static_route["vrf"]) + + else: + cmd = "{}".format(command) + + cmd = "{} json".format(cmd) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) is False: + errormsg = "[DUT: {}]: No route found in fib".format(router) + return errormsg + + network = static_route["network"] + if "no_of_ip" in static_route: + no_of_ip = static_route["no_of_ip"] + else: + no_of_ip = 1 + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_ip) + st_found = False + nh_found = False + + for st_rt in ip_list: + st_rt = str( + ipaddress.ip_network(frr_unicode(st_rt), strict=False) + ) + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json: + st_found = True + found_routes.append(st_rt) + + if next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + count = 0 + for nh in next_hop: + for nh_dict in rib_routes_json[st_rt][0][ + "nexthops" + ]: + if nh_dict["ip"] != nh: + continue + else: + count += 1 + + if count == len(next_hop): + nh_found = True + else: + missing_routes.append(st_rt) + errormsg = ( + "Nexthop {} is Missing" + " for route {} in " + "RIB of router {}\n".format( + next_hop, st_rt, dut + ) + ) + return errormsg + + else: + missing_routes.append(st_rt) + + if len(missing_routes) > 0: + errormsg = "[DUT: {}]: Missing route in FIB:" " {}".format( + dut, missing_routes + ) + return errormsg + + if nh_found: + logger.info( + "Found next_hop {} for all routes in RIB" + " of router {}\n".format(next_hop, dut) + ) + + if found_routes: + logger.info( + "[DUT: %s]: Verified routes in FIB, found" " routes are: %s\n", + dut, + found_routes, + ) + + continue + + if "bgp" in input_dict[routerInput]: + if ( + "advertise_networks" + not in input_dict[routerInput]["bgp"]["address_family"][addr_type][ + "unicast" + ] + ): + continue + + found_routes = [] + missing_routes = [] + advertise_network = input_dict[routerInput]["bgp"]["address_family"][ + addr_type + ]["unicast"]["advertise_networks"] + + # Continue if there are no network advertise + if len(advertise_network) == 0: + continue + + for advertise_network_dict in advertise_network: + if "vrf" in advertise_network_dict: + cmd = "{} vrf {} json".format(command, static_route["vrf"]) + else: + cmd = "{} json".format(command) + + rib_routes_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary rib_routes_json is not empty + if bool(rib_routes_json) is False: + errormsg = "No route found in rib of router {}..".format(router) + return errormsg + + start_ip = advertise_network_dict["network"] + if "no_of_network" in advertise_network_dict: + no_of_network = advertise_network_dict["no_of_network"] + else: + no_of_network = 1 + + # Generating IPs for verification + ip_list = generate_ips(start_ip, no_of_network) + st_found = False + nh_found = False + + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt), strict=False)) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != addr_type: + continue + + if st_rt in rib_routes_json: + st_found = True + found_routes.append(st_rt) + + if next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + count = 0 + for nh in next_hop: + for nh_dict in rib_routes_json[st_rt][0]["nexthops"]: + if nh_dict["ip"] != nh: + continue + else: + count += 1 + + if count == len(next_hop): + nh_found = True + else: + missing_routes.append(st_rt) + errormsg = ( + "Nexthop {} is Missing" + " for route {} in " + "RIB of router {}\n".format(next_hop, st_rt, dut) + ) + return errormsg + else: + missing_routes.append(st_rt) + + if len(missing_routes) > 0: + errormsg = "[DUT: {}]: Missing route in FIB: " "{} \n".format( + dut, missing_routes + ) + return errormsg + + if nh_found: + logger.info( + "Found next_hop {} for all routes in RIB" + " of router {}\n".format(next_hop, dut) + ) + + if found_routes: + logger.info( + "[DUT: {}]: Verified routes FIB" + ", found routes are: {}\n".format(dut, found_routes) + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def verify_admin_distance_for_static_routes(tgen, input_dict): + """ + API to verify admin distance for static routes as defined in input_dict/ + input JSON by running show ip/ipv6 route json command. + Parameter + --------- + * `tgen` : topogen object + * `input_dict`: having details like - for which router and static routes + admin dsitance needs to be verified + Usage + ----- + # To verify admin distance is 10 for prefix 10.0.20.1/32 having next_hop + 10.0.0.2 in router r1 + input_dict = { + "r1": { + "static_routes": [{ + "network": "10.0.20.1/32", + "admin_distance": 10, + "next_hop": "10.0.0.2" + }] + } + } + result = verify_admin_distance_for_static_routes(tgen, input_dict) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + rnode = router_list[router] + + for static_route in input_dict[router]["static_routes"]: + addr_type = validate_ip_address(static_route["network"]) + # Command to execute + if addr_type == "ipv4": + command = "show ip route json" + else: + command = "show ipv6 route json" + show_ip_route_json = run_frr_cmd(rnode, command, isjson=True) + + logger.info( + "Verifying admin distance for static route %s" " under dut %s:", + static_route, + router, + ) + network = static_route["network"] + next_hop = static_route["next_hop"] + admin_distance = static_route["admin_distance"] + route_data = show_ip_route_json[network][0] + if network in show_ip_route_json: + if route_data["nexthops"][0]["ip"] == next_hop: + if route_data["distance"] != admin_distance: + errormsg = ( + "Verification failed: admin distance" + " for static route {} under dut {}," + " found:{} but expected:{}".format( + static_route, + router, + route_data["distance"], + admin_distance, + ) + ) + return errormsg + else: + logger.info( + "Verification successful: admin" + " distance for static route %s under" + " dut %s, found:%s", + static_route, + router, + route_data["distance"], + ) + + else: + errormsg = ( + "Static route {} not found in " + "show_ip_route_json for dut {}".format(network, router) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def verify_prefix_lists(tgen, input_dict): + """ + Running "show ip prefix-list" command and verifying given prefix-list + is present in router. + Parameters + ---------- + * `tgen` : topogen object + * `input_dict`: data to verify prefix lists + Usage + ----- + # To verify pf_list_1 is present in router r1 + input_dict = { + "r1": { + "prefix_lists": ["pf_list_1"] + }} + result = verify_prefix_lists("ipv4", input_dict, tgen) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + + rnode = router_list[router] + + # Show ip prefix list + show_prefix_list = run_frr_cmd(rnode, "show ip prefix-list") + + # Verify Prefix list is deleted + prefix_lists_addr = input_dict[router]["prefix_lists"] + for addr_type in prefix_lists_addr: + if not check_address_types(addr_type): + continue + # show ip prefix list + if addr_type == "ipv4": + cmd = "show ip prefix-list" + else: + cmd = "show {} prefix-list".format(addr_type) + show_prefix_list = run_frr_cmd(rnode, cmd) + for prefix_list in prefix_lists_addr[addr_type].keys(): + if prefix_list in show_prefix_list: + errormsg = ( + "Prefix list {} is/are present in the router" + " {}".format(prefix_list, router) + ) + return errormsg + + logger.info( + "Prefix list %s is/are not present in the router" " from router %s", + prefix_list, + router, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=12) +def verify_route_maps(tgen, input_dict): + """ + Running "show route-map" command and verifying given route-map + is present in router. + Parameters + ---------- + * `tgen` : topogen object + * `input_dict`: data to verify prefix lists + Usage + ----- + # To verify rmap_1 and rmap_2 are present in router r1 + input_dict = { + "r1": { + "route_maps": ["rmap_1", "rmap_2"] + } + } + result = verify_route_maps(tgen, input_dict) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + + rnode = router_list[router] + # Show ip route-map + show_route_maps = rnode.vtysh_cmd("show route-map") + + # Verify route-map is deleted + route_maps = input_dict[router]["route_maps"] + for route_map in route_maps: + if route_map in show_route_maps: + errormsg = "Route map {} is not deleted from router" " {}".format( + route_map, router + ) + return errormsg + + logger.info( + "Route map %s is/are deleted successfully from" " router %s", + route_maps, + router, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=16) +def verify_bgp_community(tgen, addr_type, router, network, input_dict=None): + """ + API to veiryf BGP large community is attached in route for any given + DUT by running "show bgp ipv4/6 {route address} json" command. + Parameters + ---------- + * `tgen`: topogen object + * `addr_type` : ip type, ipv4/ipv6 + * `dut`: Device Under Test + * `network`: network for which set criteria needs to be verified + * `input_dict`: having details like - for which router, community and + values needs to be verified + Usage + ----- + networks = ["200.50.2.0/32"] + input_dict = { + "largeCommunity": "2:1:1 2:2:2 2:3:3 2:4:4 2:5:5" + } + result = verify_bgp_community(tgen, "ipv4", dut, network, input_dict=None) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + router_list = tgen.routers() + if router not in router_list: + return False + + rnode = router_list[router] + + logger.debug( + "Verifying BGP community attributes on dut %s: for %s " "network %s", + router, + addr_type, + network, + ) + + for net in network: + cmd = "show bgp {} {} json".format(addr_type, net) + show_bgp_json = rnode.vtysh_cmd(cmd, isjson=True) + logger.info(show_bgp_json) + if "paths" not in show_bgp_json: + return "Prefix {} not found in BGP table of router: {}".format(net, router) + + as_paths = show_bgp_json["paths"] + found = False + for i in range(len(as_paths)): + if ( + "largeCommunity" in show_bgp_json["paths"][i] + or "community" in show_bgp_json["paths"][i] + ): + found = True + logger.info( + "Large Community attribute is found for route:" " %s in router: %s", + net, + router, + ) + if input_dict is not None: + for criteria, comm_val in input_dict.items(): + show_val = show_bgp_json["paths"][i][criteria]["string"] + if comm_val == show_val: + logger.info( + "Verifying BGP %s for prefix: %s" + " in router: %s, found expected" + " value: %s", + criteria, + net, + router, + comm_val, + ) + else: + errormsg = ( + "Failed: Verifying BGP attribute" + " {} for route: {} in router: {}" + ", expected value: {} but found" + ": {}".format(criteria, net, router, comm_val, show_val) + ) + return errormsg + + if not found: + errormsg = ( + "Large Community attribute is not found for route: " + "{} in router: {} ".format(net, router) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def get_ipv6_linklocal_address(topo, node, intf): + """ + API to get the link local ipv6 address of a particular interface + + Parameters + ---------- + * `node`: node on which link local ip to be fetched. + * `intf` : interface for which link local ip needs to be returned. + * `topo` : base topo + + Usage + ----- + result = get_ipv6_linklocal_address(topo, 'r1', 'r2') + + Returns link local ip of interface between r1 and r2. + + Returns + ------- + 1) link local ipv6 address from the interface + 2) errormsg - when link local ip not found + """ + tgen = get_topogen() + ext_nh = tgen.net[node].get_ipv6_linklocal() + req_nh = topo[node]["links"][intf]["interface"] + llip = None + for llips in ext_nh: + if llips[0] == req_nh: + llip = llips[1] + logger.info("Link local ip found = %s", llip) + return llip + + errormsg = "Failed: Link local ip not found on router {}, " "interface {}".format( + node, intf + ) + + return errormsg + + +def verify_create_community_list(tgen, input_dict): + """ + API is to verify if large community list is created for any given DUT in + input_dict by running "sh bgp large-community-list {"comm_name"} detail" + command. + Parameters + ---------- + * `tgen`: topogen object + * `input_dict`: having details like - for which router, large community + needs to be verified + Usage + ----- + input_dict = { + "r1": { + "large-community-list": { + "standard": { + "Test1": [{"action": "PERMIT", "attribute":\ + ""}] + }}}} + result = verify_create_community_list(tgen, input_dict) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + + rnode = router_list[router] + + logger.info("Verifying large-community is created for dut %s:", router) + + for comm_data in input_dict[router]["bgp_community_lists"]: + comm_name = comm_data["name"] + comm_type = comm_data["community_type"] + show_bgp_community = run_frr_cmd( + rnode, "show bgp large-community-list {} detail".format(comm_name) + ) + + # Verify community list and type + if comm_name in show_bgp_community and comm_type in show_bgp_community: + logger.info( + "BGP %s large-community-list %s is" " created", comm_type, comm_name + ) + else: + errormsg = "BGP {} large-community-list {} is not" " created".format( + comm_type, comm_name + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def verify_cli_json(tgen, input_dict): + """ + API to verify if JSON is available for clis + command. + Parameters + ---------- + * `tgen`: topogen object + * `input_dict`: CLIs for which JSON needs to be verified + Usage + ----- + input_dict = { + "edge1":{ + "cli": ["show evpn vni detail", show evpn rmac vni all] + } + } + + result = verify_cli_json(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for dut in input_dict.keys(): + rnode = tgen.gears[dut] + + for cli in input_dict[dut]["cli"]: + logger.info( + "[DUT: %s]: Verifying JSON is available for " "CLI %s :", dut, cli + ) + + test_cli = "{} json".format(cli) + ret_json = rnode.vtysh_cmd(test_cli, isjson=True) + if not bool(ret_json): + errormsg = "CLI: %s, JSON format is not available" % (cli) + return errormsg + elif "unknown" in ret_json or "Unknown" in ret_json: + errormsg = "CLI: %s, JSON format is not available" % (cli) + return errormsg + else: + logger.info( + "CLI : %s JSON format is available: " "\n %s", cli, ret_json + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +@retry(retry_timeout=12) +def verify_evpn_vni(tgen, input_dict): + """ + API to verify evpn vni details using "show evpn vni detail json" + command. + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict`: having details like - for which router, evpn details + needs to be verified + Usage + ----- + input_dict = { + "edge1":{ + "vni": [ + { + "75100":{ + "vrf": "RED", + "vxlanIntf": "vxlan75100", + "localVtepIp": "120.1.1.1", + "sviIntf": "br100" + } + } + ] + } + } + + result = verify_evpn_vni(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for dut in input_dict.keys(): + rnode = tgen.gears[dut] + + logger.info("[DUT: %s]: Verifying evpn vni details :", dut) + + cmd = "show evpn vni detail json" + evpn_all_vni_json = run_frr_cmd(rnode, cmd, isjson=True) + if not bool(evpn_all_vni_json): + errormsg = "No output for '{}' cli".format(cmd) + return errormsg + + if "vni" in input_dict[dut]: + for vni_dict in input_dict[dut]["vni"]: + found = False + vni = vni_dict["name"] + for evpn_vni_json in evpn_all_vni_json: + if "vni" in evpn_vni_json: + if evpn_vni_json["vni"] != int(vni): + continue + + for attribute in vni_dict.keys(): + if vni_dict[attribute] != evpn_vni_json[attribute]: + errormsg = ( + "[DUT: %s] Verifying " + "%s for VNI: %s [FAILED]||" + ", EXPECTED : %s " + " FOUND : %s" + % ( + dut, + attribute, + vni, + vni_dict[attribute], + evpn_vni_json[attribute], + ) + ) + return errormsg + + else: + found = True + logger.info( + "[DUT: %s] Verifying" + " %s for VNI: %s , " + "Found Expected : %s ", + dut, + attribute, + vni, + evpn_vni_json[attribute], + ) + + if evpn_vni_json["state"] != "Up": + errormsg = ( + "[DUT: %s] Failed: Verifying" + " State for VNI: %s is not Up" % (dut, vni) + ) + return errormsg + + else: + errormsg = ( + "[DUT: %s] Failed:" + " VNI: %s is not present in JSON" % (dut, vni) + ) + return errormsg + + if found: + logger.info( + "[DUT %s]: Verifying VNI : %s " + "details and state is Up [PASSED]!!", + dut, + vni, + ) + return True + + else: + errormsg = ( + "[DUT: %s] Failed:" " vni details are not present in input data" % (dut) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return False + + +@retry(retry_timeout=12) +def verify_vrf_vni(tgen, input_dict): + """ + API to verify vrf vni details using "show vrf vni json" + command. + Parameters + ---------- + * `tgen`: topogen object + * `input_dict`: having details like - for which router, evpn details + needs to be verified + Usage + ----- + input_dict = { + "edge1":{ + "vrfs": [ + { + "RED":{ + "vni": 75000, + "vxlanIntf": "vxlan75100", + "sviIntf": "br100", + "routerMac": "00:80:48:ba:d1:00", + "state": "Up" + } + } + ] + } + } + + result = verify_vrf_vni(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + for dut in input_dict.keys(): + rnode = tgen.gears[dut] + + logger.info("[DUT: %s]: Verifying vrf vni details :", dut) + + cmd = "show vrf vni json" + vrf_all_vni_json = run_frr_cmd(rnode, cmd, isjson=True) + if not bool(vrf_all_vni_json): + errormsg = "No output for '{}' cli".format(cmd) + return errormsg + + if "vrfs" in input_dict[dut]: + for vrfs in input_dict[dut]["vrfs"]: + for vrf, vrf_dict in vrfs.items(): + found = False + for vrf_vni_json in vrf_all_vni_json["vrfs"]: + if "vrf" in vrf_vni_json: + if vrf_vni_json["vrf"] != vrf: + continue + + for attribute in vrf_dict.keys(): + if vrf_dict[attribute] == vrf_vni_json[attribute]: + found = True + logger.info( + "[DUT %s]: VRF: %s, " + "verifying %s " + ", Found Expected: %s " + "[PASSED]!!", + dut, + vrf, + attribute, + vrf_vni_json[attribute], + ) + else: + errormsg = ( + "[DUT: %s] VRF: %s, " + "verifying %s [FAILED!!] " + ", EXPECTED : %s " + ", FOUND : %s" + % ( + dut, + vrf, + attribute, + vrf_dict[attribute], + vrf_vni_json[attribute], + ) + ) + return errormsg + + else: + errormsg = "[DUT: %s] VRF: %s " "is not present in JSON" % ( + dut, + vrf, + ) + return errormsg + + if found: + logger.info( + "[DUT %s] Verifying VRF: %s " " details [PASSED]!!", + dut, + vrf, + ) + return True + + else: + errormsg = ( + "[DUT: %s] Failed:" " vrf details are not present in input data" % (dut) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return False + + +def required_linux_kernel_version(required_version): + """ + This API is used to check linux version compatibility of the test suite. + If version mentioned in required_version is higher than the linux kernel + of the system, test suite will be skipped. This API returns true or errormsg. + + Parameters + ---------- + * `required_version` : Kernel version required for the suites to run. + + Usage + ----- + result = linux_kernel_version_lowerthan('4.15') + + Returns + ------- + errormsg(str) or True + """ + system_kernel = platform.release() + if version_cmp(system_kernel, required_version) < 0: + error_msg = ( + 'These tests will not run on kernel "{}", ' + "they require kernel >= {})".format(system_kernel, required_version) + ) + + logger.info(error_msg) + + return error_msg + return True + + +class HostApplicationHelper(object): + """Helper to track and cleanup per-host based test processes.""" + + def __init__(self, tgen=None, base_cmd=None): + self.base_cmd_str = "" + self.host_procs = {} + self.tgen = None + self.set_base_cmd(base_cmd if base_cmd else []) + if tgen is not None: + self.init(tgen) + + def __enter__(self): + self.init() + return self + + def __exit__(self, type, value, traceback): + self.cleanup() + + def __str__(self): + return "HostApplicationHelper({})".format(self.base_cmd_str) + + def set_base_cmd(self, base_cmd): + assert isinstance(base_cmd, list) or isinstance(base_cmd, tuple) + self.base_cmd = base_cmd + if base_cmd: + self.base_cmd_str = " ".join(base_cmd) + else: + self.base_cmd_str = "" + + def init(self, tgen=None): + """Initialize the helper with tgen if needed. + + If overridden, need to handle multiple entries but one init. Will be called on + object creation if tgen is supplied. Will be called again on __enter__ so should + not re-init if already inited. + """ + if self.tgen: + assert tgen is None or self.tgen == tgen + else: + self.tgen = tgen + + def started_proc(self, host, p): + """Called after process started on host. + + Return value is passed to `stopping_proc` method.""" + logger.debug("%s: Doing nothing after starting process", self) + return False + + def stopping_proc(self, host, p, info): + """Called after process started on host.""" + logger.debug("%s: Doing nothing before stopping process", self) + + def _add_host_proc(self, host, p): + v = self.started_proc(host, p) + + if host not in self.host_procs: + self.host_procs[host] = [] + logger.debug("%s: %s: tracking process %s", self, host, p) + self.host_procs[host].append((p, v)) + + def stop_host(self, host): + """Stop the process on the host. + + Override to do additional cleanup.""" + if host in self.host_procs: + hlogger = self.tgen.net[host].logger + for p, v in self.host_procs[host]: + self.stopping_proc(host, p, v) + logger.debug("%s: %s: terminating process %s", self, host, p.pid) + hlogger.debug("%s: %s: terminating process %s", self, host, p.pid) + rc = p.poll() + if rc is not None: + logger.error( + "%s: %s: process early exit %s: %s", + self, + host, + p.pid, + comm_error(p), + ) + hlogger.error( + "%s: %s: process early exit %s: %s", + self, + host, + p.pid, + comm_error(p), + ) + else: + p.terminate() + p.wait() + logger.debug( + "%s: %s: terminated process %s: %s", + self, + host, + p.pid, + comm_error(p), + ) + hlogger.debug( + "%s: %s: terminated process %s: %s", + self, + host, + p.pid, + comm_error(p), + ) + + del self.host_procs[host] + + def stop_all_hosts(self): + hosts = set(self.host_procs) + for host in hosts: + self.stop_host(host) + + def cleanup(self): + self.stop_all_hosts() + + def run(self, host, cmd_args, **kwargs): + cmd = list(self.base_cmd) + cmd.extend(cmd_args) + p = self.tgen.gears[host].popen(cmd, **kwargs) + assert p.poll() is None + self._add_host_proc(host, p) + return p + + def check_procs(self): + """Check that all current processes are running, log errors if not. + + Returns: List of stopped processes.""" + procs = [] + + logger.debug("%s: checking procs on hosts %s", self, self.host_procs.keys()) + + for host in self.host_procs: + hlogger = self.tgen.net[host].logger + for p, _ in self.host_procs[host]: + logger.debug("%s: checking %s proc %s", self, host, p) + rc = p.poll() + if rc is None: + continue + logger.error( + "%s: %s proc exited: %s", self, host, comm_error(p), exc_info=True + ) + hlogger.error( + "%s: %s proc exited: %s", self, host, comm_error(p), exc_info=True + ) + procs.append(p) + return procs + + +class IPerfHelper(HostApplicationHelper): + def __str__(self): + return "IPerfHelper()" + + def run_join( + self, + host, + join_addr, + l4Type="UDP", + join_interval=1, + join_intf=None, + join_towards=None, + ): + """ + Use iperf to send IGMP join and listen to traffic + + Parameters: + ----------- + * `host`: iperf host from where IGMP join would be sent + * `l4Type`: string, one of [ TCP, UDP ] + * `join_addr`: multicast address (or addresses) to join to + * `join_interval`: seconds between periodic bandwidth reports + * `join_intf`: the interface to bind the join to + * `join_towards`: router whos interface to bind the join to + + returns: Success (bool) + """ + + iperf_path = self.tgen.net.get_exec_path("iperf") + + assert join_addr + if not isinstance(join_addr, list) and not isinstance(join_addr, tuple): + join_addr = [ipaddress.IPv4Address(frr_unicode(join_addr))] + + for bindTo in join_addr: + iperf_args = [iperf_path, "-s"] + + if l4Type == "UDP": + iperf_args.append("-u") + + iperf_args.append("-B") + if join_towards: + to_intf = frr_unicode( + self.tgen.json_topo["routers"][host]["links"][join_towards][ + "interface" + ] + ) + iperf_args.append("{}%{}".format(str(bindTo), to_intf)) + elif join_intf: + iperf_args.append("{}%{}".format(str(bindTo), join_intf)) + else: + iperf_args.append(str(bindTo)) + + if join_interval: + iperf_args.append("-i") + iperf_args.append(str(join_interval)) + + p = self.run(host, iperf_args) + if p.poll() is not None: + logger.error("IGMP join failed on %s: %s", bindTo, comm_error(p)) + return False + return True + + def run_traffic( + self, host, sentToAddress, ttl, time=0, l4Type="UDP", bind_towards=None + ): + """ + Run iperf to send IGMP join and traffic + + Parameters: + ----------- + * `host`: iperf host to send traffic from + * `l4Type`: string, one of [ TCP, UDP ] + * `sentToAddress`: multicast address to send traffic to + * `ttl`: time to live + * `time`: time in seconds to transmit for + * `bind_towards`: Router who's interface the source ip address is got from + + returns: Success (bool) + """ + + iperf_path = self.tgen.net.get_exec_path("iperf") + + if sentToAddress and not isinstance(sentToAddress, list): + sentToAddress = [ipaddress.IPv4Address(frr_unicode(sentToAddress))] + + for sendTo in sentToAddress: + iperf_args = [iperf_path, "-c", sendTo] + + # Bind to Interface IP + if bind_towards: + ifaddr = frr_unicode( + self.tgen.json_topo["routers"][host]["links"][bind_towards]["ipv4"] + ) + ipaddr = ipaddress.IPv4Interface(ifaddr).ip + iperf_args.append("-B") + iperf_args.append(str(ipaddr)) + + # UDP/TCP + if l4Type == "UDP": + iperf_args.append("-u") + iperf_args.append("-b") + iperf_args.append("0.012m") + + # TTL + if ttl: + iperf_args.append("-T") + iperf_args.append(str(ttl)) + + # Time + if time: + iperf_args.append("-t") + iperf_args.append(str(time)) + + p = self.run(host, iperf_args) + if p.poll() is not None: + logger.error( + "mcast traffic send failed for %s: %s", sendTo, comm_error(p) + ) + return False + + return True + + +def verify_ip_nht(tgen, input_dict): + """ + Running "show ip nht" command and verifying given nexthop resolution + Parameters + ---------- + * `tgen` : topogen object + * `input_dict`: data to verify nexthop + Usage + ----- + input_dict_4 = { + "r1": { + nh: { + "Address": nh, + "resolvedVia": "connected", + "nexthops": { + "nexthop1": { + "Interface": intf + } + } + } + } + } + result = verify_ip_nht(tgen, input_dict_4) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: verify_ip_nht()") + + router_list = tgen.routers() + for router in input_dict.keys(): + if router not in router_list: + continue + + rnode = router_list[router] + nh_list = input_dict[router] + + if validate_ip_address(next(iter(nh_list))) == "ipv6": + show_ip_nht = run_frr_cmd(rnode, "show ipv6 nht") + else: + show_ip_nht = run_frr_cmd(rnode, "show ip nht") + + for nh in nh_list: + if nh in show_ip_nht: + nht = run_frr_cmd(rnode, "show ip nht {}".format(nh)) + if "unresolved" in nht: + errormsg = "Nexthop {} became unresolved on {}".format(nh, router) + return errormsg + else: + logger.info("Nexthop %s is resolved on %s", nh, router) + return True + else: + errormsg = "Nexthop {} is resolved on {}".format(nh, router) + return errormsg + + logger.debug("Exiting lib API: verify_ip_nht()") + return False + + +def scapy_send_raw_packet(tgen, topo, senderRouter, intf, packet=None): + """ + Using scapy Raw() method to send BSR raw packet from one FRR + to other + + Parameters: + ----------- + * `tgen` : Topogen object + * `topo` : json file data + * `senderRouter` : Sender router + * `packet` : packet in raw format + + returns: + -------- + errormsg or True + """ + + global CD + result = "" + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + sender_interface = intf + rnode = tgen.routers()[senderRouter] + + for destLink, data in topo["routers"][senderRouter]["links"].items(): + if "type" in data and data["type"] == "loopback": + continue + + if not packet: + packet = topo["routers"][senderRouter]["pkt"]["test_packets"][packet][ + "data" + ] + + python3_path = tgen.net.get_exec_path(["python3", "python"]) + script_path = os.path.join(CD, "send_bsr_packet.py") + cmd = "{} {} '{}' '{}' --interval=1 --count=1".format( + python3_path, script_path, packet, sender_interface + ) + + logger.info("Scapy cmd: \n %s", cmd) + result = rnode.run(cmd) + + if result == "": + return result + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True diff --git a/tests/topotests/lib/exa-receive.py b/tests/topotests/lib/exa-receive.py new file mode 100755 index 0000000..2ea3a75 --- /dev/null +++ b/tests/topotests/lib/exa-receive.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 + +""" +exa-receive.py: Save received routes form ExaBGP into file +""" + +import argparse +import os +from sys import stdin +from datetime import datetime + +parser = argparse.ArgumentParser() +parser.add_argument( + "--no-timestamp", dest="timestamp", action="store_false", help="Disable timestamps" +) +parser.add_argument( + "--logdir", default="/tmp/gearlogdir", help="The directory to store the peer log in" +) +parser.add_argument("peer", type=int, help="The peer number") +args = parser.parse_args() + +savepath = os.path.join(args.logdir, "peer{}-received.log".format(args.peer)) +routesavefile = open(savepath, "w") + +while True: + try: + line = stdin.readline() + if not line: + break + + if not args.timestamp: + routesavefile.write(line) + else: + timestamp = datetime.now().strftime("%Y%m%d_%H:%M:%S - ") + routesavefile.write(timestamp + line) + routesavefile.flush() + except KeyboardInterrupt: + pass + except IOError: + # most likely a signal during readline + pass + +routesavefile.close() diff --git a/tests/topotests/lib/fixtures.py b/tests/topotests/lib/fixtures.py new file mode 100644 index 0000000..9d8f63a --- /dev/null +++ b/tests/topotests/lib/fixtures.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 eval: (yapf-mode 1) -*- +# +# August 27 2021, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 2021, LabN Consulting, L.L.C. ("LabN") +# +# 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 lib.topojson as topojson +import lib.topogen as topogen +from lib.topolog import logger + + +def tgen_json(request): + logger.info("Creating/starting topogen topology for %s", request.module.__name__) + + tgen = topojson.setup_module_from_json(request.module.__file__) + yield tgen + + logger.info("Stopping topogen topology for %s", request.module.__name__) + tgen.stop_topology() + + +def topo(tgen): + """Make tgen json object available as test argument.""" + return tgen.json_topo + + +def tgen(): + """Make global topogen object available as test argument.""" + return topogen.get_topogen() diff --git a/tests/topotests/lib/grpc-query.py b/tests/topotests/lib/grpc-query.py new file mode 100755 index 0000000..61f01c3 --- /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() diff --git a/tests/topotests/lib/ltemplate.py b/tests/topotests/lib/ltemplate.py new file mode 100644 index 0000000..2544023 --- /dev/null +++ b/tests/topotests/lib/ltemplate.py @@ -0,0 +1,320 @@ +#!/usr/bin/env python + +# +# Part of NetDEF Topology Tests +# +# Copyright (c) 2017 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +ltemplate.py: LabN template for FRR tests. +""" + +import os +import sys +import platform + +import pytest + +# pylint: disable=C0413 +# Import topogen and topotest helpers +from lib import topotest +from lib.topogen import Topogen, TopoRouter, get_topogen +from lib.topolog import logger +from lib.lutil import * + +# Required to instantiate the topology builder class. + +customize = None + + +class LTemplate: + test = None + testdir = None + scriptdir = None + logdir = None + prestarthooksuccess = True + poststarthooksuccess = True + iproute2Ver = None + + def __init__(self, test, testdir): + pathname = os.path.join(testdir, "customize.py") + global customize + if sys.version_info >= (3, 5): + import importlib.util + + spec = importlib.util.spec_from_file_location("customize", pathname) + customize = importlib.util.module_from_spec(spec) + spec.loader.exec_module(customize) + else: + import imp + + customize = imp.load_source("customize", pathname) + self.test = test + self.testdir = testdir + self.scriptdir = testdir + self.logdir = "" + logger.info("LTemplate: " + test) + + def setup_module(self, mod): + "Sets up the pytest environment" + # This function initiates the topology build with Topogen... + tgen = Topogen(customize.build_topo, mod.__name__) + # ... and here it calls Mininet initialization functions. + tgen.start_topology() + + self.logdir = tgen.logdir + + logger.info("Topology started") + try: + self.prestarthooksuccess = customize.ltemplatePreRouterStartHook() + except AttributeError: + # not defined + logger.debug("ltemplatePreRouterStartHook() not defined") + if self.prestarthooksuccess != True: + logger.info("ltemplatePreRouterStartHook() failed, skipping test") + return + + # This is a sample of configuration loading. + router_list = tgen.routers() + + # For all registered routers, load the zebra configuration file + for rname, router in router_list.items(): + logger.info("Setting up %s" % rname) + for rd_val in TopoRouter.RD: + config = os.path.join( + self.testdir, "{}/{}.conf".format(rname, TopoRouter.RD[rd_val]) + ) + prog = os.path.join(tgen.net[rname].daemondir, TopoRouter.RD[rd_val]) + if os.path.exists(config): + if os.path.exists(prog): + router.load_config(rd_val, config) + else: + logger.warning( + "{} not found, but have {}.conf file".format( + prog, TopoRouter.RD[rd_val] + ) + ) + + # After loading the configurations, this function loads configured daemons. + logger.info("Starting routers") + tgen.start_router() + try: + self.poststarthooksuccess = customize.ltemplatePostRouterStartHook() + except AttributeError: + # not defined + logger.debug("ltemplatePostRouterStartHook() not defined") + luStart(baseScriptDir=self.scriptdir, baseLogDir=self.logdir, net=tgen.net) + + +# initialized by ltemplate_start +_lt = None + + +def setup_module(mod): + global _lt + root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + test = mod.__name__[: mod.__name__.rfind(".")] + testdir = os.path.join(root, test) + + # don't do this for now as reload didn't work as expected + # fixup sys.path, want test dir there only once + # try: + # sys.path.remove(testdir) + # except ValueError: + # logger.debug(testdir+" not found in original sys.path") + # add testdir + # sys.path.append(testdir) + + # init class + _lt = LTemplate(test, testdir) + _lt.setup_module(mod) + + # drop testdir + # sys.path.remove(testdir) + + +def teardown_module(mod): + global _lt + "Teardown the pytest environment" + tgen = get_topogen() + + if _lt != None and _lt.scriptdir != None and _lt.prestarthooksuccess == True: + luShowResults(logger.info) + print(luFinish()) + + # This function tears down the whole topology. + tgen.stop_topology() + _lt = None + + +def ltemplateTest( + script, SkipIfFailed=True, CallOnFail=None, CheckFuncStr=None, KeepGoing=False +): + global _lt + if _lt == None or _lt.prestarthooksuccess != True: + return + + tgen = get_topogen() + if not os.path.isfile(script): + if not os.path.isfile(os.path.join(_lt.scriptdir, script)): + logger.error("Could not find script file: " + script) + assert "Could not find script file: " + script + logger.info("Starting template test: " + script) + numEntry = luNumFail() + + if SkipIfFailed and tgen.routers_have_failure(): + pytest.skip(tgen.errors) + if numEntry > 0: + if not KeepGoing: + pytest.skip("Have %d errors" % numEntry) + + if CheckFuncStr != None: + check = eval(CheckFuncStr) + if check != True: + pytest.skip("Check function '" + CheckFuncStr + "' returned: " + check) + + if CallOnFail != None: + CallOnFail = eval(CallOnFail) + luInclude(script, CallOnFail) + numFail = luNumFail() - numEntry + if numFail > 0: + luShowFail() + fatal_error = "%d tests failed" % numFail + if not KeepGoing: + assert ( + "scripts/cleanup_all.py failed" == "See summary output above" + ), fatal_error + + +# Memory leak test template +def test_memory_leak(): + "Run the memory leak test and report results." + tgen = get_topogen() + if not tgen.is_memleak_enabled(): + pytest.skip("Memory leak test/report is disabled") + + tgen.report_memory_leaks() + + +class ltemplateRtrCmd: + def __init__(self): + self.resetCounts() + + def doCmd(self, tgen, rtr, cmd, checkstr=None): + logger.info("doCmd: {} {}".format(rtr, cmd)) + output = tgen.net[rtr].cmd(cmd).strip() + if len(output): + self.output += 1 + if checkstr != None: + ret = re.search(checkstr, output) + if ret == None: + self.nomatch += 1 + else: + self.match += 1 + return ret + logger.info("output: " + output) + else: + logger.info("No output") + self.none += 1 + return None + + def resetCounts(self): + self.match = 0 + self.nomatch = 0 + self.output = 0 + self.none = 0 + + def getMatch(self): + return self.match + + def getNoMatch(self): + return self.nomatch + + def getOutput(self): + return self.output + + def getNone(self): + return self.none + + +def ltemplateVersionCheck( + vstr, rname="r1", compstr="<", cli=False, kernel="4.9", iproute2=None, mpls=True +): + tgen = get_topogen() + router = tgen.gears[rname] + + if cli: + logger.info("calling mininet CLI") + tgen.mininet_cli() + logger.info("exited mininet CLI") + + if _lt == None: + ret = "Template not initialized" + return ret + + if _lt.prestarthooksuccess != True: + ret = "ltemplatePreRouterStartHook failed" + return ret + + if _lt.poststarthooksuccess != True: + ret = "ltemplatePostRouterStartHook failed" + return ret + + if mpls == True and tgen.hasmpls != True: + ret = "MPLS not initialized" + return ret + + if kernel != None: + krel = platform.release() + if topotest.version_cmp(krel, kernel) < 0: + ret = "Skipping tests, old kernel ({} < {})".format(krel, kernel) + return ret + + if iproute2 != None: + if _lt.iproute2Ver == None: + # collect/log info on iproute2 + cc = ltemplateRtrCmd() + found = cc.doCmd( + tgen, rname, "apt-cache policy iproute2", r"Installed: ([\d\.]*)" + ) + if found != None: + iproute2Ver = found.group(1) + else: + iproute2Ver = "0-unknown" + logger.info("Have iproute2 version=" + iproute2Ver) + + if topotest.version_cmp(iproute2Ver, iproute2) < 0: + ret = "Skipping tests, old iproute2 ({} < {})".format(iproute2Ver, iproute2) + return ret + + ret = True + try: + if router.has_version(compstr, vstr): + ret = "Skipping tests, old FRR version {} {}".format(compstr, vstr) + return ret + except: + ret = True + + return ret + + +# for testing +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args)) diff --git a/tests/topotests/lib/lutil.py b/tests/topotests/lib/lutil.py new file mode 100644 index 0000000..5c1fa24 --- /dev/null +++ b/tests/topotests/lib/lutil.py @@ -0,0 +1,465 @@ +#!/usr/bin/env python + +# Copyright 2017, 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 os +import re +import sys +import time +import json +import math +import time +from lib.topolog import logger +from lib.topotest import json_cmp + + +# L utility functions +# +# These functions are inteneted to provide support for CI testing within MiniNet +# environments. + + +class lUtil: + # to be made configurable in the future + base_script_dir = "." + base_log_dir = "." + fout_name = "output.log" + fsum_name = "summary.txt" + l_level = 6 + CallOnFail = False + + l_total = 0 + l_pass = 0 + l_fail = 0 + l_filename = "" + l_last = None + l_line = 0 + l_dotall_experiment = False + l_last_nl = None + + fout = "" + fsum = "" + net = "" + + def log(self, str, level=6): + if self.l_level > 0: + if self.fout == "": + self.fout = open(self.fout_name, "w") + self.fout.write(str + "\n") + if level <= self.l_level: + print(str) + + def summary(self, str): + if self.fsum == "": + self.fsum = open(self.fsum_name, "w") + self.fsum.write( + "\ +******************************************************************************\n" + ) + self.fsum.write( + "\ +Test Target Summary Pass Fail\n" + ) + self.fsum.write( + "\ +******************************************************************************\n" + ) + self.fsum.write(str + "\n") + + def result(self, target, success, str, logstr=None): + if success: + p = 1 + f = 0 + self.l_pass += 1 + sstr = "PASS" + else: + f = 1 + p = 0 + self.l_fail += 1 + sstr = "FAIL" + self.l_total += 1 + if logstr != None: + self.log("R:%d %s: %s" % (self.l_total, sstr, logstr)) + res = "%-4d %-6s %-56s %-4d %d" % (self.l_total, target, str, p, f) + self.log("R:" + res) + self.summary(res) + if f == 1 and self.CallOnFail != False: + self.CallOnFail() + + def closeFiles(self): + ret = ( + "\ +******************************************************************************\n\ +Total %-4d %-4d %d\n\ +******************************************************************************" + % (self.l_total, self.l_pass, self.l_fail) + ) + if self.fsum != "": + self.fsum.write(ret + "\n") + self.fsum.close() + self.fsum = "" + if self.fout != "": + if os.path.isfile(self.fsum_name): + r = open(self.fsum_name, "r") + self.fout.write(r.read()) + r.close() + self.fout.close() + self.fout = "" + return ret + + def setFilename(self, name): + str = "FILE: " + name + self.log(str) + self.summary(str) + self.l_filename = name + self.line = 0 + + def getCallOnFail(self): + return self.CallOnFail + + def setCallOnFail(self, CallOnFail): + self.CallOnFail = CallOnFail + + def strToArray(self, string): + a = [] + c = 0 + end = "" + words = string.split() + if len(words) < 1 or words[0].startswith("#"): + return a + words = string.split() + for word in words: + if len(end) == 0: + a.append(word) + else: + a[c] += str(" " + word) + if end == "\\": + end = "" + if not word.endswith("\\"): + if end != '"': + if word.startswith('"'): + end = '"' + else: + c += 1 + else: + if word.endswith('"'): + end = "" + c += 1 + else: + c += 1 + else: + end = "\\" + # if len(end) == 0: + # print('%d:%s:' % (c, a[c-1])) + + return a + + def execTestFile(self, tstFile): + if os.path.isfile(tstFile): + f = open(tstFile) + for line in f: + if len(line) > 1: + a = self.strToArray(line) + if len(a) >= 6: + luCommand(a[1], a[2], a[3], a[4], a[5]) + else: + self.l_line += 1 + self.log("%s:%s %s" % (self.l_filename, self.l_line, line)) + if len(a) >= 2: + if a[0] == "sleep": + time.sleep(int(a[1])) + elif a[0] == "include": + self.execTestFile(a[1]) + f.close() + else: + self.log("unable to read: " + tstFile) + sys.exit(1) + + def command(self, target, command, regexp, op, result, returnJson, startt=None): + global net + if op == "jsoncmp_pass" or op == "jsoncmp_fail": + returnJson = True + + self.log( + "%s (#%d) %s:%s COMMAND:%s:%s:%s:%s:%s:" + % ( + time.asctime(), + self.l_total + 1, + self.l_filename, + self.l_line, + target, + command, + regexp, + op, + result, + ) + ) + if self.net == "": + return False + # self.log("Running %s %s" % (target, command)) + js = None + out = self.net[target].cmd(command).rstrip() + if len(out) == 0: + report = "<no output>" + else: + report = out + if returnJson == True: + try: + js = json.loads(out) + except: + js = None + self.log( + "WARNING: JSON load failed -- confirm command output is in JSON format." + ) + self.log("COMMAND OUTPUT:%s:" % report) + + # JSON comparison + if op == "jsoncmp_pass" or op == "jsoncmp_fail": + try: + expect = json.loads(regexp) + except: + expect = None + self.log( + "WARNING: JSON load failed -- confirm regex input is in JSON format." + ) + json_diff = json_cmp(js, expect) + if json_diff != None: + if op == "jsoncmp_fail": + success = True + else: + success = False + self.log("JSON DIFF:%s:" % json_diff) + ret = success + else: + if op == "jsoncmp_fail": + success = False + else: + success = True + self.result(target, success, result) + if js != None: + return js + return ret + + # Experiment: can we achieve the same match behavior via DOTALL + # without converting newlines to spaces? + out_nl = out + search_nl = re.search(regexp, out_nl, re.DOTALL) + self.l_last_nl = search_nl + # Set up for comparison + if search_nl != None: + group_nl = search_nl.group() + group_nl_converted = " ".join(group_nl.splitlines()) + else: + group_nl_converted = None + + out = " ".join(out.splitlines()) + search = re.search(regexp, out) + self.l_last = search + if search == None: + if op == "fail": + success = True + else: + success = False + ret = success + else: + ret = search.group() + if op != "fail": + success = True + level = 7 + else: + success = False + level = 5 + self.log("found:%s:" % ret, level) + # Experiment: compare matched strings obtained each way + if self.l_dotall_experiment and (group_nl_converted != ret): + self.log( + "DOTALL experiment: strings differ dotall=[%s] orig=[%s]" + % (group_nl_converted, ret), + 9, + ) + if startt != None: + if js != None or ret is not False: + delta = time.time() - startt + self.result(target, success, "%s +%4.2f secs" % (result, delta)) + elif op == "pass" or op == "fail": + self.result(target, success, result) + if js != None: + return js + return ret + + def wait( + self, target, command, regexp, op, result, wait, returnJson, wait_time=0.5 + ): + self.log( + "%s:%s WAIT:%s:%s:%s:%s:%s:%s:%s:" + % ( + self.l_filename, + self.l_line, + target, + command, + regexp, + op, + result, + wait, + wait_time, + ) + ) + found = False + n = 0 + startt = time.time() + + # Calculate the amount of `sleep`s we are going to peform. + wait_count = int(math.ceil(wait / wait_time)) + 1 + + while wait_count > 0: + n += 1 + found = self.command(target, command, regexp, op, result, returnJson, startt) + if found is not False: + break + + wait_count -= 1 + if wait_count > 0: + time.sleep(wait_time) + + delta = time.time() - startt + self.log("Done after %d loops, time=%s, Found=%s" % (n, delta, found)) + return found + + +# initialized by luStart +LUtil = None + +# entry calls +def luStart( + baseScriptDir=".", + baseLogDir=".", + net="", + fout="output.log", + fsum="summary.txt", + level=None, +): + global LUtil + # init class + LUtil = lUtil() + LUtil.base_script_dir = baseScriptDir + LUtil.base_log_dir = baseLogDir + LUtil.net = net + if fout != "": + LUtil.fout_name = baseLogDir + "/" + fout + if fsum != None: + LUtil.fsum_name = baseLogDir + "/" + fsum + if level != None: + LUtil.l_level = level + LUtil.l_dotall_experiment = False + LUtil.l_dotall_experiment = True + + +def luCommand( + target, + command, + regexp=".", + op="none", + result="", + time=10, + returnJson=False, + wait_time=0.5, +): + if op != "wait": + return LUtil.command(target, command, regexp, op, result, returnJson) + else: + return LUtil.wait( + target, command, regexp, op, result, time, returnJson, wait_time + ) + + +def luLast(usenl=False): + if usenl: + if LUtil.l_last_nl != None: + LUtil.log("luLast:%s:" % LUtil.l_last_nl.group(), 7) + return LUtil.l_last_nl + else: + if LUtil.l_last != None: + LUtil.log("luLast:%s:" % LUtil.l_last.group(), 7) + return LUtil.l_last + + +def luInclude(filename, CallOnFail=None): + tstFile = LUtil.base_script_dir + "/" + filename + LUtil.setFilename(filename) + if CallOnFail != None: + oldCallOnFail = LUtil.getCallOnFail() + LUtil.setCallOnFail(CallOnFail) + if filename.endswith(".py"): + LUtil.log("luInclude: execfile " + tstFile) + with open(tstFile) as infile: + exec(infile.read()) + else: + LUtil.log("luInclude: execTestFile " + tstFile) + LUtil.execTestFile(tstFile) + if CallOnFail != None: + LUtil.setCallOnFail(oldCallOnFail) + + +def luFinish(): + global LUtil + ret = LUtil.closeFiles() + # done + LUtil = None + return ret + + +def luNumFail(): + return LUtil.l_fail + + +def luNumPass(): + return LUtil.l_pass + + +def luResult(target, success, str, logstr=None): + return LUtil.result(target, success, str, logstr) + + +def luShowResults(prFunction): + printed = 0 + sf = open(LUtil.fsum_name, "r") + for line in sf: + printed += 1 + prFunction(line.rstrip()) + sf.close() + + +def luShowFail(): + printed = 0 + sf = open(LUtil.fsum_name, "r") + for line in sf: + if line[-2] != "0": + printed += 1 + logger.error(line.rstrip()) + sf.close() + if printed > 0: + logger.error("See %s for details of errors" % LUtil.fout_name) + + +# for testing +if __name__ == "__main__": + print(os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + "/lib") + luStart() + for arg in sys.argv[1:]: + luInclude(arg) + luFinish() + sys.exit(0) diff --git a/tests/topotests/lib/mcast-tester.py b/tests/topotests/lib/mcast-tester.py new file mode 100755 index 0000000..30beccb --- /dev/null +++ b/tests/topotests/lib/mcast-tester.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +# REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +# INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +# LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +# OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. + +""" +Subscribe to a multicast group so that the kernel sends an IGMP JOIN +for the multicast group we subscribed to. +""" + +import argparse +import json +import os +import socket +import struct +import subprocess +import sys +import time + + +# +# Functions +# +def interface_name_to_index(name): + "Gets the interface index using its name. Returns None on failure." + interfaces = json.loads(subprocess.check_output("ip -j link show", shell=True)) + + for interface in interfaces: + if interface["ifname"] == name: + return interface["ifindex"] + + return None + + +def multicast_join(sock, ifindex, group, port): + "Joins a multicast group." + mreq = struct.pack( + "=4sLL", socket.inet_aton(args.group), socket.INADDR_ANY, ifindex + ) + + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + sock.bind((group, port)) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP, mreq) + + +# +# Main code. +# +parser = argparse.ArgumentParser(description="Multicast RX utility") +parser.add_argument("group", help="Multicast IP") +parser.add_argument("interface", help="Interface name") +parser.add_argument("--socket", help="Point to topotest UNIX socket") +parser.add_argument( + "--send", help="Transmit instead of join with interval", type=float, default=0 +) +args = parser.parse_args() + +ttl = 16 +port = 1000 + +# Get interface index/validate. +ifindex = interface_name_to_index(args.interface) +if ifindex is None: + sys.stderr.write("Interface {} does not exists\n".format(args.interface)) + sys.exit(1) + +# We need root privileges to set up multicast. +if os.geteuid() != 0: + sys.stderr.write("ERROR: You must have root privileges\n") + sys.exit(1) + +# Wait for topotest to synchronize with us. +if not args.socket: + toposock = None +else: + toposock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) + while True: + try: + toposock.connect(args.socket) + break + except ConnectionRefusedError: + time.sleep(1) + continue + # Set topotest socket non blocking so we can multiplex the main loop. + toposock.setblocking(False) + +msock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) +if args.send > 0: + # Prepare multicast bit in that interface. + msock.setsockopt( + socket.SOL_SOCKET, + 25, + struct.pack("%ds" % len(args.interface), args.interface.encode("utf-8")), + ) + # Set packets TTL. + msock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, struct.pack("b", ttl)) + # Block to ensure packet send. + msock.setblocking(True) +else: + multicast_join(msock, ifindex, args.group, port) + + +def should_exit(): + if not toposock: + # If we are sending then we have slept + if not args.send: + time.sleep(100) + return False + else: + try: + data = toposock.recv(1) + if data == b"": + print(" -> Connection closed") + return True + except BlockingIOError: + return False + + +counter = 0 +while not should_exit(): + if args.send > 0: + msock.sendto(b"test %d" % counter, (args.group, port)) + counter += 1 + time.sleep(args.send) + +msock.close() +sys.exit(0) diff --git a/tests/topotests/lib/micronet.py b/tests/topotests/lib/micronet.py new file mode 100644 index 0000000..dfa10cc --- /dev/null +++ b/tests/topotests/lib/micronet.py @@ -0,0 +1,1005 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# July 9 2021, Christian Hopps <chopps@labn.net> +# +# 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 +# +import datetime +import logging +import os +import re +import shlex +import subprocess +import sys +import tempfile +import time as time_mod +import traceback + +root_hostname = subprocess.check_output("hostname") + +# This allows us to cleanup any leftovers later on +os.environ["MICRONET_PID"] = str(os.getpid()) + + +class Timeout(object): + def __init__(self, delta): + self.started_on = datetime.datetime.now() + self.expires_on = self.started_on + datetime.timedelta(seconds=delta) + + def elapsed(self): + elapsed = datetime.datetime.now() - self.started_on + return elapsed.total_seconds() + + def is_expired(self): + return datetime.datetime.now() > self.expires_on + + +def is_string(value): + """Return True if value is a string.""" + try: + return isinstance(value, basestring) # type: ignore + except NameError: + return isinstance(value, str) + + +def shell_quote(command): + """Return command wrapped in single quotes.""" + if sys.version_info[0] >= 3: + return shlex.quote(command) + return "'{}'".format(command.replace("'", "'\"'\"'")) # type: ignore + + +def cmd_error(rc, o, e): + s = "rc {}".format(rc) + o = "\n\tstdout: " + o.strip() if o and o.strip() else "" + e = "\n\tstderr: " + e.strip() if e and e.strip() else "" + return s + o + e + + +def proc_error(p, o, e): + args = p.args if is_string(p.args) else " ".join(p.args) + s = "rc {} pid {}\n\targs: {}".format(p.returncode, p.pid, args) + o = "\n\tstdout: " + o.strip() if o and o.strip() else "" + e = "\n\tstderr: " + e.strip() if e and e.strip() else "" + return s + o + e + + +def comm_error(p): + rc = p.poll() + assert rc is not None + if not hasattr(p, "saved_output"): + p.saved_output = p.communicate() + return proc_error(p, *p.saved_output) + + +class Commander(object): # pylint: disable=R0205 + """ + Commander. + + An object that can execute commands. + """ + + tmux_wait_gen = 0 + + def __init__(self, name, logger=None): + """Create a Commander.""" + self.name = name + self.last = None + self.exec_paths = {} + self.pre_cmd = [] + self.pre_cmd_str = "" + + if not logger: + self.logger = logging.getLogger(__name__ + ".commander." + name) + else: + self.logger = logger + + self.cwd = self.cmd_raises("pwd").strip() + + def set_logger(self, logfile): + self.logger = logging.getLogger(__name__ + ".commander." + self.name) + if is_string(logfile): + handler = logging.FileHandler(logfile, mode="w") + else: + handler = logging.StreamHandler(logfile) + + fmtstr = "%(asctime)s.%(msecs)03d %(levelname)s: {}({}): %(message)s".format( + self.__class__.__name__, self.name + ) + handler.setFormatter(logging.Formatter(fmt=fmtstr)) + self.logger.addHandler(handler) + + def set_pre_cmd(self, pre_cmd=None): + if not pre_cmd: + self.pre_cmd = [] + self.pre_cmd_str = "" + else: + self.pre_cmd = pre_cmd + self.pre_cmd_str = " ".join(self.pre_cmd) + " " + + def __str__(self): + return "Commander({})".format(self.name) + + def get_exec_path(self, binary): + """Return the full path to the binary executable. + + `binary` :: binary name or list of binary names + """ + if is_string(binary): + bins = [binary] + else: + bins = binary + for b in bins: + if b in self.exec_paths: + return self.exec_paths[b] + + rc, output, _ = self.cmd_status("which " + b, warn=False) + if not rc: + return os.path.abspath(output.strip()) + return None + + def get_tmp_dir(self, uniq): + return os.path.join(tempfile.mkdtemp(), uniq) + + def test(self, flags, arg): + """Run test binary, with flags and arg""" + test_path = self.get_exec_path(["test"]) + rc, output, _ = self.cmd_status([test_path, flags, arg], warn=False) + return not rc + + def path_exists(self, path): + """Check if path exists.""" + return self.test("-e", path) + + def _get_cmd_str(self, cmd): + if is_string(cmd): + return self.pre_cmd_str + cmd + cmd = self.pre_cmd + cmd + return " ".join(cmd) + + def _get_sub_args(self, cmd, defaults, **kwargs): + if is_string(cmd): + defaults["shell"] = True + pre_cmd = self.pre_cmd_str + else: + defaults["shell"] = False + pre_cmd = self.pre_cmd + cmd = [str(x) for x in cmd] + defaults.update(kwargs) + return pre_cmd, cmd, defaults + + def _popen(self, method, cmd, skip_pre_cmd=False, **kwargs): + if sys.version_info[0] >= 3: + defaults = { + "encoding": "utf-8", + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + } + else: + defaults = { + "stdout": subprocess.PIPE, + "stderr": subprocess.PIPE, + } + pre_cmd, cmd, defaults = self._get_sub_args(cmd, defaults, **kwargs) + + self.logger.debug('%s: %s("%s", kwargs: %s)', self, method, cmd, defaults) + + actual_cmd = cmd if skip_pre_cmd else pre_cmd + cmd + p = subprocess.Popen(actual_cmd, **defaults) + if not hasattr(p, "args"): + p.args = actual_cmd + return p, actual_cmd + + def set_cwd(self, cwd): + self.logger.warning("%s: 'cd' (%s) does not work outside namespaces", self, cwd) + self.cwd = cwd + + def popen(self, cmd, **kwargs): + """ + Creates a pipe with the given `command`. + + Args: + command: `str` or `list` of command to open a pipe with. + **kwargs: kwargs is eventually passed on to Popen. If `command` is a string + then will be invoked with shell=True, otherwise `command` is a list and + will be invoked with shell=False. + + Returns: + a subprocess.Popen object. + """ + p, _ = self._popen("popen", cmd, **kwargs) + return p + + def cmd_status(self, cmd, raises=False, warn=True, stdin=None, **kwargs): + """Execute a command.""" + + # We are not a shell like mininet, so we need to intercept this + chdir = False + if not is_string(cmd): + cmds = cmd + else: + # XXX we can drop this when the code stops assuming it works + m = re.match(r"cd(\s*|\s+(\S+))$", cmd) + if m and m.group(2): + self.logger.warning( + "Bad call to 'cd' (chdir) emulating, use self.set_cwd():\n%s", + "".join(traceback.format_stack(limit=12)), + ) + assert is_string(cmd) + chdir = True + cmd += " && pwd" + + # If we are going to run under bash then we don't need shell=True! + cmds = ["/bin/bash", "-c", cmd] + + pinput = None + + if is_string(stdin) or isinstance(stdin, bytes): + pinput = stdin + stdin = subprocess.PIPE + + p, actual_cmd = self._popen("cmd_status", cmds, stdin=stdin, **kwargs) + stdout, stderr = p.communicate(input=pinput) + rc = p.wait() + + # For debugging purposes. + self.last = (rc, actual_cmd, cmd, stdout, stderr) + + if rc: + if warn: + self.logger.warning( + "%s: proc failed: %s:", self, proc_error(p, stdout, stderr) + ) + if raises: + # error = Exception("stderr: {}".format(stderr)) + # This annoyingly doesn't' show stderr when printed normally + error = subprocess.CalledProcessError(rc, actual_cmd) + error.stdout, error.stderr = stdout, stderr + raise error + elif chdir: + self.set_cwd(stdout.strip()) + + return rc, stdout, stderr + + def cmd_legacy(self, cmd, **kwargs): + """Execute a command with stdout and stderr joined, *IGNORES ERROR*.""" + + defaults = {"stderr": subprocess.STDOUT} + defaults.update(kwargs) + _, stdout, _ = self.cmd_status(cmd, raises=False, **defaults) + return stdout + + def cmd_raises(self, cmd, **kwargs): + """Execute a command. Raise an exception on errors""" + + rc, stdout, _ = self.cmd_status(cmd, raises=True, **kwargs) + assert rc == 0 + return stdout + + # Run a command in a new window (gnome-terminal, screen, tmux, xterm) + def run_in_window( + self, + cmd, + wait_for=False, + background=False, + name=None, + title=None, + forcex=False, + new_window=False, + tmux_target=None, + ): + """ + Run a command in a new window (TMUX, Screen or XTerm). + + Args: + wait_for: True to wait for exit from command or `str` as channel neme to signal on exit, otherwise False + background: Do not change focus to new window. + title: Title for new pane (tmux) or window (xterm). + name: Name of the new window (tmux) + forcex: Force use of X11. + new_window: Open new window (instead of pane) in TMUX + tmux_target: Target for tmux pane. + + Returns: + the pane/window identifier from TMUX (depends on `new_window`) + """ + + channel = None + if is_string(wait_for): + channel = wait_for + elif wait_for is True: + channel = "{}-wait-{}".format(os.getpid(), Commander.tmux_wait_gen) + Commander.tmux_wait_gen += 1 + + sudo_path = self.get_exec_path(["sudo"]) + nscmd = sudo_path + " " + self.pre_cmd_str + cmd + if "TMUX" in os.environ and not forcex: + cmd = [self.get_exec_path("tmux")] + if new_window: + cmd.append("new-window") + cmd.append("-P") + if name: + cmd.append("-n") + cmd.append(name) + if tmux_target: + cmd.append("-t") + cmd.append(tmux_target) + else: + cmd.append("split-window") + cmd.append("-P") + cmd.append("-h") + if not tmux_target: + tmux_target = os.getenv("TMUX_PANE", "") + if background: + cmd.append("-d") + if tmux_target: + cmd.append("-t") + cmd.append(tmux_target) + if title: + nscmd = "printf '\033]2;{}\033\\'; {}".format(title, nscmd) + if channel: + nscmd = 'trap "tmux wait -S {}; exit 0" EXIT; {}'.format(channel, nscmd) + cmd.append(nscmd) + elif "STY" in os.environ and not forcex: + # wait for not supported in screen for now + channel = None + cmd = [self.get_exec_path("screen")] + if title: + cmd.append("-t") + cmd.append(title) + if not os.path.exists( + "/run/screen/S-{}/{}".format(os.environ["USER"], os.environ["STY"]) + ): + cmd = ["sudo", "-u", os.environ["SUDO_USER"]] + cmd + cmd.extend(nscmd.split(" ")) + elif "DISPLAY" in os.environ: + # We need it broken up for xterm + user_cmd = cmd + cmd = [self.get_exec_path("xterm")] + if "SUDO_USER" in os.environ: + cmd = [self.get_exec_path("sudo"), "-u", os.environ["SUDO_USER"]] + cmd + if title: + cmd.append("-T") + cmd.append(title) + cmd.append("-e") + cmd.append(sudo_path) + cmd.extend(self.pre_cmd) + cmd.extend(["bash", "-c", user_cmd]) + # if channel: + # return self.cmd_raises(cmd, skip_pre_cmd=True) + # else: + p = self.popen( + cmd, + skip_pre_cmd=True, + stdin=None, + shell=False, + ) + time_mod.sleep(2) + if p.poll() is not None: + self.logger.error("%s: Failed to launch xterm: %s", self, comm_error(p)) + return p + else: + self.logger.error( + "DISPLAY, STY, and TMUX not in environment, can't open window" + ) + raise Exception("Window requestd but TMUX, Screen and X11 not available") + + pane_info = self.cmd_raises(cmd, skip_pre_cmd=True).strip() + + # Re-adjust the layout + if "TMUX" in os.environ: + self.cmd_status( + "tmux select-layout -t {} tiled".format( + pane_info if not tmux_target else tmux_target + ), + skip_pre_cmd=True, + ) + + # Wait here if we weren't handed the channel to wait for + if channel and wait_for is True: + cmd = [self.get_exec_path("tmux"), "wait", channel] + self.cmd_status(cmd, skip_pre_cmd=True) + + return pane_info + + def delete(self): + pass + + +class LinuxNamespace(Commander): + """ + A linux Namespace. + + An object that creates and executes commands in a linux namespace + """ + + def __init__( + self, + name, + net=True, + mount=True, + uts=True, + cgroup=False, + ipc=False, + pid=False, + time=False, + user=False, + set_hostname=True, + private_mounts=None, + logger=None, + ): + """ + Create a new linux namespace. + + Args: + name: Internal name for the namespace. + net: Create network namespace. + mount: Create network namespace. + uts: Create UTS (hostname) namespace. + cgroup: Create cgroup namespace. + ipc: Create IPC namespace. + pid: Create PID namespace, also mounts new /proc. + time: Create time namespace. + user: Create user namespace, also keeps capabilities. + set_hostname: Set the hostname to `name`, uts must also be True. + private_mounts: List of strings of the form + "[/external/path:]/internal/path. If no external path is specified a + tmpfs is mounted on the internal path. Any paths specified are first + passed to `mkdir -p`. + logger: Passed to superclass. + """ + super(LinuxNamespace, self).__init__(name, logger) + + self.logger.debug("%s: Creating", self) + + self.intfs = [] + + nslist = [] + cmd = ["/usr/bin/unshare"] + flags = "-" + self.ifnetns = {} + + if cgroup: + nslist.append("cgroup") + flags += "C" + if ipc: + nslist.append("ipc") + flags += "i" + if mount: + nslist.append("mnt") + flags += "m" + if net: + nslist.append("net") + flags += "n" + if pid: + nslist.append("pid") + flags += "p" + cmd.append("--mount-proc") + if time: + # XXX this filename is probably wrong + nslist.append("time") + flags += "T" + if user: + nslist.append("user") + flags += "U" + cmd.append("--keep-caps") + if uts: + nslist.append("uts") + cmd.append("--uts") + + cmd.append(flags) + cmd.append("/bin/cat") + + # Using cat and a stdin PIPE is nice as it will exit when we do. However, we + # also detach it from the pgid so that signals do not propagate to it. This is + # b/c it would exit early (e.g., ^C) then, at least the main micronet proc which + # has no other processes like frr daemons running, will take the main network + # namespace with it, which will remove the bridges and the veth pair (because + # the bridge side veth is deleted). + self.logger.debug("%s: creating namespace process: %s", self, cmd) + p = subprocess.Popen( + cmd, + stdin=subprocess.PIPE, + stdout=open("/dev/null", "w"), + stderr=open("/dev/null", "w"), + preexec_fn=os.setsid, # detach from pgid so signals don't propogate + shell=False, + ) + self.p = p + self.pid = p.pid + + self.logger.debug("%s: namespace pid: %d", self, self.pid) + + # ----------------------------------------------- + # Now let's wait until unshare completes it's job + # ----------------------------------------------- + timeout = Timeout(30) + while p.poll() is None and not timeout.is_expired(): + for fname in tuple(nslist): + ours = os.readlink("/proc/self/ns/{}".format(fname)) + theirs = os.readlink("/proc/{}/ns/{}".format(self.pid, fname)) + # See if their namespace is different + if ours != theirs: + nslist.remove(fname) + if not nslist: + break + elapsed = int(timeout.elapsed()) + if elapsed <= 3: + time_mod.sleep(0.1) + elif elapsed > 10: + self.logger.warning("%s: unshare taking more than %ss", self, elapsed) + time_mod.sleep(3) + else: + self.logger.info("%s: unshare taking more than %ss", self, elapsed) + time_mod.sleep(1) + assert p.poll() is None, "unshare unexpectedly exited!" + assert not nslist, "unshare never unshared!" + + # Set pre-command based on our namespace proc + self.base_pre_cmd = ["/usr/bin/nsenter", "-a", "-t", str(self.pid)] + if not pid: + self.base_pre_cmd.append("-F") + self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + self.cwd]) + + # Remount sysfs and cgroup to pickup any changes + self.cmd_raises("mount -t sysfs sysfs /sys") + self.cmd_raises( + "mount -o rw,nosuid,nodev,noexec,relatime -t cgroup2 cgroup /sys/fs/cgroup" + ) + + # Set the hostname to the namespace name + if uts and set_hostname: + # Debugging get the root hostname + self.cmd_raises("hostname " + self.name) + nroot = subprocess.check_output("hostname") + if root_hostname != nroot: + result = self.p.poll() + assert root_hostname == nroot, "STATE of namespace process {}".format( + result + ) + + if private_mounts: + if is_string(private_mounts): + private_mounts = [private_mounts] + for m in private_mounts: + s = m.split(":", 1) + if len(s) == 1: + self.tmpfs_mount(s[0]) + else: + self.bind_mount(s[0], s[1]) + + o = self.cmd_legacy("ls -l /proc/{}/ns".format(self.pid)) + self.logger.debug("namespaces:\n %s", o) + + # Doing this here messes up all_protocols ipv6 check + self.cmd_raises("ip link set lo up") + + def __str__(self): + return "LinuxNamespace({})".format(self.name) + + def tmpfs_mount(self, inner): + self.cmd_raises("mkdir -p " + inner) + self.cmd_raises("mount -n -t tmpfs tmpfs " + inner) + + def bind_mount(self, outer, inner): + self.cmd_raises("mkdir -p " + inner) + self.cmd_raises("mount --rbind {} {} ".format(outer, inner)) + + def add_vlan(self, vlanname, linkiface, vlanid): + self.logger.debug("Adding VLAN interface: %s (%s)", vlanname, vlanid) + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + self.cmd_raises( + [ + ip_path, + "link", + "add", + "link", + linkiface, + "name", + vlanname, + "type", + "vlan", + "id", + vlanid, + ] + ) + self.cmd_raises([ip_path, "link", "set", "dev", vlanname, "up"]) + + def add_loop(self, loopname): + self.logger.debug("Adding Linux iface: %s", loopname) + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + self.cmd_raises([ip_path, "link", "add", loopname, "type", "dummy"]) + self.cmd_raises([ip_path, "link", "set", "dev", loopname, "up"]) + + def add_l3vrf(self, vrfname, tableid): + self.logger.debug("Adding Linux VRF: %s", vrfname) + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + self.cmd_raises( + [ip_path, "link", "add", vrfname, "type", "vrf", "table", tableid] + ) + self.cmd_raises([ip_path, "link", "set", "dev", vrfname, "up"]) + + def del_iface(self, iface): + self.logger.debug("Removing Linux Iface: %s", iface) + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + self.cmd_raises([ip_path, "link", "del", iface]) + + def attach_iface_to_l3vrf(self, ifacename, vrfname): + self.logger.debug("Attaching Iface %s to Linux VRF %s", ifacename, vrfname) + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + if vrfname: + self.cmd_raises( + [ip_path, "link", "set", "dev", ifacename, "master", vrfname] + ) + else: + self.cmd_raises([ip_path, "link", "set", "dev", ifacename, "nomaster"]) + + def add_netns(self, ns): + self.logger.debug("Adding network namespace %s", ns) + + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + if os.path.exists("/run/netns/{}".format(ns)): + self.logger.warning("%s: Removing existing nsspace %s", self, ns) + try: + self.delete_netns(ns) + except Exception as ex: + self.logger.warning( + "%s: Couldn't remove existing nsspace %s: %s", + self, + ns, + str(ex), + exc_info=True, + ) + self.cmd_raises([ip_path, "netns", "add", ns]) + + def delete_netns(self, ns): + self.logger.debug("Deleting network namespace %s", ns) + + ip_path = self.get_exec_path("ip") + assert ip_path, "XXX missing ip command!" + self.cmd_raises([ip_path, "netns", "delete", ns]) + + def set_intf_netns(self, intf, ns, up=False): + # In case a user hard-codes 1 thinking it "resets" + ns = str(ns) + if ns == "1": + ns = str(self.pid) + + self.logger.debug("Moving interface %s to namespace %s", intf, ns) + + cmd = "ip link set {} netns " + ns + if up: + cmd += " up" + self.intf_ip_cmd(intf, cmd) + if ns == str(self.pid): + # If we are returning then remove from dict + if intf in self.ifnetns: + del self.ifnetns[intf] + else: + self.ifnetns[intf] = ns + + def reset_intf_netns(self, intf): + self.logger.debug("Moving interface %s to default namespace", intf) + self.set_intf_netns(intf, str(self.pid)) + + def intf_ip_cmd(self, intf, cmd): + """Run an ip command for considering an interfaces possible namespace. + + `cmd` - format is run using the interface name on the command + """ + if intf in self.ifnetns: + assert cmd.startswith("ip ") + cmd = "ip -n " + self.ifnetns[intf] + cmd[2:] + self.cmd_raises(cmd.format(intf)) + + def set_cwd(self, cwd): + # Set pre-command based on our namespace proc + self.logger.debug("%s: new CWD %s", self, cwd) + self.set_pre_cmd(self.base_pre_cmd + ["--wd=" + cwd]) + + def register_interface(self, ifname): + if ifname not in self.intfs: + self.intfs.append(ifname) + + def delete(self): + if self.p and self.p.poll() is None: + if sys.version_info[0] >= 3: + try: + self.p.terminate() + self.p.communicate(timeout=10) + except subprocess.TimeoutExpired: + self.p.kill() + self.p.communicate(timeout=2) + else: + self.p.kill() + self.p.communicate() + self.set_pre_cmd(["/bin/false"]) + + +class SharedNamespace(Commander): + """ + Share another namespace. + + An object that executes commands in an existing pid's linux namespace + """ + + def __init__(self, name, pid, logger=None): + """ + Share a linux namespace. + + Args: + name: Internal name for the namespace. + pid: PID of the process to share with. + """ + super(SharedNamespace, self).__init__(name, logger) + + self.logger.debug("%s: Creating", self) + + self.pid = pid + self.intfs = [] + + # Set pre-command based on our namespace proc + self.set_pre_cmd( + ["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + self.cwd] + ) + + def __str__(self): + return "SharedNamespace({})".format(self.name) + + def set_cwd(self, cwd): + # Set pre-command based on our namespace proc + self.logger.debug("%s: new CWD %s", self, cwd) + self.set_pre_cmd(["/usr/bin/nsenter", "-a", "-t", str(self.pid), "--wd=" + cwd]) + + def register_interface(self, ifname): + if ifname not in self.intfs: + self.intfs.append(ifname) + + +class Bridge(SharedNamespace): + """ + A linux bridge. + """ + + next_brid_ord = 0 + + @classmethod + def _get_next_brid(cls): + brid_ord = cls.next_brid_ord + cls.next_brid_ord += 1 + return brid_ord + + def __init__(self, name=None, unet=None, logger=None): + """Create a linux Bridge.""" + + self.unet = unet + self.brid_ord = self._get_next_brid() + if name: + self.brid = name + else: + self.brid = "br{}".format(self.brid_ord) + name = self.brid + + super(Bridge, self).__init__(name, unet.pid, logger) + + self.logger.debug("Bridge: Creating") + + assert len(self.brid) <= 16 # Make sure fits in IFNAMSIZE + self.cmd_raises("ip link delete {} || true".format(self.brid)) + self.cmd_raises("ip link add {} type bridge".format(self.brid)) + self.cmd_raises("ip link set {} up".format(self.brid)) + + self.logger.debug("%s: Created, Running", self) + + def __str__(self): + return "Bridge({})".format(self.brid) + + def delete(self): + """Stop the bridge (i.e., delete the linux resources).""" + + rc, o, e = self.cmd_status("ip link show {}".format(self.brid), warn=False) + if not rc: + rc, o, e = self.cmd_status( + "ip link delete {}".format(self.brid), warn=False + ) + if rc: + self.logger.error( + "%s: error deleting bridge %s: %s", + self, + self.brid, + cmd_error(rc, o, e), + ) + else: + self.logger.debug("%s: Deleted.", self) + + +class Micronet(LinuxNamespace): # pylint: disable=R0205 + """ + Micronet. + """ + + def __init__(self): + """Create a Micronet.""" + + self.hosts = {} + self.switches = {} + self.links = {} + self.macs = {} + self.rmacs = {} + + super(Micronet, self).__init__("micronet", mount=True, net=True, uts=True) + + self.logger.debug("%s: Creating", self) + + def __str__(self): + return "Micronet()" + + def __getitem__(self, key): + if key in self.switches: + return self.switches[key] + return self.hosts[key] + + def add_host(self, name, cls=LinuxNamespace, **kwargs): + """Add a host to micronet.""" + + self.logger.debug("%s: add_host %s", self, name) + + self.hosts[name] = cls(name, **kwargs) + # Create a new mounted FS for tracking nested network namespaces creatd by the + # user with `ip netns add` + self.hosts[name].tmpfs_mount("/run/netns") + + def add_link(self, name1, name2, if1, if2): + """Add a link between switch and host to micronet.""" + isp2p = False + if name1 in self.switches: + assert name2 in self.hosts + elif name2 in self.switches: + assert name1 in self.hosts + name1, name2 = name2, name1 + if1, if2 = if2, if1 + else: + # p2p link + assert name1 in self.hosts + assert name2 in self.hosts + isp2p = True + + lname = "{}:{}-{}:{}".format(name1, if1, name2, if2) + self.logger.debug("%s: add_link %s%s", self, lname, " p2p" if isp2p else "") + self.links[lname] = (name1, if1, name2, if2) + + # And create the veth now. + if isp2p: + lhost, rhost = self.hosts[name1], self.hosts[name2] + lifname = "i1{:x}".format(lhost.pid) + rifname = "i2{:x}".format(rhost.pid) + self.cmd_raises( + "ip link add {} type veth peer name {}".format(lifname, rifname) + ) + + self.cmd_raises("ip link set {} netns {}".format(lifname, lhost.pid)) + lhost.cmd_raises("ip link set {} name {}".format(lifname, if1)) + lhost.cmd_raises("ip link set {} up".format(if1)) + lhost.register_interface(if1) + + self.cmd_raises("ip link set {} netns {}".format(rifname, rhost.pid)) + rhost.cmd_raises("ip link set {} name {}".format(rifname, if2)) + rhost.cmd_raises("ip link set {} up".format(if2)) + rhost.register_interface(if2) + else: + switch = self.switches[name1] + host = self.hosts[name2] + + assert len(if1) <= 16 and len(if2) <= 16 # Make sure fits in IFNAMSIZE + + self.logger.debug("%s: Creating veth pair for link %s", self, lname) + self.cmd_raises( + "ip link add {} type veth peer name {} netns {}".format( + if1, if2, host.pid + ) + ) + self.cmd_raises("ip link set {} netns {}".format(if1, switch.pid)) + switch.register_interface(if1) + host.register_interface(if2) + self.cmd_raises("ip link set {} master {}".format(if1, switch.brid)) + self.cmd_raises("ip link set {} up".format(if1)) + host.cmd_raises("ip link set {} up".format(if2)) + + # Cache the MAC values, and reverse mapping + self.get_mac(name1, if1) + self.get_mac(name2, if2) + + def add_switch(self, name): + """Add a switch to micronet.""" + + self.logger.debug("%s: add_switch %s", self, name) + self.switches[name] = Bridge(name, self) + + def get_mac(self, name, ifname): + if name in self.hosts: + dev = self.hosts[name] + else: + dev = self.switches[name] + + if (name, ifname) not in self.macs: + _, output, _ = dev.cmd_status("ip -o link show " + ifname) + m = re.match(".*link/(loopback|ether) ([0-9a-fA-F:]+) .*", output) + mac = m.group(2) + self.macs[(name, ifname)] = mac + self.rmacs[mac] = (name, ifname) + + return self.macs[(name, ifname)] + + def delete(self): + """Delete the micronet topology.""" + + self.logger.debug("%s: Deleting.", self) + + for lname, (_, _, rname, rif) in self.links.items(): + host = self.hosts[rname] + + self.logger.debug("%s: Deleting veth pair for link %s", self, lname) + + rc, o, e = host.cmd_status("ip link delete {}".format(rif), warn=False) + if rc: + self.logger.error( + "Error deleting veth pair %s: %s", lname, cmd_error(rc, o, e) + ) + + self.links = {} + + for host in self.hosts.values(): + try: + host.delete() + except Exception as error: + self.logger.error( + "%s: error while deleting host %s: %s", self, host, error + ) + + self.hosts = {} + + for switch in self.switches.values(): + try: + switch.delete() + except Exception as error: + self.logger.error( + "%s: error while deleting switch %s: %s", self, switch, error + ) + self.switches = {} + + self.logger.debug("%s: Deleted.", self) + + super(Micronet, self).delete() + + +# --------------------------- +# Root level utility function +# --------------------------- + + +def get_exec_path(binary): + base = Commander("base") + return base.get_exec_path(binary) + + +commander = Commander("micronet") diff --git a/tests/topotests/lib/micronet_cli.py b/tests/topotests/lib/micronet_cli.py new file mode 100644 index 0000000..ef804f6 --- /dev/null +++ b/tests/topotests/lib/micronet_cli.py @@ -0,0 +1,319 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# July 24 2021, Christian Hopps <chopps@labn.net> +# +# 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 +# +import argparse +import logging +import os +import pty +import re +import readline +import select +import socket +import subprocess +import sys +import tempfile +import termios +import tty + + +ENDMARKER = b"\x00END\x00" + + +def lineiter(sock): + s = "" + while True: + sb = sock.recv(256) + if not sb: + return + + s += sb.decode("utf-8") + i = s.find("\n") + if i != -1: + yield s[:i] + s = s[i + 1 :] + + +def spawn(unet, host, cmd): + if sys.stdin.isatty(): + old_tty = termios.tcgetattr(sys.stdin) + tty.setraw(sys.stdin.fileno()) + try: + master_fd, slave_fd = pty.openpty() + + # use os.setsid() make it run in a new process group, or bash job + # control will not be enabled + p = unet.hosts[host].popen( + cmd, + preexec_fn=os.setsid, + stdin=slave_fd, + stdout=slave_fd, + stderr=slave_fd, + universal_newlines=True, + ) + + while p.poll() is None: + r, w, e = select.select([sys.stdin, master_fd], [], [], 0.25) + if sys.stdin in r: + d = os.read(sys.stdin.fileno(), 10240) + os.write(master_fd, d) + elif master_fd in r: + o = os.read(master_fd, 10240) + if o: + os.write(sys.stdout.fileno(), o) + finally: + # restore tty settings back + if sys.stdin.isatty(): + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) + + +def doline(unet, line, writef): + def host_cmd_split(unet, cmd): + csplit = cmd.split() + for i, e in enumerate(csplit): + if e not in unet.hosts: + break + hosts = csplit[:i] + if not hosts: + hosts = sorted(unet.hosts.keys()) + cmd = " ".join(csplit[i:]) + return hosts, cmd + + line = line.strip() + m = re.match(r"^(\S+)(?:\s+(.*))?$", line) + if not m: + return True + + cmd = m.group(1) + oargs = m.group(2) if m.group(2) else "" + if cmd == "q" or cmd == "quit": + return False + if cmd == "hosts": + writef("%% hosts: %s\n" % " ".join(sorted(unet.hosts.keys()))) + elif cmd in ["term", "vtysh", "xterm"]: + args = oargs.split() + if not args or (len(args) == 1 and args[0] == "*"): + args = sorted(unet.hosts.keys()) + hosts = [unet.hosts[x] for x in args if x in unet.hosts] + for host in hosts: + if cmd == "t" or cmd == "term": + host.run_in_window("bash", title="sh-%s" % host) + elif cmd == "v" or cmd == "vtysh": + host.run_in_window("vtysh", title="vt-%s" % host) + elif cmd == "x" or cmd == "xterm": + host.run_in_window("bash", title="sh-%s" % host, forcex=True) + elif cmd == "sh": + hosts, cmd = host_cmd_split(unet, oargs) + for host in hosts: + if sys.stdin.isatty(): + spawn(unet, host, cmd) + else: + if len(hosts) > 1: + writef("------ Host: %s ------\n" % host) + output = unet.hosts[host].cmd_legacy(cmd) + writef(output) + if len(hosts) > 1: + writef("------- End: %s ------\n" % host) + writef("\n") + elif cmd == "h" or cmd == "help": + writef( + """ +Commands: + help :: this help + sh [hosts] <shell-command> :: execute <shell-command> on <host> + term [hosts] :: open shell terminals for hosts + vtysh [hosts] :: open vtysh terminals for hosts + [hosts] <vtysh-command> :: execute vtysh-command on hosts\n\n""" + ) + else: + hosts, cmd = host_cmd_split(unet, line) + for host in hosts: + if len(hosts) > 1: + writef("------ Host: %s ------\n" % host) + output = unet.hosts[host].cmd_legacy('vtysh -c "{}"'.format(cmd)) + writef(output) + if len(hosts) > 1: + writef("------- End: %s ------\n" % host) + writef("\n") + return True + + +def cli_server_setup(unet): + sockdir = tempfile.mkdtemp("-sockdir", "pyt") + sockpath = os.path.join(sockdir, "cli-server.sock") + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(10) + sock.bind(sockpath) + sock.listen(1) + return sock, sockdir, sockpath + except Exception: + unet.cmd_status("rm -rf " + sockdir) + raise + + +def cli_server(unet, server_sock): + sock, addr = server_sock.accept() + + # Go into full non-blocking mode now + sock.settimeout(None) + + for line in lineiter(sock): + line = line.strip() + + def writef(x): + xb = x.encode("utf-8") + sock.send(xb) + + if not doline(unet, line, writef): + return + sock.send(ENDMARKER) + + +def cli_client(sockpath, prompt="unet> "): + sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + sock.settimeout(10) + sock.connect(sockpath) + + # Go into full non-blocking mode now + sock.settimeout(None) + + print("\n--- Micronet CLI Starting ---\n\n") + while True: + if sys.version_info[0] == 2: + line = raw_input(prompt) # pylint: disable=E0602 + else: + line = input(prompt) + if line is None: + return + + # Need to put \n back + line += "\n" + + # Send the CLI command + sock.send(line.encode("utf-8")) + + def bendswith(b, sentinel): + slen = len(sentinel) + return len(b) >= slen and b[-slen:] == sentinel + + # Collect the output + rb = b"" + while not bendswith(rb, ENDMARKER): + lb = sock.recv(4096) + if not lb: + return + rb += lb + + # Remove the marker + rb = rb[: -len(ENDMARKER)] + + # Write the output + sys.stdout.write(rb.decode("utf-8")) + + +def local_cli(unet, outf, prompt="unet> "): + print("\n--- Micronet CLI Starting ---\n\n") + while True: + if sys.version_info[0] == 2: + line = raw_input(prompt) # pylint: disable=E0602 + else: + line = input(prompt) + if line is None: + return + if not doline(unet, line, outf.write): + return + + +def cli( + unet, + histfile=None, + sockpath=None, + force_window=False, + title=None, + prompt=None, + background=True, +): + logger = logging.getLogger("cli-client") + + if prompt is None: + prompt = "unet> " + + if force_window or not sys.stdin.isatty(): + # Run CLI in another window b/c we have no tty. + sock, sockdir, sockpath = cli_server_setup(unet) + + python_path = unet.get_exec_path(["python3", "python"]) + us = os.path.realpath(__file__) + cmd = "{} {}".format(python_path, us) + if histfile: + cmd += " --histfile=" + histfile + if title: + cmd += " --prompt={}".format(title) + cmd += " " + sockpath + + try: + unet.run_in_window(cmd, new_window=True, title=title, background=background) + return cli_server(unet, sock) + finally: + unet.cmd_status("rm -rf " + sockdir) + + if not unet: + logger.debug("client-cli using sockpath %s", sockpath) + + try: + if histfile is None: + histfile = os.path.expanduser("~/.micronet-history.txt") + if not os.path.exists(histfile): + if unet: + unet.cmd("touch " + histfile) + else: + subprocess.run("touch " + histfile) + if histfile: + readline.read_history_file(histfile) + except Exception: + pass + + try: + if sockpath: + cli_client(sockpath, prompt=prompt) + else: + local_cli(unet, sys.stdout, prompt=prompt) + except EOFError: + pass + except Exception as ex: + logger.critical("cli: got exception: %s", ex, exc_info=True) + raise + finally: + readline.write_history_file(histfile) + + +if __name__ == "__main__": + logging.basicConfig(level=logging.DEBUG, filename="/tmp/topotests/cli-client.log") + logger = logging.getLogger("cli-client") + logger.info("Start logging cli-client") + + parser = argparse.ArgumentParser() + parser.add_argument("--histfile", help="file to user for history") + parser.add_argument("--prompt-text", help="prompt string to use") + parser.add_argument("socket", help="path to pair of sockets to communicate over") + args = parser.parse_args() + + prompt = "{}> ".format(args.prompt_text) if args.prompt_text else "unet> " + cli(None, args.histfile, args.socket, prompt=prompt) diff --git a/tests/topotests/lib/micronet_compat.py b/tests/topotests/lib/micronet_compat.py new file mode 100644 index 0000000..a3d3f4c --- /dev/null +++ b/tests/topotests/lib/micronet_compat.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# July 11 2021, Christian Hopps <chopps@labn.net> +# +# 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 +# + +import glob +import logging +import os +import signal +import time + +from lib.micronet import LinuxNamespace, Micronet +from lib.micronet_cli import cli + + +def get_pids_with_env(has_var, has_val=None): + result = {} + for pidenv in glob.iglob("/proc/*/environ"): + pid = pidenv.split("/")[2] + try: + with open(pidenv, "rb") as rfb: + envlist = [ + x.decode("utf-8").split("=", 1) for x in rfb.read().split(b"\0") + ] + envlist = [[x[0], ""] if len(x) == 1 else x for x in envlist] + envdict = dict(envlist) + if has_var not in envdict: + continue + if has_val is None: + result[pid] = envdict + elif envdict[has_var] == str(has_val): + result[pid] = envdict + except Exception: + # E.g., process exited and files are gone + pass + return result + + +def _kill_piddict(pids_by_upid, sig): + for upid, pids in pids_by_upid: + logging.info( + "Sending %s to (%s) of micronet pid %s", sig, ", ".join(pids), upid + ) + for pid in pids: + try: + os.kill(int(pid), sig) + except Exception: + pass + + +def _get_our_pids(): + ourpid = str(os.getpid()) + piddict = get_pids_with_env("MICRONET_PID", ourpid) + pids = [x for x in piddict if x != ourpid] + if pids: + return {ourpid: pids} + return {} + + +def _get_other_pids(): + piddict = get_pids_with_env("MICRONET_PID") + unet_pids = {d["MICRONET_PID"] for d in piddict.values()} + pids_by_upid = {p: set() for p in unet_pids} + for pid, envdict in piddict.items(): + pids_by_upid[envdict["MICRONET_PID"]].add(pid) + # Filter out any child pid sets whos micronet pid is still running + return {x: y for x, y in pids_by_upid.items() if x not in y} + + +def _get_pids_by_upid(ours): + if ours: + return _get_our_pids() + return _get_other_pids() + + +def _cleanup_pids(ours): + pids_by_upid = _get_pids_by_upid(ours).items() + if not pids_by_upid: + return + + _kill_piddict(pids_by_upid, signal.SIGTERM) + + # Give them 5 second to exit cleanly + logging.info("Waiting up to 5s to allow for clean exit of abandon'd pids") + for _ in range(0, 5): + pids_by_upid = _get_pids_by_upid(ours).items() + if not pids_by_upid: + return + time.sleep(1) + + pids_by_upid = _get_pids_by_upid(ours).items() + _kill_piddict(pids_by_upid, signal.SIGKILL) + + +def cleanup_current(): + """Attempt to cleanup preview runs. + + Currently this only scans for old processes. + """ + logging.info("reaping current micronet processes") + _cleanup_pids(True) + + +def cleanup_previous(): + """Attempt to cleanup preview runs. + + Currently this only scans for old processes. + """ + logging.info("reaping past micronet processes") + _cleanup_pids(False) + + +class Node(LinuxNamespace): + """Node (mininet compat).""" + + def __init__(self, name, **kwargs): + """ + Create a Node. + """ + self.params = kwargs + + if "private_mounts" in kwargs: + private_mounts = kwargs["private_mounts"] + else: + private_mounts = kwargs.get("privateDirs", []) + + logger = kwargs.get("logger") + + super(Node, self).__init__(name, logger=logger, private_mounts=private_mounts) + + def cmd(self, cmd, **kwargs): + """Execute a command, joins stdout, stderr, ignores exit status.""" + + return super(Node, self).cmd_legacy(cmd, **kwargs) + + def config(self, lo="up", **params): + """Called by Micronet when topology is built (but not started).""" + # mininet brings up loopback here. + del params + del lo + + def intfNames(self): + return self.intfs + + def terminate(self): + return + + +class Topo(object): # pylint: disable=R0205 + def __init__(self, *args, **kwargs): + raise Exception("Remove Me") + + +class Mininet(Micronet): + """ + Mininet using Micronet. + """ + + g_mnet_inst = None + + def __init__(self, controller=None): + """ + Create a Micronet. + """ + assert not controller + + if Mininet.g_mnet_inst is not None: + Mininet.g_mnet_inst.stop() + Mininet.g_mnet_inst = self + + self.configured_hosts = set() + self.host_params = {} + self.prefix_len = 8 + + # SNMPd used to require this, which was set int he mininet shell + # that all commands executed from. This is goofy default so let's not + # do it if we don't have to. The snmpd.conf files have been updated + # to set permissions to root:frr 770 to make this unneeded in that case + # os.umask(0) + + super(Mininet, self).__init__() + + self.logger.debug("%s: Creating", self) + + def __str__(self): + return "Mininet()" + + def configure_hosts(self): + """ + Configure hosts once the topology has been built. + + This function can be called multiple times if routers are added to the topology + later. + """ + if not self.hosts: + return + + self.logger.debug("Configuring hosts: %s", self.hosts.keys()) + + for name in sorted(self.hosts.keys()): + if name in self.configured_hosts: + continue + + host = self.hosts[name] + first_intf = host.intfs[0] if host.intfs else None + params = self.host_params[name] + + if first_intf and "ip" in params: + ip = params["ip"] + i = ip.find("/") + if i == -1: + plen = self.prefix_len + else: + plen = int(ip[i + 1 :]) + ip = ip[:i] + + host.cmd_raises("ip addr add {}/{} dev {}".format(ip, plen, first_intf)) + + if "defaultRoute" in params: + host.cmd_raises( + "ip route add default {}".format(params["defaultRoute"]) + ) + + host.config() + + self.configured_hosts.add(name) + + def add_host(self, name, cls=Node, **kwargs): + """Add a host to micronet.""" + + self.host_params[name] = kwargs + super(Mininet, self).add_host(name, cls=cls, **kwargs) + + def start(self): + """Start the micronet topology.""" + self.logger.debug("%s: Starting (no-op).", self) + + def stop(self): + """Stop the mininet topology (deletes).""" + self.logger.debug("%s: Stopping (deleting).", self) + + self.delete() + + self.logger.debug("%s: Stopped (deleted).", self) + + if Mininet.g_mnet_inst == self: + Mininet.g_mnet_inst = None + + def cli(self): + cli(self) diff --git a/tests/topotests/lib/ospf.py b/tests/topotests/lib/ospf.py new file mode 100644 index 0000000..e7ea7d3 --- /dev/null +++ b/tests/topotests/lib/ospf.py @@ -0,0 +1,2491 @@ +# +# Copyright (c) 2020 by VMware, Inc. ("VMware") +# Used Copyright (c) 2018 by Network Device Education Foundation, Inc. +# ("NetDEF") in this file. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import ipaddress +import sys +from copy import deepcopy + +# Import common_config to use commomnly used APIs +from lib.common_config import ( + create_common_configurations, + InvalidCLIError, + generate_ips, + retry, + run_frr_cmd, + validate_ip_address, +) +from lib.topolog import logger +from lib.topotest import frr_unicode + +################################ +# Configure procs +################################ + + +def create_router_ospf(tgen, topo=None, input_dict=None, build=False, load_config=True): + """ + API to configure ospf on router. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + * `load_config` : Loading the config to router this is set as True. + + Usage + ----- + input_dict = { + "r1": { + "ospf": { + "router_id": "22.22.22.22", + "area": [{ "id": "0.0.0.0", "type": "nssa"}] + } + } + + result = create_router_ospf(tgen, topo, input_dict) + + Returns + ------- + True or False + """ + logger.debug("Entering lib API: create_router_ospf()") + result = False + + if topo is None: + topo = tgen.json_topo + + if not input_dict: + input_dict = deepcopy(topo) + else: + topo = topo["routers"] + input_dict = deepcopy(input_dict) + + for ospf in ["ospf", "ospf6"]: + config_data_dict = {} + + for router in input_dict.keys(): + if ospf not in input_dict[router]: + logger.debug("Router %s: %s not present in input_dict", router, ospf) + continue + + config_data = __create_ospf_global( + tgen, input_dict, router, build, load_config, ospf + ) + if config_data: + if router not in config_data_dict: + config_data_dict[router] = config_data + else: + config_data_dict[router].extend(config_data) + try: + result = create_common_configurations( + tgen, config_data_dict, ospf, build, load_config + ) + except InvalidCLIError: + logger.error("create_router_ospf (ipv4)", exc_info=True) + result = False + + logger.debug("Exiting lib API: create_router_ospf()") + return result + + +def __create_ospf_global(tgen, input_dict, router, build, load_config, ospf): + """ + Helper API to create ospf global configuration. + + Parameters + ---------- + * `tgen` : Topogen object + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router to be configured. + * `build` : Only for initial setup phase this is set as True. + * `load_config` : Loading the config to router this is set as True. + * `ospf` : either 'ospf' or 'ospf6' + + Usage + ----- + input_dict = { + "routers": { + "r1": { + "links": { + "r3": { + "ipv6": "2013:13::1/64", + "ospf6": { + "hello_interval": 1, + "dead_interval": 4, + "network": "point-to-point" + } + } + }, + "ospf6": { + "router_id": "1.1.1.1", + "neighbors": { + "r3": { + "area": "1.1.1.1" + } + } + } + } + } + + Returns + ------- + list of configuration commands + """ + + config_data = [] + + if ospf not in input_dict[router]: + return config_data + + logger.debug("Entering lib API: __create_ospf_global()") + + ospf_data = input_dict[router][ospf] + del_ospf_action = ospf_data.setdefault("delete", False) + if del_ospf_action: + config_data = ["no router {}".format(ospf)] + return config_data + + cmd = "router {}".format(ospf) + + config_data.append(cmd) + + # router id + router_id = ospf_data.setdefault("router_id", None) + del_router_id = ospf_data.setdefault("del_router_id", False) + if del_router_id: + config_data.append("no {} router-id".format(ospf)) + if router_id: + config_data.append("{} router-id {}".format(ospf, router_id)) + + # log-adjacency-changes + log_adj_changes = ospf_data.setdefault("log_adj_changes", None) + del_log_adj_changes = ospf_data.setdefault("del_log_adj_changes", False) + if del_log_adj_changes: + config_data.append("no log-adjacency-changes detail") + if log_adj_changes: + config_data.append("log-adjacency-changes {}".format(log_adj_changes)) + + # aggregation timer + aggr_timer = ospf_data.setdefault("aggr_timer", None) + del_aggr_timer = ospf_data.setdefault("del_aggr_timer", False) + if del_aggr_timer: + config_data.append("no aggregation timer") + if aggr_timer: + config_data.append("aggregation timer {}".format(aggr_timer)) + + # maximum path information + ecmp_data = ospf_data.setdefault("maximum-paths", {}) + if ecmp_data: + cmd = "maximum-paths {}".format(ecmp_data) + del_action = ospf_data.setdefault("del_max_path", False) + if del_action: + cmd = "no maximum-paths" + config_data.append(cmd) + + # redistribute command + redistribute_data = ospf_data.setdefault("redistribute", {}) + if redistribute_data: + for redistribute in redistribute_data: + if "redist_type" not in redistribute: + logger.debug( + "Router %s: 'redist_type' not present in " "input_dict", router + ) + else: + cmd = "redistribute {}".format(redistribute["redist_type"]) + for red_type in redistribute_data: + if "route_map" in red_type: + cmd = cmd + " route-map {}".format(red_type["route_map"]) + del_action = redistribute.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # area information + area_data = ospf_data.setdefault("area", {}) + if area_data: + for area in area_data: + if "id" not in area: + logger.debug( + "Router %s: 'area id' not present in " "input_dict", router + ) + else: + cmd = "area {}".format(area["id"]) + + if "type" in area: + cmd = cmd + " {}".format(area["type"]) + + del_action = area.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # def route information + def_rte_data = ospf_data.setdefault("default-information", {}) + if def_rte_data: + if "originate" not in def_rte_data: + logger.debug( + "Router %s: 'originate key' not present in " "input_dict", router + ) + else: + cmd = "default-information originate" + + if "always" in def_rte_data: + cmd = cmd + " always" + + if "metric" in def_rte_data: + cmd = cmd + " metric {}".format(def_rte_data["metric"]) + + if "metric-type" in def_rte_data: + cmd = cmd + " metric-type {}".format(def_rte_data["metric-type"]) + + if "route-map" in def_rte_data: + cmd = cmd + " route-map {}".format(def_rte_data["route-map"]) + + del_action = def_rte_data.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # summary information + summary_data = ospf_data.setdefault("summary-address", {}) + if summary_data: + for summary in summary_data: + if "prefix" not in summary: + logger.debug( + "Router %s: 'summary-address' not present in " "input_dict", + router, + ) + else: + cmd = "summary {}/{}".format(summary["prefix"], summary["mask"]) + + _tag = summary.setdefault("tag", None) + if _tag: + cmd = "{} tag {}".format(cmd, _tag) + + _advertise = summary.setdefault("advertise", True) + if not _advertise: + cmd = "{} no-advertise".format(cmd) + + del_action = summary.setdefault("delete", False) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # ospf gr information + gr_data = ospf_data.setdefault("graceful-restart", {}) + if gr_data: + + if "opaque" in gr_data and gr_data["opaque"]: + cmd = "capability opaque" + if gr_data.setdefault("delete", False): + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if "helper enable" in gr_data and not gr_data["helper enable"]: + cmd = "graceful-restart helper enable" + if gr_data.setdefault("delete", False): + cmd = "no {}".format(cmd) + config_data.append(cmd) + elif "helper enable" in gr_data and type(gr_data["helper enable"]) is list: + for rtrs in gr_data["helper enable"]: + cmd = "graceful-restart helper enable {}".format(rtrs) + if gr_data.setdefault("delete", False): + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if "helper" in gr_data: + if type(gr_data["helper"]) is not list: + gr_data["helper"] = list(gr_data["helper"]) + for helper_role in gr_data["helper"]: + cmd = "graceful-restart helper {}".format(helper_role) + if gr_data.setdefault("delete", False): + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if "supported-grace-time" in gr_data: + cmd = "graceful-restart helper supported-grace-time {}".format( + gr_data["supported-grace-time"] + ) + if gr_data.setdefault("delete", False): + cmd = "no {}".format(cmd) + config_data.append(cmd) + + logger.debug("Exiting lib API: create_ospf_global()") + + return config_data + + +def config_ospf_interface( + tgen, topo=None, input_dict=None, build=False, load_config=True +): + """ + API to configure ospf on router. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + * `load_config` : Loading the config to router this is set as True. + + Usage + ----- + r1_ospf_auth = { + "r1": { + "links": { + "r2": { + "ospf": { + "authentication": "message-digest", + "authentication-key": "ospf", + "message-digest-key": "10" + } + } + } + } + } + result = config_ospf_interface(tgen, topo, r1_ospf_auth) + + Returns + ------- + True or False + """ + logger.debug("Enter lib config_ospf_interface") + result = False + + if topo is None: + topo = tgen.json_topo + + if not input_dict: + input_dict = deepcopy(topo) + else: + input_dict = deepcopy(input_dict) + + config_data_dict = {} + + for router in input_dict.keys(): + config_data = [] + for lnk in input_dict[router]["links"].keys(): + if "ospf" not in input_dict[router]["links"][lnk]: + logger.debug( + "Router %s: ospf config is not present in" "input_dict", router + ) + continue + ospf_data = input_dict[router]["links"][lnk]["ospf"] + data_ospf_area = ospf_data.setdefault("area", None) + data_ospf_auth = ospf_data.setdefault("authentication", None) + data_ospf_dr_priority = ospf_data.setdefault("priority", None) + data_ospf_cost = ospf_data.setdefault("cost", None) + data_ospf_mtu = ospf_data.setdefault("mtu_ignore", None) + + try: + intf = topo["routers"][router]["links"][lnk]["interface"] + except KeyError: + intf = topo["switches"][router]["links"][lnk]["interface"] + + # interface + cmd = "interface {}".format(intf) + + config_data.append(cmd) + # interface area config + if data_ospf_area: + cmd = "ip ospf area {}".format(data_ospf_area) + config_data.append(cmd) + + # interface ospf auth + if data_ospf_auth: + if data_ospf_auth == "null": + cmd = "ip ospf authentication null" + elif data_ospf_auth == "message-digest": + cmd = "ip ospf authentication message-digest" + else: + cmd = "ip ospf authentication" + + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if "message-digest-key" in ospf_data: + cmd = "ip ospf message-digest-key {} md5 {}".format( + ospf_data["message-digest-key"], ospf_data["authentication-key"] + ) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if ( + "authentication-key" in ospf_data + and "message-digest-key" not in ospf_data + ): + cmd = "ip ospf authentication-key {}".format( + ospf_data["authentication-key"] + ) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # interface ospf dr priority + if data_ospf_dr_priority: + cmd = "ip ospf priority {}".format(ospf_data["priority"]) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # interface ospf cost + if data_ospf_cost: + cmd = "ip ospf cost {}".format(ospf_data["cost"]) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # interface ospf mtu + if data_ospf_mtu: + cmd = "ip ospf mtu-ignore" + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if build: + return config_data + + if config_data: + config_data_dict[router] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "interface_config", build=build + ) + + logger.debug("Exiting lib API: config_ospf_interface()") + return result + + +def clear_ospf(tgen, router, ospf=None): + """ + This API is to clear ospf neighborship by running + clear ip ospf interface * command, + + Parameters + ---------- + * `tgen`: topogen object + * `router`: device under test + + Usage + ----- + clear_ospf(tgen, "r1") + """ + + logger.debug("Entering lib API: clear_ospf()") + if router not in tgen.routers(): + return False + + rnode = tgen.routers()[router] + # Clearing OSPF + if ospf: + version = "ipv6" + else: + version = "ip" + + cmd = "clear {} ospf interface".format(version) + logger.info("Clearing ospf process on router %s.. using command '%s'", router, cmd) + run_frr_cmd(rnode, cmd) + + logger.debug("Exiting lib API: clear_ospf()") + + +def redistribute_ospf(tgen, topo, dut, route_type, **kwargs): + """ + Redstribution of routes inside ospf. + + Parameters + ---------- + * `tgen`: Topogen object + * `topo` : json file data + * `dut`: device under test + * `route_type`: "static" or "connected" or .... + * `kwargs`: pass extra information (see below) + + Usage + ----- + redistribute_ospf(tgen, topo, "r0", "static", delete=True) + redistribute_ospf(tgen, topo, "r0", "static", route_map="rmap_ipv4") + """ + + ospf_red = {dut: {"ospf": {"redistribute": [{"redist_type": route_type}]}}} + for k, v in kwargs.items(): + ospf_red[dut]["ospf"]["redistribute"][0][k] = v + + result = create_router_ospf(tgen, topo, ospf_red) + assert result is True, "Testcase : Failed \n Error: {}".format(result) + + +################################ +# Verification procs +################################ +@retry(retry_timeout=80) +def verify_ospf_neighbor( + tgen, topo=None, dut=None, input_dict=None, lan=False, expected=True +): + """ + This API is to verify ospf neighborship by running + show ip ospf neighbour command, + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `lan` : verify neighbors in lan topology + * `expected` : expected results from API, by-default True + + Usage + ----- + 1. To check FULL neighbors. + verify_ospf_neighbor(tgen, topo, dut=dut) + + 2. To check neighbors with their roles. + input_dict = { + "r0": { + "ospf": { + "neighbors": { + "r1": { + "state": "Full", + "role": "DR" + }, + "r2": { + "state": "Full", + "role": "DROther" + }, + "r3": { + "state": "Full", + "role": "DROther" + } + } + } + } + } + result = verify_ospf_neighbor(tgen, topo, dut, input_dict, lan=True) + + Returns + ------- + True or False (Error Message) + """ + logger.debug("Entering lib API: verify_ospf_neighbor()") + result = False + if topo is None: + topo = tgen.json_topo + + if input_dict: + for router, rnode in tgen.routers().items(): + if "ospf" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF neighborship on router %s:", router) + show_ospf_json = run_frr_cmd( + rnode, "show ip ospf neighbor all json", isjson=True + ) + + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + ospf_data_list = input_dict[router]["ospf"] + ospf_nbr_list = ospf_data_list["neighbors"] + + for ospf_nbr, nbr_data in ospf_nbr_list.items(): + data_ip = topo["routers"][ospf_nbr]["links"] + data_rid = topo["routers"][ospf_nbr]["ospf"]["router_id"] + if ospf_nbr in data_ip: + nbr_details = nbr_data[ospf_nbr] + elif lan: + for switch in topo["switches"]: + if "ospf" in topo["switches"][switch]["links"][router]: + neighbor_ip = data_ip[switch]["ipv4"].split("/")[0] + else: + continue + else: + neighbor_ip = data_ip[router]["ipv4"].split("/")[0] + + nh_state = None + neighbor_ip = neighbor_ip.lower() + nbr_rid = data_rid + try: + nh_state = show_ospf_json[nbr_rid][0]["state"].split("/")[0] + intf_state = show_ospf_json[nbr_rid][0]["state"].split("/")[1] + except KeyError: + errormsg = "[DUT: {}] OSPF peer {} missing".format(router, nbr_rid) + return errormsg + + nbr_state = nbr_data.setdefault("state", None) + nbr_role = nbr_data.setdefault("role", None) + + if nbr_state: + if nbr_state == nh_state: + logger.info( + "[DUT: {}] OSPF Nbr is {}:{} State {}".format( + router, ospf_nbr, nbr_rid, nh_state + ) + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF is not Converged, neighbor" + " state is {}".format(router, nh_state) + ) + return errormsg + if nbr_role: + if nbr_role == intf_state: + logger.info( + "[DUT: {}] OSPF Nbr is {}: {} Role {}".format( + router, ospf_nbr, nbr_rid, nbr_role + ) + ) + else: + errormsg = ( + "[DUT: {}] OSPF is not Converged with rid" + "{}, role is {}".format(router, nbr_rid, intf_state) + ) + return errormsg + continue + else: + for router, rnode in tgen.routers().items(): + if "ospf" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF neighborship on router %s:", router) + show_ospf_json = run_frr_cmd( + rnode, "show ip ospf neighbor all json", isjson=True + ) + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + ospf_data_list = topo["routers"][router]["ospf"] + ospf_neighbors = ospf_data_list["neighbors"] + total_peer = 0 + total_peer = len(ospf_neighbors.keys()) + no_of_ospf_nbr = 0 + ospf_nbr_list = ospf_data_list["neighbors"] + no_of_peer = 0 + for ospf_nbr, nbr_data in ospf_nbr_list.items(): + if nbr_data: + data_ip = topo["routers"][nbr_data["nbr"]]["links"] + data_rid = topo["routers"][nbr_data["nbr"]]["ospf"]["router_id"] + else: + data_ip = topo["routers"][ospf_nbr]["links"] + data_rid = topo["routers"][ospf_nbr]["ospf"]["router_id"] + if ospf_nbr in data_ip: + nbr_details = nbr_data[ospf_nbr] + elif lan: + for switch in topo["switches"]: + if "ospf" in topo["switches"][switch]["links"][router]: + neighbor_ip = data_ip[switch]["ipv4"].split("/")[0] + else: + continue + else: + neighbor_ip = data_ip[router]["ipv4"].split("/")[0] + + nh_state = None + neighbor_ip = neighbor_ip.lower() + nbr_rid = data_rid + try: + nh_state = show_ospf_json[nbr_rid][0]["state"].split("/")[0] + except KeyError: + errormsg = "[DUT: {}] OSPF peer {} missing,from " "{} ".format( + router, nbr_rid, ospf_nbr + ) + return errormsg + + if nh_state == "Full": + no_of_peer += 1 + + if no_of_peer == total_peer: + logger.info("[DUT: {}] OSPF is Converged".format(router)) + result = True + else: + errormsg = "[DUT: {}] OSPF is not Converged".format(router) + return errormsg + + logger.debug("Exiting API: verify_ospf_neighbor()") + return result + + +################################ +# Verification procs +################################ +@retry(retry_timeout=50) +def verify_ospf6_neighbor(tgen, topo=None, dut=None, input_dict=None, lan=False): + """ + This API is to verify ospf neighborship by running + show ipv6 ospf neighbour command, + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `lan` : verify neighbors in lan topology + + Usage + ----- + 1. To check FULL neighbors. + verify_ospf_neighbor(tgen, topo, dut=dut) + + 2. To check neighbors with their roles. + input_dict = { + "r0": { + "ospf6": { + "neighbors": { + "r1": { + "state": "Full", + "role": "DR" + }, + "r2": { + "state": "Full", + "role": "DROther" + }, + "r3": { + "state": "Full", + "role": "DROther" + } + } + } + } + } + result = verify_ospf6_neighbor(tgen, topo, dut, input_dict, lan=True) + + 3. To check there are no neighbors. + input_dict = { + "r0": { + "ospf6": { + "neighbors": [] + } + } + } + result = verify_ospf6_neighbor(tgen, topo, dut, input_dict) + + Returns + ------- + True or False (Error Message) + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + + if topo is None: + topo = tgen.json_topo + + if input_dict: + for router, rnode in tgen.routers().items(): + if "ospf6" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF neighborship on router %s:", router) + show_ospf_json = run_frr_cmd( + rnode, "show ipv6 ospf neighbor json", isjson=True + ) + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF6 is not running" + return errormsg + + ospf_data_list = input_dict[router]["ospf6"] + ospf_nbr_list = ospf_data_list["neighbors"] + + # Check if looking for no neighbors + if ospf_nbr_list == []: + if show_ospf_json["neighbors"] == []: + logger.info("[DUT: {}] OSPF6 no neighbors found".format(router)) + return True + else: + errormsg = ( + "[DUT: {}] OSPF6 active neighbors found, expected None".format( + router + ) + ) + return errormsg + + for ospf_nbr, nbr_data in ospf_nbr_list.items(): + + try: + data_ip = data_rid = topo["routers"][ospf_nbr]["ospf6"]["router_id"] + except KeyError: + data_ip = data_rid = topo["routers"][nbr_data["nbr"]]["ospf6"][ + "router_id" + ] + + if ospf_nbr in data_ip: + nbr_details = nbr_data[ospf_nbr] + elif lan: + for switch in topo["switches"]: + if "ospf6" in topo["switches"][switch]["links"][router]: + neighbor_ip = data_ip + else: + continue + else: + neighbor_ip = data_ip[router]["ipv6"].split("/")[0] + + nh_state = None + neighbor_ip = neighbor_ip.lower() + nbr_rid = data_rid + get_index_val = dict( + (d["neighborId"], dict(d, index=index)) + for (index, d) in enumerate(show_ospf_json["neighbors"]) + ) + try: + nh_state = get_index_val.get(neighbor_ip)["state"] + intf_state = get_index_val.get(neighbor_ip)["ifState"] + except TypeError: + errormsg = "[DUT: {}] OSPF peer {} missing,from " "{} ".format( + router, nbr_rid, ospf_nbr + ) + return errormsg + + nbr_state = nbr_data.setdefault("state", None) + nbr_role = nbr_data.setdefault("role", None) + + if nbr_state: + if nbr_state == nh_state: + logger.info( + "[DUT: {}] OSPF6 Nbr is {}:{} State {}".format( + router, ospf_nbr, nbr_rid, nh_state + ) + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF6 is not Converged, neighbor" + " state is {} , Expected state is {}".format( + router, nh_state, nbr_state + ) + ) + return errormsg + if nbr_role: + if nbr_role == intf_state: + logger.info( + "[DUT: {}] OSPF6 Nbr is {}: {} Role {}".format( + router, ospf_nbr, nbr_rid, nbr_role + ) + ) + else: + errormsg = ( + "[DUT: {}] OSPF6 is not Converged with rid" + "{}, role is {}, Expected role is {}".format( + router, nbr_rid, intf_state, nbr_role + ) + ) + return errormsg + continue + else: + + for router, rnode in tgen.routers().items(): + if "ospf6" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF6 neighborship on router %s:", router) + show_ospf_json = run_frr_cmd( + rnode, "show ipv6 ospf neighbor json", isjson=True + ) + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF6 is not running" + return errormsg + + ospf_data_list = topo["routers"][router]["ospf6"] + ospf_neighbors = ospf_data_list["neighbors"] + total_peer = 0 + total_peer = len(ospf_neighbors.keys()) + no_of_ospf_nbr = 0 + ospf_nbr_list = ospf_data_list["neighbors"] + no_of_peer = 0 + for ospf_nbr, nbr_data in ospf_nbr_list.items(): + try: + data_ip = data_rid = topo["routers"][ospf_nbr]["ospf6"]["router_id"] + except KeyError: + data_ip = data_rid = topo["routers"][nbr_data["nbr"]]["ospf6"][ + "router_id" + ] + + if ospf_nbr in data_ip: + nbr_details = nbr_data[ospf_nbr] + elif lan: + for switch in topo["switches"]: + if "ospf6" in topo["switches"][switch]["links"][router]: + neighbor_ip = data_ip + else: + continue + else: + neighbor_ip = data_ip + + nh_state = None + neighbor_ip = neighbor_ip.lower() + nbr_rid = data_rid + get_index_val = dict( + (d["neighborId"], dict(d, index=index)) + for (index, d) in enumerate(show_ospf_json["neighbors"]) + ) + try: + nh_state = get_index_val.get(neighbor_ip)["state"] + intf_state = get_index_val.get(neighbor_ip)["ifState"] + except TypeError: + errormsg = "[DUT: {}] OSPF peer {} missing,from " "{} ".format( + router, nbr_rid, ospf_nbr + ) + return errormsg + + if nh_state == "Full": + no_of_peer += 1 + + if no_of_peer == total_peer: + logger.info("[DUT: {}] OSPF6 is Converged".format(router)) + result = True + else: + errormsg = "[DUT: {}] OSPF6 is not Converged".format(router) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=40) +def verify_ospf_rib( + tgen, dut, input_dict, next_hop=None, tag=None, metric=None, fib=None, expected=True +): + """ + This API is to verify ospf routes by running + show ip ospf route command. + + Parameters + ---------- + * `tgen` : Topogen object + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `next_hop` : next to be verified + * `tag` : tag to be verified + * `metric` : metric to be verified + * `fib` : True if the route is installed in FIB. + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "r1": { + "static_routes": [ + { + "network": ip_net, + "no_of_ip": 1, + "routeType": "N" + } + ] + } + } + + result = verify_ospf_rib(tgen, dut, input_dict,next_hop=nh) + + Returns + ------- + True or False (Error Message) + """ + + logger.info("Entering lib API: verify_ospf_rib()") + result = False + router_list = tgen.routers() + additional_nexthops_in_required_nhs = [] + found_hops = [] + for routerInput in input_dict.keys(): + for router, rnode in router_list.items(): + if router != dut: + continue + + logger.info("Checking router %s RIB:", router) + + # Verifying RIB routes + command = "show ip ospf route" + + found_routes = [] + missing_routes = [] + + if ( + "static_routes" in input_dict[routerInput] + or "prefix" in input_dict[routerInput] + ): + if "prefix" in input_dict[routerInput]: + static_routes = input_dict[routerInput]["prefix"] + else: + static_routes = input_dict[routerInput]["static_routes"] + + for static_route in static_routes: + cmd = "{}".format(command) + + cmd = "{} json".format(cmd) + + ospf_rib_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Verifying output dictionary ospf_rib_json is not empty + if bool(ospf_rib_json) is False: + errormsg = ( + "[DUT: {}] No routes found in OSPF route " + "table".format(router) + ) + return errormsg + + network = static_route["network"] + no_of_ip = static_route.setdefault("no_of_ip", 1) + _tag = static_route.setdefault("tag", None) + _rtype = static_route.setdefault("routeType", None) + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_ip) + st_found = False + nh_found = False + + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt))) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != "ipv4": + continue + + if st_rt in ospf_rib_json: + st_found = True + found_routes.append(st_rt) + + if fib and next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + for mnh in range(0, len(ospf_rib_json[st_rt])): + if ( + "fib" + in ospf_rib_json[st_rt][mnh]["nexthops"][0] + ): + found_hops.append( + [ + rib_r["ip"] + for rib_r in ospf_rib_json[st_rt][mnh][ + "nexthops" + ] + ] + ) + + if found_hops[0]: + missing_list_of_nexthops = set( + found_hops[0] + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops[0]) + + if additional_nexthops_in_required_nhs: + logger.info( + "Nexthop " + "%s is not active for route %s in " + "RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is not active" + " for route {} in RIB of router" + " {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + + elif next_hop and fib is None: + if type(next_hop) is not list: + next_hop = [next_hop] + found_hops = [ + rib_r["ip"] + for rib_r in ospf_rib_json[st_rt]["nexthops"] + ] + + if found_hops: + missing_list_of_nexthops = set( + found_hops + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops) + + if additional_nexthops_in_required_nhs: + logger.info( + "Missing nexthop %s for route" + " %s in RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is Missing for " + "route {} in RIB of router {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + if _rtype: + if "routeType" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: routeType missing" + " for route {} in OSPF RIB \n".format( + dut, st_rt + ) + ) + return errormsg + elif _rtype != ospf_rib_json[st_rt]["routeType"]: + errormsg = ( + "[DUT: {}]: routeType mismatch" + " for route {} in OSPF RIB \n".format( + dut, st_rt + ) + ) + return errormsg + else: + logger.info( + "[DUT: {}]: Found routeType {}" + " for route {}".format(dut, _rtype, st_rt) + ) + if tag: + if "tag" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: tag is not" + " present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if _tag != ospf_rib_json[st_rt]["tag"]: + errormsg = ( + "[DUT: {}]: tag value {}" + " is not matched for" + " route {} in RIB \n".format( + dut, + _tag, + st_rt, + ) + ) + return errormsg + + if metric is not None: + if "type2cost" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: metric is" + " not present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if metric != ospf_rib_json[st_rt]["type2cost"]: + errormsg = ( + "[DUT: {}]: metric value " + "{} is not matched for " + "route {} in RIB \n".format( + dut, + metric, + st_rt, + ) + ) + return errormsg + + else: + missing_routes.append(st_rt) + + if nh_found: + logger.info( + "[DUT: {}]: Found next_hop {} for all OSPF" + " routes in RIB".format(router, next_hop) + ) + + if len(missing_routes) > 0: + errormsg = "[DUT: {}]: Missing route in RIB, " "routes: {}".format( + dut, missing_routes + ) + return errormsg + + if found_routes: + logger.info( + "[DUT: %s]: Verified routes in RIB, found" " routes are: %s\n", + dut, + found_routes, + ) + result = True + + logger.info("Exiting lib API: verify_ospf_rib()") + return result + + +@retry(retry_timeout=20) +def verify_ospf_interface( + tgen, topo=None, dut=None, lan=False, input_dict=None, expected=True +): + """ + This API is to verify ospf routes by running + show ip ospf interface command. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : topology descriptions + * `dut`: device under test + * `lan`: if set to true this interface belongs to LAN. + * `input_dict` : Input dict data, required when configuring from testcase + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict= { + 'r0': { + 'links':{ + 's1': { + 'ospf':{ + 'priority':98, + 'timerDeadSecs': 4, + 'area': '0.0.0.3', + 'mcastMemberOspfDesignatedRouters': True, + 'mcastMemberOspfAllRouters': True, + 'ospfEnabled': True, + + } + } + } + } + } + result = verify_ospf_interface(tgen, topo, dut=dut, input_dict=input_dict) + + Returns + ------- + True or False (Error Message) + """ + + logger.debug("Entering lib API: verify_ospf_interface()") + result = False + if topo is None: + topo = tgen.json_topo + + for router, rnode in tgen.routers().items(): + if "ospf" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF interface on router %s:", router) + show_ospf_json = run_frr_cmd(rnode, "show ip ospf interface json", isjson=True) + + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + # To find neighbor ip type + ospf_intf_data = input_dict[router]["links"] + for ospf_intf, intf_data in ospf_intf_data.items(): + intf = topo["routers"][router]["links"][ospf_intf]["interface"] + if intf in show_ospf_json["interfaces"]: + for intf_attribute in intf_data["ospf"]: + if ( + intf_data["ospf"][intf_attribute] + == show_ospf_json["interfaces"][intf][intf_attribute] + ): + logger.info( + "[DUT: %s] OSPF interface %s: %s is %s", + router, + intf, + intf_attribute, + intf_data["ospf"][intf_attribute], + ) + else: + errormsg = "[DUT: {}] OSPF interface {}: {} is {}, \ + Expected is {}".format( + router, + intf, + intf_attribute, + intf_data["ospf"][intf_attribute], + show_ospf_json["interfaces"][intf][intf_attribute], + ) + return errormsg + result = True + logger.debug("Exiting API: verify_ospf_interface()") + return result + + +@retry(retry_timeout=20) +def verify_ospf_database(tgen, topo, dut, input_dict, expected=True): + """ + This API is to verify ospf lsa's by running + show ip ospf database command. + + Parameters + ---------- + * `tgen` : Topogen object + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `topo` : next to be verified + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "areas": { + "0.0.0.0": { + "Router Link States": { + "100.1.1.0-100.1.1.0": { + "LSID": "100.1.1.0", + "Advertised router": "100.1.1.0", + "LSA Age": 130, + "Sequence Number": "80000006", + "Checksum": "a703", + "Router links": 3 + } + }, + "Net Link States": { + "10.0.0.2-100.1.1.1": { + "LSID": "10.0.0.2", + "Advertised router": "100.1.1.1", + "LSA Age": 137, + "Sequence Number": "80000001", + "Checksum": "9583" + } + }, + }, + } + } + result = verify_ospf_database(tgen, topo, dut, input_dict) + + Returns + ------- + True or False (Error Message) + """ + + result = False + router = dut + logger.debug("Entering lib API: verify_ospf_database()") + + if "ospf" not in topo["routers"][dut]: + errormsg = "[DUT: {}] OSPF is not configured on the router.".format(dut) + return errormsg + + rnode = tgen.routers()[dut] + + logger.info("Verifying OSPF interface on router %s:", dut) + show_ospf_json = run_frr_cmd(rnode, "show ip ospf database json", isjson=True) + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + # for inter and inter lsa's + ospf_db_data = input_dict.setdefault("areas", None) + ospf_external_lsa = input_dict.setdefault("AS External Link States", None) + if ospf_db_data: + for ospf_area, area_lsa in ospf_db_data.items(): + if ospf_area in show_ospf_json["areas"]: + if "Router Link States" in area_lsa: + for lsa in area_lsa["Router Link States"]: + if ( + lsa + in show_ospf_json["areas"][ospf_area]["Router Link States"] + ): + logger.info( + "[DUT: %s] OSPF LSDB area %s:Router " "LSA %s", + router, + ospf_area, + lsa, + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Router LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + if "Net Link States" in area_lsa: + for lsa in area_lsa["Net Link States"]: + if lsa in show_ospf_json["areas"][ospf_area]["Net Link States"]: + logger.info( + "[DUT: %s] OSPF LSDB area %s:Network " "LSA %s", + router, + ospf_area, + lsa, + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Network LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + if "Summary Link States" in area_lsa: + for lsa in area_lsa["Summary Link States"]: + if ( + lsa + in show_ospf_json["areas"][ospf_area]["Summary Link States"] + ): + logger.info( + "[DUT: %s] OSPF LSDB area %s:Summary " "LSA %s", + router, + ospf_area, + lsa, + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Summary LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + if "ASBR-Summary Link States" in area_lsa: + for lsa in area_lsa["ASBR-Summary Link States"]: + if ( + lsa + in show_ospf_json["areas"][ospf_area][ + "ASBR-Summary Link States" + ] + ): + logger.info( + "[DUT: %s] OSPF LSDB area %s:ASBR Summary " "LSA %s", + router, + ospf_area, + lsa, + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " ASBR Summary LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + if ospf_external_lsa: + for ospf_ext_lsa, ext_lsa_data in ospf_external_lsa.items(): + if ospf_ext_lsa in show_ospf_json["AS External Link States"]: + logger.info( + "[DUT: %s] OSPF LSDB:External LSA %s", router, ospf_ext_lsa + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB : expected" + " External LSA is {}".format(router, ospf_ext_lsa) + ) + return errormsg + + logger.debug("Exiting API: verify_ospf_database()") + return result + + +@retry(retry_timeout=20) +def verify_ospf_summary(tgen, topo, dut, input_dict, ospf=None, expected=True): + """ + This API is to verify ospf routes by running + show ip ospf interface command. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : topology descriptions + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + + Usage + ----- + input_dict = { + "11.0.0.0/8": { + "Summary address": "11.0.0.0/8", + "Metric-type": "E2", + "Metric": 20, + "Tag": 0, + "External route count": 5 + } + } + result = verify_ospf_summary(tgen, topo, dut, input_dict) + + Returns + ------- + True or False (Error Message) + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + router = dut + + logger.info("Verifying OSPF summary on router %s:", router) + + rnode = tgen.routers()[dut] + + if ospf: + if "ospf6" not in topo["routers"][dut]: + errormsg = "[DUT: {}] OSPF6 is not configured on the router.".format(router) + return errormsg + + show_ospf_json = run_frr_cmd( + rnode, "show ipv6 ospf summary detail json", isjson=True + ) + else: + if "ospf" not in topo["routers"][dut]: + errormsg = "[DUT: {}] OSPF is not configured on the router.".format(router) + return errormsg + + show_ospf_json = run_frr_cmd( + rnode, "show ip ospf summary detail json", isjson=True + ) + + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + # To find neighbor ip type + ospf_summary_data = input_dict + + if ospf: + show_ospf_json = show_ospf_json["default"] + + for ospf_summ, summ_data in ospf_summary_data.items(): + if ospf_summ not in show_ospf_json: + continue + summary = ospf_summary_data[ospf_summ]["Summary address"] + + if summary in show_ospf_json: + for summ in summ_data: + if summ_data[summ] == show_ospf_json[summary][summ]: + logger.info( + "[DUT: %s] OSPF summary %s:%s is %s", + router, + summary, + summ, + summ_data[summ], + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF summary {} : {} is {}, " + "Expected is {}".format( + router, + summary, + summ, + show_ospf_json[summary][summ], + summ_data[summ], + ) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=30) +def verify_ospf6_rib( + tgen, dut, input_dict, next_hop=None, tag=None, metric=None, fib=None +): + """ + This API is to verify ospf routes by running + show ip ospf route command. + + Parameters + ---------- + * `tgen` : Topogen object + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `next_hop` : next to be verified + * `tag` : tag to be verified + * `metric` : metric to be verified + * `fib` : True if the route is installed in FIB. + + Usage + ----- + input_dict = { + "r1": { + "static_routes": [ + { + "network": ip_net, + "no_of_ip": 1, + "routeType": "N" + } + ] + } + } + + result = verify_ospf6_rib(tgen, dut, input_dict,next_hop=nh) + + Returns + ------- + True or False (Error Message) + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + router_list = tgen.routers() + additional_nexthops_in_required_nhs = [] + found_hops = [] + for routerInput in input_dict.keys(): + for router, rnode in router_list.items(): + if router != dut: + continue + + logger.info("Checking router %s RIB:", router) + + # Verifying RIB routes + command = "show ipv6 ospf route detail" + + found_routes = [] + missing_routes = [] + + if ( + "static_routes" in input_dict[routerInput] + or "prefix" in input_dict[routerInput] + ): + if "prefix" in input_dict[routerInput]: + static_routes = input_dict[routerInput]["prefix"] + else: + static_routes = input_dict[routerInput]["static_routes"] + + for static_route in static_routes: + cmd = "{}".format(command) + + cmd = "{} json".format(cmd) + + ospf_rib_json = run_frr_cmd(rnode, cmd, isjson=True) + + # Fix for PR 2644182 + try: + ospf_rib_json = ospf_rib_json["routes"] + except KeyError: + pass + + # Verifying output dictionary ospf_rib_json is not empty + if bool(ospf_rib_json) is False: + errormsg = ( + "[DUT: {}] No routes found in OSPF6 route " + "table".format(router) + ) + return errormsg + + network = static_route["network"] + no_of_ip = static_route.setdefault("no_of_ip", 1) + _tag = static_route.setdefault("tag", None) + _rtype = static_route.setdefault("routeType", None) + + # Generating IPs for verification + ip_list = generate_ips(network, no_of_ip) + if len(ip_list) == 1: + ip_list = [network] + st_found = False + nh_found = False + for st_rt in ip_list: + st_rt = str(ipaddress.ip_network(frr_unicode(st_rt))) + + _addr_type = validate_ip_address(st_rt) + if _addr_type != "ipv6": + continue + + if st_rt in ospf_rib_json: + + st_found = True + found_routes.append(st_rt) + + if fib and next_hop: + if type(next_hop) is not list: + next_hop = [next_hop] + + for mnh in range(0, len(ospf_rib_json[st_rt])): + if ( + "fib" + in ospf_rib_json[st_rt][mnh]["nextHops"][0] + ): + found_hops.append( + [ + rib_r["ip"] + for rib_r in ospf_rib_json[st_rt][mnh][ + "nextHops" + ] + ] + ) + + if found_hops[0]: + missing_list_of_nexthops = set( + found_hops[0] + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops[0]) + + if additional_nexthops_in_required_nhs: + logger.info( + "Nexthop " + "%s is not active for route %s in " + "RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is not active" + " for route {} in RIB of router" + " {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + + elif next_hop and fib is None: + if type(next_hop) is not list: + next_hop = [next_hop] + found_hops = [ + rib_r["nextHop"] + for rib_r in ospf_rib_json[st_rt]["nextHops"] + ] + + if found_hops: + missing_list_of_nexthops = set( + found_hops + ).difference(next_hop) + additional_nexthops_in_required_nhs = set( + next_hop + ).difference(found_hops) + if additional_nexthops_in_required_nhs: + logger.info( + "Missing nexthop %s for route" + " %s in RIB of router %s\n", + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + errormsg = ( + "Nexthop {} is Missing for " + "route {} in RIB of router {}\n".format( + additional_nexthops_in_required_nhs, + st_rt, + dut, + ) + ) + return errormsg + else: + nh_found = True + if _rtype: + if "destinationType" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: destinationType missing" + "for route {} in OSPF RIB \n".format(dut, st_rt) + ) + return errormsg + elif _rtype != ospf_rib_json[st_rt]["destinationType"]: + errormsg = ( + "[DUT: {}]: destinationType mismatch" + "for route {} in OSPF RIB \n".format(dut, st_rt) + ) + return errormsg + else: + logger.info( + "DUT: {}]: Found destinationType {}" + "for route {}".format(dut, _rtype, st_rt) + ) + if tag: + if "tag" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: tag is not" + " present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if _tag != ospf_rib_json[st_rt]["tag"]: + errormsg = ( + "[DUT: {}]: tag value {}" + " is not matched for" + " route {} in RIB \n".format( + dut, + _tag, + st_rt, + ) + ) + return errormsg + + if metric is not None: + if "metricCostE2" not in ospf_rib_json[st_rt]: + errormsg = ( + "[DUT: {}]: metric is" + " not present for" + " route {} in RIB \n".format(dut, st_rt) + ) + return errormsg + + if metric != ospf_rib_json[st_rt]["metricCostE2"]: + errormsg = ( + "[DUT: {}]: metric value " + "{} is not matched for " + "route {} in RIB \n".format( + dut, + metric, + st_rt, + ) + ) + return errormsg + + else: + missing_routes.append(st_rt) + + if nh_found: + logger.info( + "[DUT: {}]: Found next_hop {} for all OSPF" + " routes in RIB".format(router, next_hop) + ) + + if len(missing_routes) > 0: + errormsg = "[DUT: {}]: Missing route in RIB, " "routes: {}".format( + dut, missing_routes + ) + return errormsg + + if found_routes: + logger.info( + "[DUT: %s]: Verified routes in RIB, found" " routes are: %s\n", + dut, + found_routes, + ) + result = True + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=6) +def verify_ospf6_interface(tgen, topo=None, dut=None, lan=False, input_dict=None): + """ + This API is to verify ospf routes by running + show ip ospf interface command. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : topology descriptions + * `dut`: device under test + * `lan`: if set to true this interface belongs to LAN. + * `input_dict` : Input dict data, required when configuring from testcase + + Usage + ----- + input_dict= { + 'r0': { + 'links':{ + 's1': { + 'ospf6':{ + 'priority':98, + 'timerDeadSecs': 4, + 'area': '0.0.0.3', + 'mcastMemberOspfDesignatedRouters': True, + 'mcastMemberOspfAllRouters': True, + 'ospfEnabled': True, + + } + } + } + } + } + result = verify_ospf_interface(tgen, topo, dut=dut, input_dict=input_dict) + + Returns + ------- + True or False (Error Message) + """ + + logger.debug("Entering lib API: verify_ospf6_interface") + result = False + + if topo is None: + topo = tgen.json_topo + + for router, rnode in tgen.routers().items(): + if "ospf6" not in topo["routers"][router]: + continue + + if dut is not None and dut != router: + continue + + logger.info("Verifying OSPF interface on router %s:", router) + show_ospf_json = run_frr_cmd( + rnode, "show ipv6 ospf interface json", isjson=True + ) + + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF6 is not running" + return errormsg + + # To find neighbor ip type + ospf_intf_data = input_dict[router]["links"] + for ospf_intf, intf_data in ospf_intf_data.items(): + intf = topo["routers"][router]["links"][ospf_intf]["interface"] + if intf in show_ospf_json: + for intf_attribute in intf_data["ospf6"]: + if intf_data["ospf6"][intf_attribute] is not list: + if ( + intf_data["ospf6"][intf_attribute] + == show_ospf_json[intf][intf_attribute] + ): + logger.info( + "[DUT: %s] OSPF6 interface %s: %s is %s", + router, + intf, + intf_attribute, + intf_data["ospf6"][intf_attribute], + ) + elif intf_data["ospf6"][intf_attribute] is list: + for addr_list in len(show_ospf_json[intf][intf_attribute]): + if ( + show_ospf_json[intf][intf_attribute][addr_list][ + "address" + ].split("/")[0] + == intf_data["ospf6"]["internetAddress"][0]["address"] + ): + break + else: + errormsg = "[DUT: {}] OSPF6 interface {}: {} is {}, \ + Expected is {}".format( + router, + intf, + intf_attribute, + intf_data["ospf6"][intf_attribute], + intf_data["ospf6"][intf_attribute], + ) + return errormsg + else: + errormsg = "[DUT: {}] OSPF6 interface {}: {} is {}, \ + Expected is {}".format( + router, + intf, + intf_attribute, + intf_data["ospf6"][intf_attribute], + intf_data["ospf6"][intf_attribute], + ) + return errormsg + result = True + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=20) +def verify_ospf6_database(tgen, topo, dut, input_dict): + """ + This API is to verify ospf lsa's by running + show ip ospf database command. + + Parameters + ---------- + * `tgen` : Topogen object + * `dut`: device under test + * `input_dict` : Input dict data, required when configuring from testcase + * `topo` : next to be verified + + Usage + ----- + input_dict = { + "areas": { + "0.0.0.0": { + "routerLinkStates": { + "100.1.1.0-100.1.1.0": { + "LSID": "100.1.1.0", + "Advertised router": "100.1.1.0", + "LSA Age": 130, + "Sequence Number": "80000006", + "Checksum": "a703", + "Router links": 3 + } + }, + "networkLinkStates": { + "10.0.0.2-100.1.1.1": { + "LSID": "10.0.0.2", + "Advertised router": "100.1.1.1", + "LSA Age": 137, + "Sequence Number": "80000001", + "Checksum": "9583" + } + }, + }, + } + } + result = verify_ospf_database(tgen, topo, dut, input_dict) + + Returns + ------- + True or False (Error Message) + """ + + result = False + router = dut + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if "ospf" not in topo["routers"][dut]: + errormsg = "[DUT: {}] OSPF is not configured on the router.".format(dut) + return errormsg + + rnode = tgen.routers()[dut] + + logger.info("Verifying OSPF interface on router %s:", dut) + show_ospf_json = run_frr_cmd(rnode, "show ip ospf database json", isjson=True) + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + return errormsg + + # for inter and inter lsa's + ospf_db_data = input_dict.setdefault("areas", None) + ospf_external_lsa = input_dict.setdefault("asExternalLinkStates", None) + + if ospf_db_data: + for ospf_area, area_lsa in ospf_db_data.items(): + if ospf_area in show_ospf_json["areas"]: + if "routerLinkStates" in area_lsa: + for lsa in area_lsa["routerLinkStates"]: + for rtrlsa in show_ospf_json["areas"][ospf_area][ + "routerLinkStates" + ]: + if ( + lsa["lsaId"] == rtrlsa["lsaId"] + and lsa["advertisedRouter"] + == rtrlsa["advertisedRouter"] + ): + result = True + break + if result: + logger.info( + "[DUT: %s] OSPF LSDB area %s:Router " "LSA %s", + router, + ospf_area, + lsa, + ) + break + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Router LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + + if "networkLinkStates" in area_lsa: + for lsa in area_lsa["networkLinkStates"]: + for netlsa in show_ospf_json["areas"][ospf_area][ + "networkLinkStates" + ]: + if ( + lsa + in show_ospf_json["areas"][ospf_area][ + "networkLinkStates" + ] + ): + if ( + lsa["lsaId"] == netlsa["lsaId"] + and lsa["advertisedRouter"] + == netlsa["advertisedRouter"] + ): + result = True + break + if result: + logger.info( + "[DUT: %s] OSPF LSDB area %s:Network " "LSA %s", + router, + ospf_area, + lsa, + ) + break + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Network LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + + if "summaryLinkStates" in area_lsa: + for lsa in area_lsa["summaryLinkStates"]: + for t3lsa in show_ospf_json["areas"][ospf_area][ + "summaryLinkStates" + ]: + if ( + lsa["lsaId"] == t3lsa["lsaId"] + and lsa["advertisedRouter"] == t3lsa["advertisedRouter"] + ): + result = True + break + if result: + logger.info( + "[DUT: %s] OSPF LSDB area %s:Summary " "LSA %s", + router, + ospf_area, + lsa, + ) + break + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Summary LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + + if "nssaExternalLinkStates" in area_lsa: + for lsa in area_lsa["nssaExternalLinkStates"]: + for t7lsa in show_ospf_json["areas"][ospf_area][ + "nssaExternalLinkStates" + ]: + if ( + lsa["lsaId"] == t7lsa["lsaId"] + and lsa["advertisedRouter"] == t7lsa["advertisedRouter"] + ): + result = True + break + if result: + logger.info( + "[DUT: %s] OSPF LSDB area %s:Type7 " "LSA %s", + router, + ospf_area, + lsa, + ) + break + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " Type7 LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + + if "asbrSummaryLinkStates" in area_lsa: + for lsa in area_lsa["asbrSummaryLinkStates"]: + for t4lsa in show_ospf_json["areas"][ospf_area][ + "asbrSummaryLinkStates" + ]: + if ( + lsa["lsaId"] == t4lsa["lsaId"] + and lsa["advertisedRouter"] == t4lsa["advertisedRouter"] + ): + result = True + break + if result: + logger.info( + "[DUT: %s] OSPF LSDB area %s:ASBR Summary " "LSA %s", + router, + ospf_area, + lsa, + ) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB area {}: expected" + " ASBR Summary LSA is {}".format(router, ospf_area, lsa) + ) + return errormsg + + if "linkLocalOpaqueLsa" in area_lsa: + for lsa in area_lsa["linkLocalOpaqueLsa"]: + try: + for lnklsa in show_ospf_json["areas"][ospf_area][ + "linkLocalOpaqueLsa" + ]: + if ( + lsa["lsaId"] in lnklsa["lsaId"] + and "linkLocalOpaqueLsa" + in show_ospf_json["areas"][ospf_area] + ): + logger.info( + ( + "[DUT: FRR] OSPF LSDB area %s:Opaque-LSA" + "%s", + ospf_area, + lsa, + ) + ) + result = True + else: + errormsg = ( + "[DUT: FRR] OSPF LSDB area: {} " + "expected Opaque-LSA is {}, Found is {}".format( + ospf_area, lsa, show_ospf_json + ) + ) + raise ValueError(errormsg) + return errormsg + except KeyError: + errormsg = "[DUT: FRR] linkLocalOpaqueLsa Not " "present" + return errormsg + + if ospf_external_lsa: + for lsa in ospf_external_lsa: + try: + for t5lsa in show_ospf_json["asExternalLinkStates"]: + if ( + lsa["lsaId"] == t5lsa["lsaId"] + and lsa["advertisedRouter"] == t5lsa["advertisedRouter"] + ): + result = True + break + except KeyError: + result = False + if result: + logger.info("[DUT: %s] OSPF LSDB:External LSA %s", router, lsa) + result = True + else: + errormsg = ( + "[DUT: {}] OSPF LSDB : expected" + " External LSA is {}".format(router, lsa) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def config_ospf6_interface( + tgen, topo=None, input_dict=None, build=False, load_config=True +): + """ + API to configure ospf on router. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + * `load_config` : Loading the config to router this is set as True. + + Usage + ----- + r1_ospf_auth = { + "r1": { + "links": { + "r2": { + "ospf": { + "authentication": 'message-digest', + "authentication-key": "ospf", + "message-digest-key": "10" + } + } + } + } + } + result = config_ospf6_interface(tgen, topo, r1_ospf_auth) + + Returns + ------- + True or False + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + if topo is None: + topo = tgen.json_topo + + if not input_dict: + input_dict = deepcopy(topo) + else: + input_dict = deepcopy(input_dict) + + config_data_dict = {} + + for router in input_dict.keys(): + config_data = [] + for lnk in input_dict[router]["links"].keys(): + if "ospf6" not in input_dict[router]["links"][lnk]: + logger.debug( + "Router %s: ospf6 config is not present in" + "input_dict, passed input_dict %s", + router, + str(input_dict), + ) + continue + ospf_data = input_dict[router]["links"][lnk]["ospf6"] + data_ospf_area = ospf_data.setdefault("area", None) + data_ospf_auth = ospf_data.setdefault("hash-algo", None) + data_ospf_keychain = ospf_data.setdefault("keychain", None) + data_ospf_dr_priority = ospf_data.setdefault("priority", None) + data_ospf_cost = ospf_data.setdefault("cost", None) + data_ospf_mtu = ospf_data.setdefault("mtu_ignore", None) + + try: + intf = topo["routers"][router]["links"][lnk]["interface"] + except KeyError: + intf = topo["switches"][router]["links"][lnk]["interface"] + + # interface + cmd = "interface {}".format(intf) + + config_data.append(cmd) + # interface area config + if data_ospf_area: + cmd = "ipv6 ospf area {}".format(data_ospf_area) + config_data.append(cmd) + + # interface ospf auth + if data_ospf_auth: + cmd = "ipv6 ospf6 authentication" + + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + + if "hash-algo" in ospf_data: + cmd = "{} key-id {} hash-algo {} key {}".format( + cmd, + ospf_data["key-id"], + ospf_data["hash-algo"], + ospf_data["key"], + ) + config_data.append(cmd) + + # interface ospf auth with keychain + if data_ospf_keychain: + cmd = "ipv6 ospf6 authentication" + + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + + if "keychain" in ospf_data: + cmd = "{} keychain {}".format(cmd, ospf_data["keychain"]) + config_data.append(cmd) + + # interface ospf dr priority + if data_ospf_dr_priority: + cmd = "ipv6 ospf priority {}".format(ospf_data["priority"]) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # interface ospf cost + if data_ospf_cost: + cmd = "ipv6 ospf cost {}".format(ospf_data["cost"]) + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # interface ospf mtu + if data_ospf_mtu: + cmd = "ipv6 ospf mtu-ignore" + if "del_action" in ospf_data: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if build: + return config_data + + if config_data: + config_data_dict[router] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "interface_config", build=build + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=20) +def verify_ospf_gr_helper(tgen, topo, dut, input_dict=None): + """ + This API is used to vreify gr helper using command + show ip ospf graceful-restart helper + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : topology descriptions + * 'dut' : router + * 'input_dict' - values to be verified + + Usage: + ------- + input_dict = { + "helperSupport":"Disabled", + "strictLsaCheck":"Enabled", + "restartSupoort":"Planned and Unplanned Restarts", + "supportedGracePeriod":1800 + } + result = verify_ospf_gr_helper(tgen, topo, dut, input_dict) + + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + + if "ospf" not in topo["routers"][dut]: + errormsg = "[DUT: {}] OSPF is not configured on the router.".format(dut) + return errormsg + + rnode = tgen.routers()[dut] + logger.info("Verifying OSPF GR details on router %s:", dut) + show_ospf_json = run_frr_cmd( + rnode, "show ip ospf graceful-restart helper json", isjson=True + ) + + # Verifying output dictionary show_ospf_json is empty or not + if not bool(show_ospf_json): + errormsg = "OSPF is not running" + raise ValueError(errormsg) + return errormsg + + for ospf_gr, gr_data in input_dict.items(): + try: + if input_dict[ospf_gr] == show_ospf_json[ospf_gr]: + logger.info( + "[DUT: FRR] OSPF GR Helper: %s is %s", + ospf_gr, + show_ospf_json[ospf_gr], + ) + result = True + else: + errormsg = ( + "[DUT: FRR] OSPF GR Helper: {} expected is {}, Found " + "is {}".format( + ospf_gr, input_dict[ospf_gr], show_ospf_json[ospf_gr] + ) + ) + raise ValueError(errormsg) + return errormsg + + except KeyError: + errormsg = "[DUT: FRR] OSPF GR Helper: {}".format(ospf_gr) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result diff --git a/tests/topotests/lib/pim.py b/tests/topotests/lib/pim.py new file mode 100644 index 0000000..03ab024 --- /dev/null +++ b/tests/topotests/lib/pim.py @@ -0,0 +1,3979 @@ +# Copyright (c) 2019 by VMware, Inc. ("VMware") +# Used Copyright (c) 2018 by Network Device Education Foundation, Inc. +# ("NetDEF") in this file. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. + +import datetime +import os +import re +import sys +import traceback +import functools +from copy import deepcopy +from time import sleep +from lib import topotest + + +# Import common_config to use commomnly used APIs +from lib.common_config import ( + create_common_configurations, + HostApplicationHelper, + InvalidCLIError, + create_common_configuration, + InvalidCLIError, + retry, + run_frr_cmd, + validate_ip_address, +) +from lib.micronet import get_exec_path +from lib.topolog import logger +from lib.topotest import frr_unicode + +#### +CWD = os.path.dirname(os.path.realpath(__file__)) + + +def create_pim_config(tgen, topo, input_dict=None, build=False, load_config=True): + """ + API to configure pim/pimv6 on router + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from + testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict = { + "r1": { + "pim": { + "join-prune-interval": "5", + "rp": [{ + "rp_addr" : "1.0.3.17". + "keep-alive-timer": "100" + "group_addr_range": ["224.1.1.0/24", "225.1.1.0/24"] + "prefix-list": "pf_list_1" + "delete": True + }] + }, + "pim6": { + "disable" : ["l1-i1-eth1"], + "rp": [{ + "rp_addr" : "2001:db8:f::5:17". + "keep-alive-timer": "100" + "group_addr_range": ["FF00::/8"] + "prefix-list": "pf_list_1" + "delete": True + }] + } + } + } + + + Returns + ------- + True or False + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + if not input_dict: + input_dict = deepcopy(topo) + else: + topo = topo["routers"] + input_dict = deepcopy(input_dict) + + config_data_dict = {} + + for router in input_dict.keys(): + config_data = _enable_disable_pim_config(tgen, topo, input_dict, router, build) + + if config_data: + config_data_dict[router] = config_data + + # Now add RP config to all routers + for router in input_dict.keys(): + if "pim" in input_dict[router] or "pim6" in input_dict[router]: + _add_pim_rp_config(tgen, topo, input_dict, router, build, config_data_dict) + try: + result = create_common_configurations( + tgen, config_data_dict, "pim", build, load_config + ) + except InvalidCLIError: + logger.error("create_pim_config", exc_info=True) + result = False + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def _add_pim_rp_config(tgen, topo, input_dict, router, build, config_data_dict): + """ + Helper API to create pim RP configurations. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured. + * `build` : Only for initial setup phase this is set as True. + * `config_data_dict` : OUT: adds `router` config to dictinary + Returns + ------- + None + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + rp_data = [] + + # PIMv4 + pim_data = None + if "pim" in input_dict[router]: + pim_data = input_dict[router]["pim"] + if "rp" in input_dict[router]["pim"]: + rp_data += pim_data["rp"] + + # PIMv6 + pim6_data = None + if "pim6" in input_dict[router]: + pim6_data = input_dict[router]["pim6"] + if "rp" in input_dict[router]["pim6"]: + rp_data += pim6_data["rp"] + + # Configure this RP on every router. + for dut in tgen.routers(): + # At least one interface must be enabled for PIM on the router + pim_if_enabled = False + pim6_if_enabled = False + for destLink, data in topo[dut]["links"].items(): + if "pim" in data: + pim_if_enabled = True + if "pim6" in data: + pim6_if_enabled = True + if not pim_if_enabled and pim_data: + continue + if not pim6_if_enabled and pim6_data: + continue + + config_data = [] + + if rp_data: + for rp_dict in deepcopy(rp_data): + # ip address of RP + if "rp_addr" not in rp_dict and build: + logger.error( + "Router %s: 'ip address of RP' not " + "present in input_dict/JSON", + router, + ) + + return False + rp_addr = rp_dict.setdefault("rp_addr", None) + if rp_addr: + addr_type = validate_ip_address(rp_addr) + # Keep alive Timer + keep_alive_timer = rp_dict.setdefault("keep_alive_timer", None) + + # Group Address range to cover + if "group_addr_range" not in rp_dict and build: + logger.error( + "Router %s:'Group Address range to cover'" + " not present in input_dict/JSON", + router, + ) + + return False + group_addr_range = rp_dict.setdefault("group_addr_range", None) + + # Group prefix-list filter + prefix_list = rp_dict.setdefault("prefix_list", None) + + # Delete rp config + del_action = rp_dict.setdefault("delete", False) + + if keep_alive_timer: + if addr_type == "ipv4": + cmd = "ip pim rp keep-alive-timer {}".format(keep_alive_timer) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + if addr_type == "ipv6": + cmd = "ipv6 pim rp keep-alive-timer {}".format(keep_alive_timer) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if rp_addr: + if group_addr_range: + if type(group_addr_range) is not list: + group_addr_range = [group_addr_range] + + for grp_addr in group_addr_range: + if addr_type == "ipv4": + cmd = "ip pim rp {} {}".format(rp_addr, grp_addr) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + if addr_type == "ipv6": + cmd = "ipv6 pim rp {} {}".format(rp_addr, grp_addr) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if prefix_list: + if addr_type == "ipv4": + cmd = "ip pim rp {} prefix-list {}".format( + rp_addr, prefix_list + ) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + if addr_type == "ipv6": + cmd = "ipv6 pim rp {} prefix-list {}".format( + rp_addr, prefix_list + ) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if config_data: + if dut not in config_data_dict: + config_data_dict[dut] = config_data + else: + config_data_dict[dut].extend(config_data) + + +def create_igmp_config(tgen, topo, input_dict=None, build=False): + """ + API to configure igmp on router + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from + testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict = { + "r1": { + "igmp": { + "interfaces": { + "r1-r0-eth0" :{ + "igmp":{ + "version": "2", + "delete": True + "query": { + "query-interval" : 100, + "query-max-response-time": 200 + } + } + } + } + } + } + } + + Returns + ------- + True or False + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + if not input_dict: + input_dict = deepcopy(topo) + else: + topo = topo["routers"] + input_dict = deepcopy(input_dict) + + config_data_dict = {} + + for router in input_dict.keys(): + if "igmp" not in input_dict[router]: + logger.debug("Router %s: 'igmp' is not present in " "input_dict", router) + continue + + igmp_data = input_dict[router]["igmp"] + + if "interfaces" in igmp_data: + config_data = [] + intf_data = igmp_data["interfaces"] + + for intf_name in intf_data.keys(): + cmd = "interface {}".format(intf_name) + config_data.append(cmd) + protocol = "igmp" + del_action = intf_data[intf_name]["igmp"].setdefault("delete", False) + del_attr = intf_data[intf_name]["igmp"].setdefault("delete_attr", False) + cmd = "ip igmp" + if del_action: + cmd = "no {}".format(cmd) + if not del_attr: + config_data.append(cmd) + + for attribute, data in intf_data[intf_name]["igmp"].items(): + if attribute == "version": + cmd = "ip {} {} {}".format(protocol, attribute, data) + if del_action: + cmd = "no {}".format(cmd) + if not del_attr: + config_data.append(cmd) + + if attribute == "join": + for group in data: + cmd = "ip {} {} {}".format(protocol, attribute, group) + if del_attr: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if attribute == "query": + for query, value in data.items(): + if query != "delete": + cmd = "ip {} {} {}".format(protocol, query, value) + + if "delete" in intf_data[intf_name][protocol]["query"]: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + if config_data: + config_data_dict[router] = config_data + + try: + result = create_common_configurations( + tgen, config_data_dict, "interface_config", build=build + ) + except InvalidCLIError: + logger.error("create_igmp_config", exc_info=True) + result = False + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def create_mld_config(tgen, topo, input_dict=None, build=False): + """ + API to configure mld for PIMv6 on router + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from + testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict = { + "r1": { + "mld": { + "interfaces": { + "r1-r0-eth0" :{ + "mld":{ + "version": "2", + "delete": True + "query": { + "query-interval" : 100, + "query-max-response-time": 200 + } + } + } + } + } + } + } + + Returns + ------- + True or False + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + result = False + if not input_dict: + input_dict = deepcopy(topo) + else: + topo = topo["routers"] + input_dict = deepcopy(input_dict) + for router in input_dict.keys(): + if "mld" not in input_dict[router]: + logger.debug("Router %s: 'mld' is not present in " "input_dict", router) + continue + + mld_data = input_dict[router]["mld"] + + if "interfaces" in mld_data: + config_data = [] + intf_data = mld_data["interfaces"] + + for intf_name in intf_data.keys(): + cmd = "interface {}".format(intf_name) + config_data.append(cmd) + protocol = "mld" + del_action = intf_data[intf_name]["mld"].setdefault("delete", False) + cmd = "ipv6 mld" + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + del_attr = intf_data[intf_name]["mld"].setdefault("delete_attr", False) + join = intf_data[intf_name]["mld"].setdefault("join", None) + source = intf_data[intf_name]["mld"].setdefault("source", None) + version = intf_data[intf_name]["mld"].setdefault("version", False) + query = intf_data[intf_name]["mld"].setdefault("query", {}) + + if version: + cmd = "ipv6 {} version {}".format(protocol, version) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if source and join: + for group in join: + cmd = "ipv6 {} join {} {}".format(protocol, group, source) + + if del_attr: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + elif join: + for group in join: + cmd = "ipv6 {} join {}".format(protocol, group) + + if del_attr: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + if query: + for _query, value in query.items(): + if _query != "delete": + cmd = "ipv6 {} {} {}".format(protocol, _query, value) + + if "delete" in intf_data[intf_name][protocol]["query"]: + cmd = "no {}".format(cmd) + + config_data.append(cmd) + try: + result = create_common_configuration( + tgen, router, config_data, "interface_config", build=build + ) + except InvalidCLIError: + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def _enable_disable_pim_config(tgen, topo, input_dict, router, build=False): + """ + Helper API to enable or disable pim on interfaces + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `router` : router id to be configured. + * `build` : Only for initial setup phase this is set as True. + + Returns + ------- + list of config + """ + + config_data = [] + + # Enable pim/pim6 on interfaces + for destRouterLink, data in sorted(topo[router]["links"].items()): + if "pim" in data and data["pim"] == "enable": + # Loopback interfaces + if "type" in data and data["type"] == "loopback": + interface_name = destRouterLink + else: + interface_name = data["interface"] + + cmd = "interface {}".format(interface_name) + config_data.append(cmd) + config_data.append("ip pim") + + if "pim6" in data and data["pim6"] == "enable": + # Loopback interfaces + if "type" in data and data["type"] == "loopback": + interface_name = destRouterLink + else: + interface_name = data["interface"] + + cmd = "interface {}".format(interface_name) + config_data.append(cmd) + config_data.append("ipv6 pim") + + # pim global config + if "pim" in input_dict[router]: + pim_data = input_dict[router]["pim"] + del_action = pim_data.setdefault("delete", False) + for t in [ + "join-prune-interval", + "keep-alive-timer", + "register-suppress-time", + ]: + if t in pim_data: + cmd = "ip pim {} {}".format(t, pim_data[t]) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + # pim6 global config + if "pim6" in input_dict[router]: + pim6_data = input_dict[router]["pim6"] + del_action = pim6_data.setdefault("delete", False) + for t in [ + "join-prune-interval", + "keep-alive-timer", + "register-suppress-time", + ]: + if t in pim6_data: + cmd = "ipv6 pim {} {}".format(t, pim6_data[t]) + if del_action: + cmd = "no {}".format(cmd) + config_data.append(cmd) + + return config_data + + +def find_rp_details(tgen, topo): + """ + Find who is RP in topology and returns list of RPs + + Parameters: + ----------- + * `tgen` : Topogen object + * `topo` : json file data + + returns: + -------- + errormsg or True + """ + + rp_details = {} + + router_list = tgen.routers() + topo_data = topo["routers"] + + for router in router_list.keys(): + + if "pim" not in topo_data[router]: + continue + + pim_data = topo_data[router]["pim"] + if "rp" in pim_data: + rp_data = pim_data["rp"] + for rp_dict in rp_data: + # ip address of RP + rp_addr = rp_dict["rp_addr"] + + for link, data in topo["routers"][router]["links"].items(): + if data["ipv4"].split("/")[0] == rp_addr: + rp_details[router] = rp_addr + + return rp_details + + +def configure_pim_force_expire(tgen, topo, input_dict, build=False): + """ + Helper API to create pim configuration. + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `input_dict` : Input dict data, required when configuring from testcase + * `build` : Only for initial setup phase this is set as True. + + Usage + ----- + input_dict ={ + "l1": { + "pim": { + "force_expire":{ + "10.0.10.1": ["255.1.1.1"] + } + } + } + } + + result = create_pim_config(tgen, topo, input_dict) + + Returns + ------- + True or False + """ + + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + config_data_dict = {} + + for dut in input_dict.keys(): + if "pim" not in input_dict[dut]: + continue + + pim_data = input_dict[dut]["pim"] + + config_data = [] + if "force_expire" in pim_data: + force_expire_data = pim_data["force_expire"] + + for source, groups in force_expire_data.items(): + if type(groups) is not list: + groups = [groups] + + for group in groups: + cmd = "ip pim force-expire source {} group {}".format( + source, group + ) + config_data.append(cmd) + + if config_data: + config_data_dict[dut] = config_data + + result = create_common_configurations( + tgen, config_data_dict, "pim", build=build + ) + except InvalidCLIError: + logger.error("configure_pim_force_expire", exc_info=True) + result = False + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +############################################# +# Verification APIs +############################################# +@retry(retry_timeout=12) +def verify_pim_neighbors(tgen, topo, dut=None, iface=None, nbr_ip=None, expected=True): + """ + Verify all PIM neighbors are up and running, config is verified + using "show ip pim neighbor" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : dut info + * `iface` : link for which PIM nbr need to check + * `nbr_ip` : neighbor ip of interface + * `expected` : expected results from API, by-default True + + Usage + ----- + result = verify_pim_neighbors(tgen, topo, dut, iface=ens192, nbr_ip=20.1.1.2) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in tgen.routers(): + if dut is not None and dut != router: + continue + + rnode = tgen.routers()[router] + show_ip_pim_neighbor_json = rnode.vtysh_cmd( + "show ip pim neighbor json", isjson=True + ) + + for destLink, data in topo["routers"][router]["links"].items(): + if iface is not None and iface != data["interface"]: + continue + + if "type" in data and data["type"] == "loopback": + continue + + if "pim" not in data: + continue + + if "pim" in data and data["pim"] == "disable": + continue + + if "pim" in data and data["pim"] == "enable": + local_interface = data["interface"] + + if "-" in destLink: + # Spliting and storing destRouterLink data in tempList + tempList = destLink.split("-") + + # destRouter + destLink = tempList.pop(0) + + # Current Router Link + tempList.insert(0, router) + curRouter = "-".join(tempList) + else: + curRouter = router + if destLink not in topo["routers"]: + continue + data = topo["routers"][destLink]["links"][curRouter] + if "type" in data and data["type"] == "loopback": + continue + + if "pim" not in data: + continue + + logger.info("[DUT: %s]: Verifying PIM neighbor status:", router) + + if "pim" in data and data["pim"] == "enable": + pim_nh_intf_ip = data["ipv4"].split("/")[0] + + # Verifying PIM neighbor + if local_interface in show_ip_pim_neighbor_json: + if show_ip_pim_neighbor_json[local_interface]: + if ( + show_ip_pim_neighbor_json[local_interface][pim_nh_intf_ip][ + "neighbor" + ] + != pim_nh_intf_ip + ): + errormsg = ( + "[DUT %s]: Local interface: %s, PIM" + " neighbor check failed " + "Expected neighbor: %s, Found neighbor:" + " %s" + % ( + router, + local_interface, + pim_nh_intf_ip, + show_ip_pim_neighbor_json[local_interface][ + pim_nh_intf_ip + ]["neighbor"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: Local interface: %s, Found" + " expected PIM neighbor %s", + router, + local_interface, + pim_nh_intf_ip, + ) + else: + errormsg = ( + "[DUT %s]: Local interface: %s, and" + "interface ip: %s is not found in " + "PIM neighbor " % (router, local_interface, pim_nh_intf_ip) + ) + return errormsg + else: + errormsg = ( + "[DUT %s]: Local interface: %s, is not " + "present in PIM neighbor " % (router, local_interface) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=40, diag_pct=0) +def verify_igmp_groups(tgen, dut, interface, group_addresses, expected=True): + """ + Verify IGMP groups are received from an intended interface + by running "show ip igmp groups" command + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `interface`: interface, from which IGMP groups would be received + * `group_addresses`: IGMP group address + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + interface = "r1-r0-eth0" + group_address = "225.1.1.1" + result = verify_igmp_groups(tgen, dut, interface, group_address) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying IGMP groups received:", dut) + show_ip_igmp_json = run_frr_cmd(rnode, "show ip igmp groups json", isjson=True) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + if interface in show_ip_igmp_json: + show_ip_igmp_json = show_ip_igmp_json[interface]["groups"] + else: + errormsg = ( + "[DUT %s]: Verifying IGMP group received" + " from interface %s [FAILED]!! " % (dut, interface) + ) + return errormsg + + found = False + for grp_addr in group_addresses: + for index in show_ip_igmp_json: + if index["group"] == grp_addr: + found = True + break + if found is not True: + errormsg = ( + "[DUT %s]: Verifying IGMP group received" + " from interface %s [FAILED]!! " + " Expected not found: %s" % (dut, interface, grp_addr) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying IGMP group %s received " + "from interface %s [PASSED]!! ", + dut, + grp_addr, + interface, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=60, diag_pct=0) +def verify_upstream_iif( + tgen, + dut, + iif, + src_address, + group_addresses, + joinState=None, + refCount=1, + expected=True, +): + """ + Verify upstream inbound interface is updated correctly + by running "show ip pim upstream" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `iif`: inbound interface + * `src_address`: source address + * `group_addresses`: IGMP group address + * `joinState`: upstream join state + * `refCount`: refCount value + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + iif = "r1-r0-eth0" + src_address = "*" + group_address = "225.1.1.1" + result = verify_upstream_iif(tgen, dut, iif, src_address, group_address, + state, refCount) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info( + "[DUT: %s]: Verifying upstream Inbound Interface" " for IGMP groups received:", + dut, + ) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + if type(iif) is not list: + iif = [iif] + + for grp in group_addresses: + addr_type = validate_ip_address(grp) + + if addr_type == "ipv4": + ip_cmd = "ip" + elif addr_type == "ipv6": + ip_cmd = "ipv6" + + cmd = "show {} pim upstream json".format(ip_cmd) + show_ip_pim_upstream_json = run_frr_cmd(rnode, cmd, isjson=True) + + for grp_addr in group_addresses: + # Verify group address + if grp_addr not in show_ip_pim_upstream_json: + errormsg = "[DUT %s]: Verifying upstream" " for group %s [FAILED]!!" % ( + dut, + grp_addr, + ) + return errormsg + group_addr_json = show_ip_pim_upstream_json[grp_addr] + + # Verify source address + if src_address not in group_addr_json: + errormsg = "[DUT %s]: Verifying upstream" " for (%s,%s) [FAILED]!!" % ( + dut, + src_address, + grp_addr, + ) + return errormsg + + # Verify Inbound Interface + found = False + for in_interface in iif: + if group_addr_json[src_address]["inboundInterface"] == in_interface: + if refCount > 0: + logger.info( + "[DUT %s]: Verifying refCount " + "for (%s,%s) [PASSED]!! " + " Found Expected: %s", + dut, + src_address, + grp_addr, + group_addr_json[src_address]["refCount"], + ) + found = True + if found: + if joinState is None: + if group_addr_json[src_address]["joinState"] != "Joined": + errormsg = ( + "[DUT %s]: Verifying iif " + "(Inbound Interface) for (%s,%s) and" + " joinState :%s [FAILED]!! " + " Expected: %s, Found: %s" + % ( + dut, + src_address, + grp_addr, + group_addr_json[src_address]["joinState"], + in_interface, + group_addr_json[src_address]["inboundInterface"], + ) + ) + return errormsg + + elif group_addr_json[src_address]["joinState"] != joinState: + errormsg = ( + "[DUT %s]: Verifying iif " + "(Inbound Interface) for (%s,%s) and" + " joinState :%s [FAILED]!! " + " Expected: %s, Found: %s" + % ( + dut, + src_address, + grp_addr, + group_addr_json[src_address]["joinState"], + in_interface, + group_addr_json[src_address]["inboundInterface"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying iif(Inbound Interface)" + " for (%s,%s) and joinState is %s [PASSED]!! " + " Found Expected: (%s)", + dut, + src_address, + grp_addr, + group_addr_json[src_address]["joinState"], + group_addr_json[src_address]["inboundInterface"], + ) + if not found: + errormsg = ( + "[DUT %s]: Verifying iif " + "(Inbound Interface) for (%s, %s) " + "[FAILED]!! " + " Expected: %s, Found: %s" + % ( + dut, + src_address, + grp_addr, + in_interface, + group_addr_json[src_address]["inboundInterface"], + ) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=12) +def verify_join_state_and_timer( + tgen, dut, iif, src_address, group_addresses, expected=True +): + """ + Verify join state is updated correctly and join timer is + running with the help of "show ip pim upstream" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `iif`: inbound interface + * `src_address`: source address + * `group_addresses`: IGMP group address + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + iif = "r1-r0-eth0" + group_address = "225.1.1.1" + result = verify_join_state_and_timer(tgen, dut, iif, group_address) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + errormsg = "" + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info( + "[DUT: %s]: Verifying Join state and Join Timer" " for IGMP groups received:", + dut, + ) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp in group_addresses: + addr_type = validate_ip_address(grp) + + if addr_type == "ipv4": + cmd = "show ip pim upstream json" + elif addr_type == "ipv6": + cmd = "show ipv6 pim upstream json" + show_ip_pim_upstream_json = run_frr_cmd(rnode, cmd, isjson=True) + + for grp_addr in group_addresses: + # Verify group address + if grp_addr not in show_ip_pim_upstream_json: + errormsg = "[DUT %s]: Verifying upstream" " for group %s [FAILED]!!" % ( + dut, + grp_addr, + ) + return errormsg + + group_addr_json = show_ip_pim_upstream_json[grp_addr] + + # Verify source address + if src_address not in group_addr_json: + errormsg = "[DUT %s]: Verifying upstream" " for (%s,%s) [FAILED]!!" % ( + dut, + src_address, + grp_addr, + ) + return errormsg + + # Verify join state + joinState = group_addr_json[src_address]["joinState"] + if joinState != "Joined": + error = ( + "[DUT %s]: Verifying join state for" + " (%s,%s) [FAILED]!! " + " Expected: %s, Found: %s" + % (dut, src_address, grp_addr, "Joined", joinState) + ) + errormsg = errormsg + "\n" + str(error) + else: + logger.info( + "[DUT %s]: Verifying join state for" + " (%s,%s) [PASSED]!! " + " Found Expected: %s", + dut, + src_address, + grp_addr, + joinState, + ) + + # Verify join timer + joinTimer = group_addr_json[src_address]["joinTimer"] + if not re.match(r"(\d{2}):(\d{2}):(\d{2})", joinTimer): + error = ( + "[DUT %s]: Verifying join timer for" + " (%s,%s) [FAILED]!! " + " Expected: %s, Found: %s" + ) % ( + dut, + src_address, + grp_addr, + "join timer should be running", + joinTimer, + ) + errormsg = errormsg + "\n" + str(error) + else: + logger.info( + "[DUT %s]: Verifying join timer is running" + " for (%s,%s) [PASSED]!! " + " Found Expected: %s", + dut, + src_address, + grp_addr, + joinTimer, + ) + + if errormsg != "": + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=120, diag_pct=0) +def verify_mroutes( + tgen, + dut, + src_address, + group_addresses, + iif, + oil, + return_uptime=False, + mwait=0, + expected=True, +): + """ + Verify ip mroutes and make sure (*, G)/(S, G) is present in mroutes + by running "show ip/ipv6 mroute" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `src_address`: source address + * `group_addresses`: IGMP group address + * `iif`: Incoming interface + * `oil`: Outgoing interface + * `return_uptime`: If True, return uptime dict, default is False + * `mwait`: Wait time, default is 0 + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + group_address = "225.1.1.1" + result = verify_mroutes(tgen, dut, src_address, group_address) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + if not isinstance(group_addresses, list): + group_addresses = [group_addresses] + + if not isinstance(iif, list) and iif != "none": + iif = [iif] + + if not isinstance(oil, list) and oil != "none": + oil = [oil] + + for grp in group_addresses: + addr_type = validate_ip_address(grp) + + if addr_type == "ipv4": + ip_cmd = "ip" + elif addr_type == "ipv6": + ip_cmd = "ipv6" + + if return_uptime: + logger.info("Sleeping for %s sec..", mwait) + sleep(mwait) + + logger.info("[DUT: %s]: Verifying ip mroutes", dut) + show_ip_mroute_json = run_frr_cmd( + rnode, "show {} mroute json".format(ip_cmd), isjson=True + ) + + if return_uptime: + uptime_dict = {} + + if bool(show_ip_mroute_json) == False: + error_msg = "[DUT %s]: mroutes are not present or flushed out !!" % (dut) + return error_msg + + for grp_addr in group_addresses: + if grp_addr not in show_ip_mroute_json: + errormsg = "[DUT %s]: Verifying (%s, %s) mroute," "[FAILED]!! " % ( + dut, + src_address, + grp_addr, + ) + return errormsg + else: + if return_uptime: + uptime_dict[grp_addr] = {} + + group_addr_json = show_ip_mroute_json[grp_addr] + + if src_address not in group_addr_json: + errormsg = "[DUT %s]: Verifying (%s, %s) mroute," "[FAILED]!! " % ( + dut, + src_address, + grp_addr, + ) + return errormsg + else: + if return_uptime: + uptime_dict[grp_addr][src_address] = {} + + mroutes = group_addr_json[src_address] + + if mroutes["installed"] != 0: + logger.info( + "[DUT %s]: mroute (%s,%s) is installed", dut, src_address, grp_addr + ) + + if "oil" not in mroutes: + if oil == "none" and mroutes["iif"] in iif: + logger.info( + "[DUT %s]: Verifying (%s, %s) mroute," + " [PASSED]!! Found Expected: " + "(iif: %s, oil: %s, installed: (%s,%s))", + dut, + src_address, + grp_addr, + mroutes["iif"], + oil, + src_address, + grp_addr, + ) + else: + errormsg = ( + "[DUT %s]: Verifying (%s, %s) mroute," + " [FAILED]!! " + "Expected: (oil: %s, installed:" + " (%s,%s)) Found: ( oil: none, " + "installed: (%s,%s))" + % ( + dut, + src_address, + grp_addr, + oil, + src_address, + grp_addr, + src_address, + grp_addr, + ) + ) + + return errormsg + + else: + found = False + for route, data in mroutes["oil"].items(): + if route in oil and route != "pimreg": + if ( + data["source"] == src_address + and data["group"] == grp_addr + and data["inboundInterface"] in iif + and data["outboundInterface"] in oil + ): + if return_uptime: + + uptime_dict[grp_addr][src_address] = data["upTime"] + + logger.info( + "[DUT %s]: Verifying (%s, %s)" + " mroute, [PASSED]!! " + "Found Expected: " + "(iif: %s, oil: %s, installed:" + " (%s,%s)", + dut, + src_address, + grp_addr, + data["inboundInterface"], + data["outboundInterface"], + data["source"], + data["group"], + ) + found = True + break + else: + continue + + if not found: + errormsg = ( + "[DUT %s]: Verifying (%s, %s)" + " mroute [FAILED]!! " + "Expected in: (iif: %s, oil: %s," + " installed: (%s,%s)) Found: " + "(iif: %s, oil: %s, " + "installed: (%s,%s))" + % ( + dut, + src_address, + grp_addr, + iif, + oil, + src_address, + grp_addr, + data["inboundInterface"], + data["outboundInterface"], + data["source"], + data["group"], + ) + ) + return errormsg + + else: + errormsg = "[DUT %s]: mroute (%s,%s) is not installed" % ( + dut, + src_address, + grp_addr, + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True if return_uptime == False else uptime_dict + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_rp_info( + tgen, + topo, + dut, + group_addresses, + oif=None, + rp=None, + source=None, + iamrp=None, + expected=True, +): + """ + Verify pim rp info by running "show ip pim rp-info" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: JSON file handler + * `dut`: device under test + * `group_addresses`: IGMP group address + * `oif`: outbound interface name + * `rp`: RP address + * `source`: Source of RP + * `iamrp`: User defined RP + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + result = verify_pim_rp_info(tgen, topo, dut, group_address, + rp=rp, source="BSR") + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + if type(oif) is not list: + oif = [oif] + + for grp in group_addresses: + addr_type = validate_ip_address(grp) + + if addr_type == "ipv4": + ip_cmd = "ip" + elif addr_type == "ipv6": + ip_cmd = "ipv6" + + for grp_addr in group_addresses: + if rp is None: + rp_details = find_rp_details(tgen, topo) + + if dut in rp_details: + iamRP = True + else: + iamRP = False + else: + if addr_type == "ipv4": + show_ip_route_json = run_frr_cmd( + rnode, "show ip route connected json", isjson=True + ) + elif addr_type == "ipv6": + show_ip_route_json = run_frr_cmd( + rnode, "show ipv6 route connected json", isjson=True + ) + for _rp in show_ip_route_json.keys(): + if rp == _rp.split("/")[0]: + iamRP = True + break + else: + iamRP = False + + logger.info("[DUT: %s]: Verifying ip rp info", dut) + cmd = "show {} pim rp-info json".format(ip_cmd) + show_ip_rp_info_json = run_frr_cmd(rnode, cmd, isjson=True) + + if rp not in show_ip_rp_info_json: + errormsg = ( + "[DUT %s]: Verifying rp-info " + "for rp_address %s [FAILED]!! " % (dut, rp) + ) + return errormsg + else: + group_addr_json = show_ip_rp_info_json[rp] + + for rp_json in group_addr_json: + if "rpAddress" not in rp_json: + errormsg = "[DUT %s]: %s key not " "present in rp-info " % ( + dut, + "rpAddress", + ) + return errormsg + + if oif is not None: + found = False + if rp_json["outboundInterface"] not in oif: + errormsg = ( + "[DUT %s]: Verifying OIF " + "for group %s and RP %s [FAILED]!! " + "Expected interfaces: (%s)," + " Found: (%s)" + % (dut, grp_addr, rp, oif, rp_json["outboundInterface"]) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying OIF " + "for group %s and RP %s [PASSED]!! " + "Found Expected: (%s)" + % (dut, grp_addr, rp, rp_json["outboundInterface"]) + ) + + if source is not None: + if rp_json["source"] != source: + errormsg = ( + "[DUT %s]: Verifying SOURCE " + "for group %s and RP %s [FAILED]!! " + "Expected: (%s)," + " Found: (%s)" % (dut, grp_addr, rp, source, rp_json["source"]) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying SOURCE " + "for group %s and RP %s [PASSED]!! " + "Found Expected: (%s)" % (dut, grp_addr, rp, rp_json["source"]) + ) + + if rp_json["group"] == grp_addr and iamrp is not None: + if iamRP: + if rp_json["iAmRP"]: + logger.info( + "[DUT %s]: Verifying group " + "and iAmRP [PASSED]!!" + " Found Expected: (%s, %s:%s)", + dut, + grp_addr, + "iAmRP", + rp_json["iAmRP"], + ) + else: + errormsg = ( + "[DUT %s]: Verifying group" + "%s and iAmRP [FAILED]!! " + "Expected: (iAmRP: %s)," + " Found: (iAmRP: %s)" + % (dut, grp_addr, "true", rp_json["iAmRP"]) + ) + return errormsg + + if not iamRP: + if rp_json["iAmRP"] == False: + logger.info( + "[DUT %s]: Verifying group " + "and iAmNotRP [PASSED]!!" + " Found Expected: (%s, %s:%s)", + dut, + grp_addr, + "iAmRP", + rp_json["iAmRP"], + ) + else: + errormsg = ( + "[DUT %s]: Verifying group" + "%s and iAmRP [FAILED]!! " + "Expected: (iAmRP: %s)," + " Found: (iAmRP: %s)" + % (dut, grp_addr, "false", rp_json["iAmRP"]) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_state( + tgen, + dut, + iif, + oil, + group_addresses, + src_address=None, + installed_fl=None, + expected=True, +): + """ + Verify pim state by running "show ip pim state" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `iif`: inbound interface + * `oil`: outbound interface + * `group_addresses`: IGMP group address + * `src_address`: source address, default = None + * installed_fl` : Installed flag + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + iif = "r1-r3-eth1" + oil = "r1-r0-eth0" + group_address = "225.1.1.1" + result = verify_pim_state(tgen, dut, iif, oil, group_address) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying pim state", dut) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp in group_addresses: + addr_type = validate_ip_address(grp) + + if addr_type == "ipv4": + ip_cmd = "ip" + elif addr_type == "ipv6": + ip_cmd = "ipv6" + + logger.info("[DUT: %s]: Verifying pim state", dut) + show_pim_state_json = run_frr_cmd( + rnode, "show {} pim state json".format(ip_cmd), isjson=True + ) + + if installed_fl is None: + installed_fl = 1 + + for grp_addr in group_addresses: + if src_address is None: + src_address = "*" + pim_state_json = show_pim_state_json[grp_addr][src_address] + else: + pim_state_json = show_pim_state_json[grp_addr][src_address] + + if pim_state_json["Installed"] == installed_fl: + logger.info( + "[DUT %s]: group %s is installed flag: %s", + dut, + grp_addr, + pim_state_json["Installed"], + ) + for interface, data in pim_state_json[iif].items(): + if interface != oil: + continue + + # Verify iif, oil and installed state + if ( + data["group"] == grp_addr + and data["installed"] == installed_fl + and data["inboundInterface"] == iif + and data["outboundInterface"] == oil + ): + logger.info( + "[DUT %s]: Verifying pim state for group" + " %s [PASSED]!! Found Expected: " + "(iif: %s, oil: %s, installed: %s) ", + dut, + grp_addr, + data["inboundInterface"], + data["outboundInterface"], + data["installed"], + ) + else: + errormsg = ( + "[DUT %s]: Verifying pim state for group" + " %s, [FAILED]!! Expected: " + "(iif: %s, oil: %s, installed: %s) " + % (dut, grp_addr, iif, oil, "1"), + "Found: (iif: %s, oil: %s, installed: %s)" + % ( + data["inboundInterface"], + data["outboundInterface"], + data["installed"], + ), + ) + return errormsg + else: + errormsg = "[DUT %s]: %s install flag value not as expected" % ( + dut, + grp_addr, + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def get_pim_interface_traffic(tgen, input_dict): + """ + get ip pim interface traffice by running + "show ip pim interface traffic" cli + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict(dict)`: defines DUT, what and from which interfaces + traffic needs to be retrieved + Usage + ----- + input_dict = { + "r1": { + "r1-r0-eth0": { + "helloRx": 0, + "helloTx": 1, + "joinRx": 0, + "joinTx": 0 + } + } + } + + result = get_pim_interface_traffic(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + output_dict = {} + for dut in input_dict.keys(): + if dut not in tgen.routers(): + continue + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying pim interface traffic", dut) + + def show_pim_intf_traffic(rnode, dut, input_dict, output_dict): + show_pim_intf_traffic_json = run_frr_cmd( + rnode, "show ip pim interface traffic json", isjson=True + ) + + output_dict[dut] = {} + for intf, data in input_dict[dut].items(): + interface_json = show_pim_intf_traffic_json[intf] + for state in data: + + # Verify Tx/Rx + if state in interface_json: + output_dict[dut][state] = interface_json[state] + else: + errormsg = ( + "[DUT %s]: %s is not present" + "for interface %s [FAILED]!! " % (dut, state, intf) + ) + return errormsg + return None + + test_func = functools.partial( + show_pim_intf_traffic, rnode, dut, input_dict, output_dict + ) + (result, out) = topotest.run_and_expect(test_func, None, count=20, wait=1) + if not result: + return out + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return output_dict + + +@retry(retry_timeout=40, diag_pct=0) +def verify_pim_interface( + tgen, topo, dut, interface=None, interface_ip=None, expected=True +): + """ + Verify all PIM interface are up and running, config is verified + using "show ip pim interface" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : device under test + * `interface` : interface name + * `interface_ip` : interface ip address + * `expected` : expected results from API, by-default True + + Usage + ----- + result = verify_pim_interfacetgen, topo, dut, interface=ens192, interface_ip=20.1.1.1) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in tgen.routers(): + if router != dut: + continue + + logger.info("[DUT: %s]: Verifying PIM interface status:", dut) + + rnode = tgen.routers()[dut] + show_ip_pim_interface_json = rnode.vtysh_cmd( + "show ip pim interface json", isjson=True + ) + + logger.info("show_ip_pim_interface_json: \n %s", show_ip_pim_interface_json) + + if interface_ip: + if interface in show_ip_pim_interface_json: + pim_intf_json = show_ip_pim_interface_json[interface] + if pim_intf_json["address"] != interface_ip: + errormsg = ( + "[DUT %s]: PIM interface " + "ip is not correct " + "[FAILED]!! Expected : %s, Found : %s" + % (dut, pim_intf_json["address"], interface_ip) + ) + return errormsg + else: + logger.info( + "[DUT %s]: PIM interface " + "ip is correct " + "[Passed]!! Expected : %s, Found : %s" + % (dut, pim_intf_json["address"], interface_ip) + ) + return True + else: + for destLink, data in topo["routers"][dut]["links"].items(): + if "type" in data and data["type"] == "loopback": + continue + + if "pim" in data and data["pim"] == "enable": + pim_interface = data["interface"] + pim_intf_ip = data["ipv4"].split("/")[0] + + if pim_interface in show_ip_pim_interface_json: + pim_intf_json = show_ip_pim_interface_json[pim_interface] + else: + errormsg = ( + "[DUT %s]: PIM interface: %s " + "PIM interface ip: %s, not Found" + % (dut, pim_interface, pim_intf_ip) + ) + return errormsg + + # Verifying PIM interface + if ( + pim_intf_json["address"] != pim_intf_ip + and pim_intf_json["state"] != "up" + ): + errormsg = ( + "[DUT %s]: PIM interface: %s " + "PIM interface ip: %s, status check " + "[FAILED]!! Expected : %s, Found : %s" + % ( + dut, + pim_interface, + pim_intf_ip, + pim_interface, + pim_intf_json["state"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: PIM interface: %s, " + "interface ip: %s, status: %s" + " [PASSED]!!", + dut, + pim_interface, + pim_intf_ip, + pim_intf_json["state"], + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def clear_pim_interface_traffic(tgen, topo): + """ + Clear ip/ipv6 pim interface traffice by running + "clear ip/ipv6 pim interface traffic" cli + + Parameters + ---------- + * `tgen`: topogen object + Usage + ----- + + result = clear_pim_interface_traffic(tgen, topo) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for dut in tgen.routers(): + if "pim" not in topo["routers"][dut]: + continue + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Clearing pim interface traffic", dut) + result = run_frr_cmd(rnode, "clear ip pim interface traffic") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +def clear_pim_interfaces(tgen, dut): + """ + Clear ip/ipv6 pim interface by running + "clear ip/ipv6 pim interfaces" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: Device Under Test + Usage + ----- + + result = clear_pim_interfaces(tgen, dut) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + nh_before_clear = {} + nh_after_clear = {} + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verify pim neighbor before pim" " neighbor clear", dut) + # To add uptime initially + sleep(10) + run_json_before = run_frr_cmd(rnode, "show ip pim neighbor json", isjson=True) + + for key, value in run_json_before.items(): + if bool(value): + for _key, _value in value.items(): + nh_before_clear[key] = _value["upTime"] + + # Clearing PIM neighbors + logger.info("[DUT: %s]: Clearing pim interfaces", dut) + run_frr_cmd(rnode, "clear ip pim interfaces") + + logger.info("[DUT: %s]: Verify pim neighbor after pim" " neighbor clear", dut) + + found = False + + # Waiting for maximum 60 sec + fail_intf = [] + for retry in range(1, 13): + logger.info("[DUT: %s]: Waiting for 5 sec for PIM neighbors" " to come up", dut) + sleep(5) + run_json_after = run_frr_cmd(rnode, "show ip pim neighbor json", isjson=True) + found = True + for pim_intf in nh_before_clear.keys(): + if pim_intf not in run_json_after or not run_json_after[pim_intf]: + found = False + fail_intf.append(pim_intf) + + if found is True: + break + else: + errormsg = ( + "[DUT: %s]: pim neighborship is not formed for %s" + "after clear_ip_pim_interfaces %s [FAILED!!]", + dut, + fail_intf, + ) + return errormsg + + for key, value in run_json_after.items(): + if bool(value): + for _key, _value in value.items(): + nh_after_clear[key] = _value["upTime"] + + # Verify uptime for neighbors + for pim_intf in nh_before_clear.keys(): + d1 = datetime.datetime.strptime(nh_before_clear[pim_intf], "%H:%M:%S") + d2 = datetime.datetime.strptime(nh_after_clear[pim_intf], "%H:%M:%S") + if d2 >= d1: + errormsg = ( + "[DUT: %s]: PIM neighborship is not cleared for", + " interface %s [FAILED!!]", + dut, + pim_intf, + ) + + logger.info("[DUT: %s]: PIM neighborship is cleared [PASSED!!]") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +def clear_igmp_interfaces(tgen, dut): + """ + Clear ip/ipv6 igmp interfaces by running + "clear ip/ipv6 igmp interfaces" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + + Usage + ----- + dut = "r1" + result = clear_igmp_interfaces(tgen, dut) + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + group_before_clear = {} + group_after_clear = {} + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: IGMP group uptime before clear" " igmp groups:", dut) + igmp_json = run_frr_cmd(rnode, "show ip igmp groups json", isjson=True) + + total_groups_before_clear = igmp_json["totalGroups"] + + for key, value in igmp_json.items(): + if type(value) is not dict: + continue + + groups = value["groups"] + group = groups[0]["group"] + uptime = groups[0]["uptime"] + group_before_clear[group] = uptime + + logger.info("[DUT: %s]: Clearing ip igmp interfaces", dut) + result = run_frr_cmd(rnode, "clear ip igmp interfaces") + + # Waiting for maximum 60 sec + for retry in range(1, 13): + logger.info( + "[DUT: %s]: Waiting for 5 sec for igmp interfaces" " to come up", dut + ) + sleep(5) + igmp_json = run_frr_cmd(rnode, "show ip igmp groups json", isjson=True) + + total_groups_after_clear = igmp_json["totalGroups"] + + if total_groups_before_clear == total_groups_after_clear: + break + + for key, value in igmp_json.items(): + if type(value) is not dict: + continue + + groups = value["groups"] + group = groups[0]["group"] + uptime = groups[0]["uptime"] + group_after_clear[group] = uptime + + # Verify uptime for groups + for group in group_before_clear.keys(): + d1 = datetime.datetime.strptime(group_before_clear[group], "%H:%M:%S") + d2 = datetime.datetime.strptime(group_after_clear[group], "%H:%M:%S") + if d2 >= d1: + errormsg = ("[DUT: %s]: IGMP group is not cleared", " [FAILED!!]", dut) + + logger.info("[DUT: %s]: IGMP group is cleared [PASSED!!]") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +@retry(retry_timeout=20) +def clear_mroute_verify(tgen, dut, expected=True): + """ + Clear ip/ipv6 mroute by running "clear ip/ipv6 mroute" cli and verify + mroutes are up again after mroute clear + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: Device Under Test + * `expected` : expected results from API, by-default True + + Usage + ----- + + result = clear_mroute_verify(tgen, dut) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + mroute_before_clear = {} + mroute_after_clear = {} + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: IP mroutes uptime before clear", dut) + mroute_json_1 = run_frr_cmd(rnode, "show ip mroute json", isjson=True) + + for group in mroute_json_1.keys(): + mroute_before_clear[group] = {} + for key in mroute_json_1[group].keys(): + for _key, _value in mroute_json_1[group][key]["oil"].items(): + if _key != "pimreg": + mroute_before_clear[group][key] = _value["upTime"] + + logger.info("[DUT: %s]: Clearing ip mroute", dut) + result = run_frr_cmd(rnode, "clear ip mroute") + + # RFC 3376: 8.2. Query Interval - Default: 125 seconds + # So waiting for maximum 130 sec to get the igmp report + for retry in range(1, 26): + logger.info("[DUT: %s]: Waiting for 2 sec for mroutes" " to come up", dut) + sleep(5) + keys_json1 = mroute_json_1.keys() + mroute_json_2 = run_frr_cmd(rnode, "show ip mroute json", isjson=True) + + if bool(mroute_json_2): + keys_json2 = mroute_json_2.keys() + + for group in mroute_json_2.keys(): + flag = False + for key in mroute_json_2[group].keys(): + if "oil" not in mroute_json_2[group]: + continue + + for _key, _value in mroute_json_2[group][key]["oil"].items(): + if _key != "pimreg" and keys_json1 == keys_json2: + break + flag = True + if flag: + break + else: + continue + + for group in mroute_json_2.keys(): + mroute_after_clear[group] = {} + for key in mroute_json_2[group].keys(): + for _key, _value in mroute_json_2[group][key]["oil"].items(): + if _key != "pimreg": + mroute_after_clear[group][key] = _value["upTime"] + + # Verify uptime for mroute + for group in mroute_before_clear.keys(): + for source in mroute_before_clear[group].keys(): + if set(mroute_before_clear[group]) != set(mroute_after_clear[group]): + errormsg = ( + "[DUT: %s]: mroute (%s, %s) has not come" + " up after mroute clear [FAILED!!]" % (dut, source, group) + ) + return errormsg + + d1 = datetime.datetime.strptime( + mroute_before_clear[group][source], "%H:%M:%S" + ) + d2 = datetime.datetime.strptime( + mroute_after_clear[group][source], "%H:%M:%S" + ) + if d2 >= d1: + errormsg = "[DUT: %s]: IP mroute is not cleared" " [FAILED!!]" % (dut) + + logger.info("[DUT: %s]: IP mroute is cleared [PASSED!!]", dut) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return True + + +def clear_mroute(tgen, dut=None): + """ + Clear ip/ipv6 mroute by running "clear ip mroute" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test, default None + + Usage + ----- + clear_mroute(tgen, dut) + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + router_list = tgen.routers() + for router, rnode in router_list.items(): + if dut is not None and router != dut: + continue + + logger.debug("[DUT: %s]: Clearing ip mroute", router) + rnode.vtysh_cmd("clear ip mroute") + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + +def reconfig_interfaces(tgen, topo, senderRouter, receiverRouter, packet=None): + """ + Configure interface ip for sender and receiver routers + as per bsr packet + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `senderRouter` : Sender router + * `receiverRouter` : Receiver router + * `packet` : BSR packet in raw format + + Returns + ------- + True or False + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + config_data = [] + + src_ip = topo["routers"][senderRouter]["bsm"]["bsr_packets"][packet]["src_ip"] + dest_ip = topo["routers"][senderRouter]["bsm"]["bsr_packets"][packet]["dest_ip"] + + for destLink, data in topo["routers"][senderRouter]["links"].items(): + if "type" in data and data["type"] == "loopback": + continue + + if "pim" in data and data["pim"] == "enable": + sender_interface = data["interface"] + sender_interface_ip = data["ipv4"] + + config_data.append("interface {}".format(sender_interface)) + config_data.append("no ip address {}".format(sender_interface_ip)) + config_data.append("ip address {}".format(src_ip)) + + result = create_common_configuration( + tgen, senderRouter, config_data, "interface_config" + ) + if result is not True: + return False + + config_data = [] + links = topo["routers"][destLink]["links"] + pim_neighbor = {key: links[key] for key in [senderRouter]} + + data = pim_neighbor[senderRouter] + if "type" in data and data["type"] == "loopback": + continue + + if "pim" in data and data["pim"] == "enable": + receiver_interface = data["interface"] + receiver_interface_ip = data["ipv4"] + + config_data.append("interface {}".format(receiver_interface)) + config_data.append("no ip address {}".format(receiver_interface_ip)) + config_data.append("ip address {}".format(dest_ip)) + + result = create_common_configuration( + tgen, receiverRouter, config_data, "interface_config" + ) + if result is not True: + return False + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: reconfig_interfaces()") + return result + + +def add_rp_interfaces_and_pim_config(tgen, topo, interface, rp, rp_mapping): + """ + Add physical interfaces tp RP for all the RPs + + Parameters + ---------- + * `tgen` : Topogen object + * `topo` : json file data + * `interface` : RP interface + * `rp` : rp for given topology + * `rp_mapping` : dictionary of all groups and RPs + + Returns + ------- + True or False + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + config_data = [] + + for group, rp_list in rp_mapping.items(): + for _rp in rp_list: + config_data.append("interface {}".format(interface)) + config_data.append("ip address {}".format(_rp)) + config_data.append("ip pim") + + # Why not config just once, why per group? + result = create_common_configuration( + tgen, rp, config_data, "interface_config" + ) + if result is not True: + return False + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def scapy_send_bsr_raw_packet(tgen, topo, senderRouter, receiverRouter, packet=None): + """ + Using scapy Raw() method to send BSR raw packet from one FRR + to other + + Parameters: + ----------- + * `tgen` : Topogen object + * `topo` : json file data + * `senderRouter` : Sender router + * `receiverRouter` : Receiver router + * `packet` : BSR packet in raw format + + returns: + -------- + errormsg or True + """ + + global CWD + result = "" + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + python3_path = tgen.net.get_exec_path(["python3", "python"]) + script_path = os.path.join(CWD, "send_bsr_packet.py") + node = tgen.net[senderRouter] + + for destLink, data in topo["routers"][senderRouter]["links"].items(): + if "type" in data and data["type"] == "loopback": + continue + + if "pim" in data and data["pim"] == "enable": + sender_interface = data["interface"] + + packet = topo["routers"][senderRouter]["bsm"]["bsr_packets"][packet]["data"] + + cmd = [ + python3_path, + script_path, + packet, + sender_interface, + "--interval=1", + "--count=1", + ] + logger.info("Scapy cmd: \n %s", cmd) + node.cmd_raises(cmd) + + logger.debug("Exiting lib API: scapy_send_bsr_raw_packet") + return True + + +def find_rp_from_bsrp_info(tgen, dut, bsr, grp=None): + """ + Find which RP is having lowest prioriy and returns rp IP + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `bsr`: BSR address + * 'grp': Group Address + + Usage + ----- + dut = "r1" + result = verify_pim_rp_info(tgen, dut, bsr) + + Returns: + dictionary: group and RP, which has to be installed as per + lowest priority or highest priority + """ + + rp_details = {} + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Fetching rp details from bsrp-info", dut) + bsrp_json = run_frr_cmd(rnode, "show ip pim bsrp-info json", isjson=True) + + if grp not in bsrp_json: + return {} + + for group, rp_data in bsrp_json.items(): + if group == "BSR Address" and bsrp_json["BSR Address"] == bsr: + continue + + if group != grp: + continue + + rp_priority = {} + rp_hash = {} + + for rp, value in rp_data.items(): + if rp == "Pending RP count": + continue + rp_priority[value["Rp Address"]] = value["Rp Priority"] + rp_hash[value["Rp Address"]] = value["Hash Val"] + + priority_dict = dict(zip(rp_priority.values(), rp_priority.keys())) + hash_dict = dict(zip(rp_hash.values(), rp_hash.keys())) + + # RP with lowest priority + if len(priority_dict) != 1: + rp_p, lowest_priority = sorted(rp_priority.items(), key=lambda x: x[1])[0] + rp_details[group] = rp_p + + # RP with highest hash value + if len(priority_dict) == 1: + rp_h, highest_hash = sorted(rp_hash.items(), key=lambda x: x[1])[-1] + rp_details[group] = rp_h + + # RP with highest IP address + if len(priority_dict) == 1 and len(hash_dict) == 1: + rp_details[group] = sorted(rp_priority.keys())[-1] + + return rp_details + + +@retry(retry_timeout=12) +def verify_pim_grp_rp_source( + tgen, topo, dut, grp_addr, rp_source, rpadd=None, expected=True +): + """ + Verify pim rp info by running "show ip pim rp-info" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: JSON file handler + * `dut`: device under test + * `grp_addr`: IGMP group address + * 'rp_source': source from which rp installed + * 'rpadd': rp address + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + group_address = "225.1.1.1" + rp_source = "BSR" + result = verify_pim_rp_and_source(tgen, topo, dut, group_address, rp_source) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying ip rp info", dut) + show_ip_rp_info_json = run_frr_cmd(rnode, "show ip pim rp-info json", isjson=True) + + if rpadd != None: + rp_json = show_ip_rp_info_json[rpadd] + if rp_json[0]["group"] == grp_addr: + if rp_json[0]["source"] == rp_source: + logger.info( + "[DUT %s]: Verifying Group and rp_source [PASSED]" + "Found Expected: %s, %s" + % (dut, rp_json[0]["group"], rp_json[0]["source"]) + ) + return True + else: + errormsg = ( + "[DUT %s]: Verifying Group and rp_source [FAILED]" + "Expected (%s, %s) " + "Found (%s, %s)" + % ( + dut, + grp_addr, + rp_source, + rp_json[0]["group"], + rp_json[0]["source"], + ) + ) + return errormsg + errormsg = ( + "[DUT %s]: Verifying Group and rp_source [FAILED]" + "Expected: %s, %s but not found" % (dut, grp_addr, rp_source) + ) + return errormsg + + for rp in show_ip_rp_info_json: + rp_json = show_ip_rp_info_json[rp] + logger.info("%s", rp_json) + if rp_json[0]["group"] == grp_addr: + if rp_json[0]["source"] == rp_source: + logger.info( + "[DUT %s]: Verifying Group and rp_source [PASSED]" + "Found Expected: %s, %s" + % (dut, rp_json[0]["group"], rp_json[0]["source"]) + ) + return True + else: + errormsg = ( + "[DUT %s]: Verifying Group and rp_source [FAILED]" + "Expected (%s, %s) " + "Found (%s, %s)" + % ( + dut, + grp_addr, + rp_source, + rp_json[0]["group"], + rp_json[0]["source"], + ) + ) + return errormsg + + errormsg = ( + "[DUT %s]: Verifying Group and rp_source [FAILED]" + "Expected: %s, %s but not found" % (dut, grp_addr, rp_source) + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + + return errormsg + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_bsr(tgen, topo, dut, bsr_ip, expected=True): + """ + Verify all PIM interface are up and running, config is verified + using "show ip pim interface" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : device under test + * 'bsr' : bsr ip to be verified + * `expected` : expected results from API, by-default True + + Usage + ----- + result = verify_pim_bsr(tgen, topo, dut, bsr_ip) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in tgen.routers(): + if router != dut: + continue + + logger.info("[DUT: %s]: Verifying PIM bsr status:", dut) + + rnode = tgen.routers()[dut] + pim_bsr_json = rnode.vtysh_cmd("show ip pim bsr json", isjson=True) + + logger.info("show_ip_pim_bsr_json: \n %s", pim_bsr_json) + + # Verifying PIM bsr + if pim_bsr_json["bsr"] != bsr_ip: + errormsg = ( + "[DUT %s]:" + "bsr status: not found" + "[FAILED]!! Expected : %s, Found : %s" + % (dut, bsr_ip, pim_bsr_json["bsr"]) + ) + return errormsg + + logger.info( + "[DUT %s]:" " bsr status: found, Address :%s" " [PASSED]!!", + dut, + pim_bsr_json["bsr"], + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_upstream_rpf( + tgen, topo, dut, interface, group_addresses, rp=None, expected=True +): + """ + Verify IP/IPv6 PIM upstream rpf, config is verified + using "show ip/ipv6 pim neighbor" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : devuce under test + * `interface` : upstream interface + * `group_addresses` : list of group address for which upstream info + needs to be checked + * `rp` : RP address + * `expected` : expected results from API, by-default True + + Usage + ----- + result = verify_pim_upstream_rpf(gen, topo, dut, interface, + group_addresses, rp=None) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if "pim" in topo["routers"][dut]: + + logger.info("[DUT: %s]: Verifying ip pim upstream rpf:", dut) + + rnode = tgen.routers()[dut] + show_ip_pim_upstream_rpf_json = rnode.vtysh_cmd( + "show ip pim upstream-rpf json", isjson=True + ) + + logger.info( + "show_ip_pim_upstream_rpf_json: \n %s", show_ip_pim_upstream_rpf_json + ) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp_addr in group_addresses: + for destLink, data in topo["routers"][dut]["links"].items(): + if "type" in data and data["type"] == "loopback": + continue + + if "pim" not in topo["routers"][destLink]: + continue + + # Verify RP info + if rp is None: + rp_details = find_rp_details(tgen, topo) + else: + rp_details = {dut: rp} + + if dut in rp_details: + pim_nh_intf_ip = topo["routers"][dut]["links"]["lo"]["ipv4"].split( + "/" + )[0] + else: + if destLink not in interface: + continue + + links = topo["routers"][destLink]["links"] + pim_neighbor = {key: links[key] for key in [dut]} + + data = pim_neighbor[dut] + if "pim" in data and data["pim"] == "enable": + pim_nh_intf_ip = data["ipv4"].split("/")[0] + + upstream_rpf_json = show_ip_pim_upstream_rpf_json[grp_addr]["*"] + + # Verifying ip pim upstream rpf + if ( + upstream_rpf_json["rpfInterface"] == interface + and upstream_rpf_json["ribNexthop"] != pim_nh_intf_ip + ): + errormsg = ( + "[DUT %s]: Verifying group: %s, " + "rpf interface: %s, " + " rib Nexthop check [FAILED]!!" + "Expected: %s, Found: %s" + % ( + dut, + grp_addr, + interface, + pim_nh_intf_ip, + upstream_rpf_json["ribNexthop"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying group: %s," + " rpf interface: %s, " + " rib Nexthop: %s [PASSED]!!", + dut, + grp_addr, + interface, + pim_nh_intf_ip, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def enable_disable_pim_unicast_bsm(tgen, router, intf, enable=True): + """ + Helper API to enable or disable pim bsm on interfaces + + Parameters + ---------- + * `tgen` : Topogen object + * `router` : router id to be configured. + * `intf` : Interface to be configured + * `enable` : this flag denotes if config should be enabled or disabled + + Returns + ------- + True or False + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + config_data = [] + cmd = "interface {}".format(intf) + config_data.append(cmd) + + if enable == True: + config_data.append("ip pim unicast-bsm") + else: + config_data.append("no ip pim unicast-bsm") + + result = create_common_configuration( + tgen, router, config_data, "interface_config", build=False + ) + if result is not True: + return False + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +def enable_disable_pim_bsm(tgen, router, intf, enable=True): + """ + Helper API to enable or disable pim bsm on interfaces + + Parameters + ---------- + * `tgen` : Topogen object + * `router` : router id to be configured. + * `intf` : Interface to be configured + * `enable` : this flag denotes if config should be enabled or disabled + + Returns + ------- + True or False + """ + result = False + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + try: + config_data = [] + cmd = "interface {}".format(intf) + config_data.append(cmd) + + if enable is True: + config_data.append("ip pim bsm") + else: + config_data.append("no ip pim bsm") + + result = create_common_configuration( + tgen, router, config_data, "interface_config", build=False + ) + if result is not True: + return False + + except InvalidCLIError: + # Traceback + errormsg = traceback.format_exc() + logger.error(errormsg) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return result + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_join( + tgen, topo, dut, interface, group_addresses, src_address=None, expected=True +): + """ + Verify ip/ipv6 pim join by running "show ip/ipv6 pim join" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo`: JSON file handler + * `dut`: device under test + * `interface`: interface name, from which PIM join would come + * `group_addresses`: IGMP group address + * `src_address`: Source address + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + interface = "r1-r0-eth0" + group_address = "225.1.1.1" + result = verify_pim_join(tgen, dut, star, group_address, interface) + + Returns + ------- + errormsg(str) or True + """ + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying pim join", dut) + show_pim_join_json = run_frr_cmd(rnode, "show ip pim join json", isjson=True) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp_addr in group_addresses: + # Verify if IGMP is enabled in DUT + if "igmp" not in topo["routers"][dut]: + pim_join = True + else: + pim_join = False + + interface_json = show_pim_join_json[interface] + + grp_addr = grp_addr.split("/")[0] + for source, data in interface_json[grp_addr].items(): + + # Verify pim join + if pim_join: + if data["group"] == grp_addr and data["channelJoinName"] == "JOIN": + logger.info( + "[DUT %s]: Verifying pim join for group: %s" + "[PASSED]!! Found Expected: (%s)", + dut, + grp_addr, + data["channelJoinName"], + ) + else: + errormsg = ( + "[DUT %s]: Verifying pim join for group: %s" + "[FAILED]!! Expected: (%s) " + "Found: (%s)" % (dut, grp_addr, "JOIN", data["channelJoinName"]) + ) + return errormsg + + if not pim_join: + if data["group"] == grp_addr and data["channelJoinName"] == "NOINFO": + logger.info( + "[DUT %s]: Verifying pim join for group: %s" + "[PASSED]!! Found Expected: (%s)", + dut, + grp_addr, + data["channelJoinName"], + ) + else: + errormsg = ( + "[DUT %s]: Verifying pim join for group: %s" + "[FAILED]!! Expected: (%s) " + "Found: (%s)" + % (dut, grp_addr, "NOINFO", data["channelJoinName"]) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=60, diag_pct=0) +def verify_igmp_config(tgen, input_dict, stats_return=False, expected=True): + """ + Verify igmp interface details, verifying following configs: + timerQueryInterval + timerQueryResponseIntervalMsec + lastMemberQueryCount + timerLastMemberQueryMsec + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict` : Input dict data, required to verify + timer + * `stats_return`: If user wants API to return statistics + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict ={ + "l1": { + "igmp": { + "interfaces": { + "l1-i1-eth1": { + "igmp": { + "query": { + "query-interval" : 200, + "query-max-response-time" : 100 + }, + "statistics": { + "queryV2" : 2, + "reportV2" : 1 + } + } + } + } + } + } + } + result = verify_igmp_config(tgen, input_dict, stats_return) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for dut in input_dict.keys(): + rnode = tgen.routers()[dut] + + for interface, data in input_dict[dut]["igmp"]["interfaces"].items(): + + statistics = False + report = False + if "statistics" in input_dict[dut]["igmp"]["interfaces"][interface]["igmp"]: + statistics = True + cmd = "show ip igmp statistics" + else: + cmd = "show ip igmp" + + logger.info( + "[DUT: %s]: Verifying IGMP interface %s detail:", dut, interface + ) + + if statistics: + if ( + "report" + in input_dict[dut]["igmp"]["interfaces"][interface]["igmp"][ + "statistics" + ] + ): + report = True + + if statistics and report: + show_ip_igmp_intf_json = run_frr_cmd( + rnode, "{} json".format(cmd), isjson=True + ) + intf_detail_json = show_ip_igmp_intf_json["global"] + else: + show_ip_igmp_intf_json = run_frr_cmd( + rnode, "{} interface {} json".format(cmd, interface), isjson=True + ) + + if not report: + if interface not in show_ip_igmp_intf_json: + errormsg = ( + "[DUT %s]: IGMP interface: %s " + " is not present in CLI output " + "[FAILED]!! " % (dut, interface) + ) + return errormsg + + else: + intf_detail_json = show_ip_igmp_intf_json[interface] + + if stats_return: + igmp_stats = {} + + if "statistics" in data["igmp"]: + if stats_return: + igmp_stats["statistics"] = {} + for query, value in data["igmp"]["statistics"].items(): + if query == "queryV2": + # Verifying IGMP interface queryV2 statistics + if stats_return: + igmp_stats["statistics"][query] = intf_detail_json[ + "queryV2" + ] + + else: + if intf_detail_json["queryV2"] != value: + errormsg = ( + "[DUT %s]: IGMP interface: %s " + " queryV2 statistics verification " + "[FAILED]!! Expected : %s," + " Found : %s" + % ( + dut, + interface, + value, + intf_detail_json["queryV2"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " + "queryV2 statistics is %s", + dut, + interface, + value, + ) + + if query == "reportV2": + # Verifying IGMP interface timerV2 statistics + if stats_return: + igmp_stats["statistics"][query] = intf_detail_json[ + "reportV2" + ] + + else: + if intf_detail_json["reportV2"] <= value: + errormsg = ( + "[DUT %s]: IGMP reportV2 " + "statistics verification " + "[FAILED]!! Expected : %s " + "or more, Found : %s" + % ( + dut, + interface, + value, + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP reportV2 " "statistics is %s", + dut, + intf_detail_json["reportV2"], + ) + + if "query" in data["igmp"]: + for query, value in data["igmp"]["query"].items(): + if query == "query-interval": + # Verifying IGMP interface query interval timer + if intf_detail_json["timerQueryInterval"] != value: + errormsg = ( + "[DUT %s]: IGMP interface: %s " + " query-interval verification " + "[FAILED]!! Expected : %s," + " Found : %s" + % ( + dut, + interface, + value, + intf_detail_json["timerQueryInterval"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " "query-interval is %s", + dut, + interface, + value, + ) + + if query == "query-max-response-time": + # Verifying IGMP interface query max response timer + if ( + intf_detail_json["timerQueryResponseIntervalMsec"] + != value * 100 + ): + errormsg = ( + "[DUT %s]: IGMP interface: %s " + "query-max-response-time " + "verification [FAILED]!!" + " Expected : %s, Found : %s" + % ( + dut, + interface, + value * 1000, + intf_detail_json["timerQueryResponseIntervalMsec"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " + "query-max-response-time is %s ms", + dut, + interface, + value * 100, + ) + + if query == "last-member-query-count": + # Verifying IGMP interface last member query count + if intf_detail_json["lastMemberQueryCount"] != value: + errormsg = ( + "[DUT %s]: IGMP interface: %s " + "last-member-query-count " + "verification [FAILED]!!" + " Expected : %s, Found : %s" + % ( + dut, + interface, + value, + intf_detail_json["lastMemberQueryCount"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " + "last-member-query-count is %s ms", + dut, + interface, + value * 1000, + ) + + if query == "last-member-query-interval": + # Verifying IGMP interface last member query interval + if ( + intf_detail_json["timerLastMemberQueryMsec"] + != value * 100 * intf_detail_json["lastMemberQueryCount"] + ): + errormsg = ( + "[DUT %s]: IGMP interface: %s " + "last-member-query-interval " + "verification [FAILED]!!" + " Expected : %s, Found : %s" + % ( + dut, + interface, + value * 1000, + intf_detail_json["timerLastMemberQueryMsec"], + ) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " + "last-member-query-interval is %s ms", + dut, + interface, + value * intf_detail_json["lastMemberQueryCount"] * 100, + ) + + if "version" in data["igmp"]: + # Verifying IGMP interface state is up + if intf_detail_json["state"] != "up": + errormsg = ( + "[DUT %s]: IGMP interface: %s " + " state: %s verification " + "[FAILED]!!" % (dut, interface, intf_detail_json["state"]) + ) + return errormsg + + logger.info( + "[DUT %s]: IGMP interface: %s " "state: %s", + dut, + interface, + intf_detail_json["state"], + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True if stats_return == False else igmp_stats + + +@retry(retry_timeout=60, diag_pct=0) +def verify_pim_config(tgen, input_dict, expected=True): + """ + Verify pim interface details, verifying following configs: + drPriority + helloPeriod + helloReceived + helloSend + drAddress + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict` : Input dict data, required to verify + timer + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict ={ + "l1": { + "igmp": { + "interfaces": { + "l1-i1-eth1": { + "pim": { + "drPriority" : 10, + "helloPeriod" : 5 + } + } + } + } + } + } + } + result = verify_pim_config(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for dut in input_dict.keys(): + rnode = tgen.routers()[dut] + + for interface, data in input_dict[dut]["pim"]["interfaces"].items(): + + logger.info("[DUT: %s]: Verifying PIM interface %s detail:", dut, interface) + + show_ip_igmp_intf_json = run_frr_cmd( + rnode, "show ip pim interface {} json".format(interface), isjson=True + ) + + if interface not in show_ip_igmp_intf_json: + errormsg = ( + "[DUT %s]: PIM interface: %s " + " is not present in CLI output " + "[FAILED]!! " % (dut, interface) + ) + return errormsg + + intf_detail_json = show_ip_igmp_intf_json[interface] + + for config, value in data.items(): + if config == "helloPeriod": + # Verifying PIM interface helloPeriod + if intf_detail_json["helloPeriod"] != value: + errormsg = ( + "[DUT %s]: PIM interface: %s " + " helloPeriod verification " + "[FAILED]!! Expected : %s," + " Found : %s" + % (dut, interface, value, intf_detail_json["helloPeriod"]) + ) + return errormsg + + logger.info( + "[DUT %s]: PIM interface: %s " "helloPeriod is %s", + dut, + interface, + value, + ) + + if config == "drPriority": + # Verifying PIM interface drPriority + if intf_detail_json["drPriority"] != value: + errormsg = ( + "[DUT %s]: PIM interface: %s " + " drPriority verification " + "[FAILED]!! Expected : %s," + " Found : %s" + % (dut, interface, value, intf_detail_json["drPriority"]) + ) + return errormsg + + logger.info( + "[DUT %s]: PIM interface: %s " "drPriority is %s", + dut, + interface, + value, + ) + + if config == "drAddress": + # Verifying PIM interface drAddress + if intf_detail_json["drAddress"] != value: + errormsg = ( + "[DUT %s]: PIM interface: %s " + " drAddress verification " + "[FAILED]!! Expected : %s," + " Found : %s" + % (dut, interface, value, intf_detail_json["drAddress"]) + ) + return errormsg + + logger.info( + "[DUT %s]: PIM interface: %s " "drAddress is %s", + dut, + interface, + value, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=20, diag_pct=0) +def verify_multicast_traffic(tgen, input_dict, return_traffic=False, expected=True): + """ + Verify multicast traffic by running + "show multicast traffic count json" cli + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict(dict)`: defines DUT, what and for which interfaces + traffic needs to be verified + * `return_traffic`: returns traffic stats + * `expected` : expected results from API, by-default True + + Usage + ----- + input_dict = { + "r1": { + "traffic_received": ["r1-r0-eth0"], + "traffic_sent": ["r1-r0-eth0"] + } + } + + result = verify_multicast_traffic(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + traffic_dict = {} + for dut in input_dict.keys(): + if dut not in tgen.routers(): + continue + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying multicast " "traffic", dut) + + show_multicast_traffic_json = run_frr_cmd( + rnode, "show ip multicast count json", isjson=True + ) + + for traffic_type, interfaces in input_dict[dut].items(): + traffic_dict[traffic_type] = {} + if traffic_type == "traffic_received": + for interface in interfaces: + traffic_dict[traffic_type][interface] = {} + interface_json = show_multicast_traffic_json[interface] + + if interface_json["pktsIn"] == 0 and interface_json["bytesIn"] == 0: + errormsg = ( + "[DUT %s]: Multicast traffic is " + "not received on interface %s " + "PktsIn: %s, BytesIn: %s " + "[FAILED]!!" + % ( + dut, + interface, + interface_json["pktsIn"], + interface_json["bytesIn"], + ) + ) + return errormsg + + elif ( + interface_json["pktsIn"] != 0 and interface_json["bytesIn"] != 0 + ): + + traffic_dict[traffic_type][interface][ + "pktsIn" + ] = interface_json["pktsIn"] + traffic_dict[traffic_type][interface][ + "bytesIn" + ] = interface_json["bytesIn"] + + logger.info( + "[DUT %s]: Multicast traffic is " + "received on interface %s " + "PktsIn: %s, BytesIn: %s " + "[PASSED]!!" + % ( + dut, + interface, + interface_json["pktsIn"], + interface_json["bytesIn"], + ) + ) + + else: + errormsg = ( + "[DUT %s]: Multicast traffic interface %s:" + " Miss-match in " + "PktsIn: %s, BytesIn: %s" + "[FAILED]!!" + % ( + dut, + interface, + interface_json["pktsIn"], + interface_json["bytesIn"], + ) + ) + return errormsg + + if traffic_type == "traffic_sent": + traffic_dict[traffic_type] = {} + for interface in interfaces: + traffic_dict[traffic_type][interface] = {} + interface_json = show_multicast_traffic_json[interface] + + if ( + interface_json["pktsOut"] == 0 + and interface_json["bytesOut"] == 0 + ): + errormsg = ( + "[DUT %s]: Multicast traffic is " + "not received on interface %s " + "PktsIn: %s, BytesIn: %s" + "[FAILED]!!" + % ( + dut, + interface, + interface_json["pktsOut"], + interface_json["bytesOut"], + ) + ) + return errormsg + + elif ( + interface_json["pktsOut"] != 0 + and interface_json["bytesOut"] != 0 + ): + + traffic_dict[traffic_type][interface][ + "pktsOut" + ] = interface_json["pktsOut"] + traffic_dict[traffic_type][interface][ + "bytesOut" + ] = interface_json["bytesOut"] + + logger.info( + "[DUT %s]: Multicast traffic is " + "received on interface %s " + "PktsOut: %s, BytesOut: %s " + "[PASSED]!!" + % ( + dut, + interface, + interface_json["pktsOut"], + interface_json["bytesOut"], + ) + ) + else: + errormsg = ( + "[DUT %s]: Multicast traffic interface %s:" + " Miss-match in " + "PktsOut: %s, BytesOut: %s " + "[FAILED]!!" + % ( + dut, + interface, + interface_json["pktsOut"], + interface_json["bytesOut"], + ) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True if return_traffic == False else traffic_dict + + +def get_refCount_for_mroute(tgen, dut, iif, src_address, group_addresses): + """ + Verify upstream inbound interface is updated correctly + by running "show ip pim upstream" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `iif`: inbound interface + * `src_address`: source address + * `group_addresses`: IGMP group address + + Usage + ----- + dut = "r1" + iif = "r1-r0-eth0" + src_address = "*" + group_address = "225.1.1.1" + result = get_refCount_for_mroute(tgen, dut, iif, src_address, + group_address) + + Returns + ------- + refCount(int) + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + refCount = 0 + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying refCount for mroutes: ", dut) + show_ip_pim_upstream_json = run_frr_cmd( + rnode, "show ip pim upstream json", isjson=True + ) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp_addr in group_addresses: + # Verify group address + if grp_addr not in show_ip_pim_upstream_json: + errormsg = "[DUT %s]: Verifying upstream" " for group %s [FAILED]!!" % ( + dut, + grp_addr, + ) + return errormsg + group_addr_json = show_ip_pim_upstream_json[grp_addr] + + # Verify source address + if src_address not in group_addr_json: + errormsg = "[DUT %s]: Verifying upstream" " for (%s,%s) [FAILED]!!" % ( + dut, + src_address, + grp_addr, + ) + return errormsg + + # Verify Inbound Interface + if group_addr_json[src_address]["inboundInterface"] == iif: + refCount = group_addr_json[src_address]["refCount"] + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return refCount + + +@retry(retry_timeout=40, diag_pct=0) +def verify_multicast_flag_state( + tgen, dut, src_address, group_addresses, flag, expected=True +): + """ + Verify flag state for mroutes and make sure (*, G)/(S, G) are having + coorect flags by running "show ip mroute" cli + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `src_address`: source address + * `group_addresses`: IGMP group address + * `flag`: flag state, needs to be verified + * `expected` : expected results from API, by-default True + + Usage + ----- + dut = "r1" + flag = "SC" + group_address = "225.1.1.1" + result = verify_multicast_flag_state(tgen, dut, src_address, + group_address, flag) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying flag state for mroutes", dut) + show_ip_mroute_json = run_frr_cmd(rnode, "show ip mroute json", isjson=True) + + if bool(show_ip_mroute_json) == False: + error_msg = "[DUT %s]: mroutes are not present or flushed out !!" % (dut) + return error_msg + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + for grp_addr in group_addresses: + if grp_addr not in show_ip_mroute_json: + errormsg = ( + "[DUT %s]: Verifying (%s, %s) mroute," "[FAILED]!! ", + dut, + src_address, + grp_addr, + ) + return errormsg + else: + group_addr_json = show_ip_mroute_json[grp_addr] + + if src_address not in group_addr_json: + errormsg = "[DUT %s]: Verifying (%s, %s) mroute," "[FAILED]!! " % ( + dut, + src_address, + grp_addr, + ) + return errormsg + else: + mroutes = group_addr_json[src_address] + + if mroutes["installed"] != 0: + logger.info( + "[DUT %s]: mroute (%s,%s) is installed", dut, src_address, grp_addr + ) + + if mroutes["flags"] != flag: + errormsg = ( + "[DUT %s]: Verifying flag for (%s, %s) " + "mroute [FAILED]!! " + "Expected: %s Found: %s" + % (dut, src_address, grp_addr, flag, mroutes["flags"]) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying flag for (%s, %s)" + " mroute, [PASSED]!! " + "Found Expected: %s", + dut, + src_address, + grp_addr, + mroutes["flags"], + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +@retry(retry_timeout=40, diag_pct=0) +def verify_igmp_interface(tgen, topo, dut, igmp_iface, interface_ip, expected=True): + """ + Verify all IGMP interface are up and running, config is verified + using "show ip igmp interface" cli + + Parameters + ---------- + * `tgen`: topogen object + * `topo` : json file data + * `dut` : device under test + * `igmp_iface` : interface name + * `interface_ip` : interface ip address + * `expected` : expected results from API, by-default True + + Usage + ----- + result = verify_igmp_interface(tgen, topo, dut, igmp_iface, interface_ip) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + for router in tgen.routers(): + if router != dut: + continue + + logger.info("[DUT: %s]: Verifying PIM interface status:", dut) + + rnode = tgen.routers()[dut] + show_ip_igmp_interface_json = run_frr_cmd( + rnode, "show ip igmp interface json", isjson=True + ) + + if igmp_iface in show_ip_igmp_interface_json: + igmp_intf_json = show_ip_igmp_interface_json[igmp_iface] + # Verifying igmp interface + if igmp_intf_json["address"] != interface_ip: + errormsg = ( + "[DUT %s]: igmp interface ip is not correct " + "[FAILED]!! Expected : %s, Found : %s" + % (dut, igmp_intf_json["address"], interface_ip) + ) + return errormsg + + logger.info( + "[DUT %s]: igmp interface: %s, " "interface ip: %s" " [PASSED]!!", + dut, + igmp_iface, + interface_ip, + ) + else: + errormsg = ( + "[DUT %s]: igmp interface: %s " + "igmp interface ip: %s, is not present " + % (dut, igmp_iface, interface_ip) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +class McastTesterHelper(HostApplicationHelper): + def __init__(self, tgen=None): + self.script_path = os.path.join(CWD, "mcast-tester.py") + self.host_conn = {} + self.listen_sock = None + + # # Get a temporary file for socket path + # (fd, sock_path) = tempfile.mkstemp("-mct.sock", "tmp" + str(os.getpid())) + # os.close(fd) + # os.remove(sock_path) + # self.app_sock_path = sock_path + + # # Listen on unix socket + # logger.debug("%s: listening on socket %s", self, self.app_sock_path) + # self.listen_sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM, 0) + # self.listen_sock.settimeout(10) + # self.listen_sock.bind(self.app_sock_path) + # self.listen_sock.listen(10) + + python3_path = get_exec_path(["python3", "python"]) + super(McastTesterHelper, self).__init__( + tgen, + # [python3_path, self.script_path, self.app_sock_path] + [python3_path, self.script_path], + ) + + def __str__(self): + return "McastTesterHelper({})".format(self.script_path) + + def run_join(self, host, join_addrs, join_towards=None, join_intf=None): + """ + Join a UDP multicast group. + + One of join_towards or join_intf MUST be set. + + Parameters: + ----------- + * `host`: host from where IGMP join would be sent + * `join_addrs`: multicast address (or addresses) to join to + * `join_intf`: the interface to bind the join[s] to + * `join_towards`: router whos interface to bind the join[s] to + """ + if not isinstance(join_addrs, list) and not isinstance(join_addrs, tuple): + join_addrs = [join_addrs] + + if join_towards: + join_intf = frr_unicode( + self.tgen.json_topo["routers"][host]["links"][join_towards]["interface"] + ) + else: + assert join_intf + + for join in join_addrs: + self.run(host, [join, join_intf]) + + return True + + def run_traffic(self, host, send_to_addrs, bind_towards=None, bind_intf=None): + """ + Send UDP multicast traffic. + + One of bind_towards or bind_intf MUST be set. + + Parameters: + ----------- + * `host`: host to send traffic from + * `send_to_addrs`: multicast address (or addresses) to send traffic to + * `bind_towards`: Router who's interface the source ip address is got from + """ + if bind_towards: + bind_intf = frr_unicode( + self.tgen.json_topo["routers"][host]["links"][bind_towards]["interface"] + ) + else: + assert bind_intf + + if not isinstance(send_to_addrs, list) and not isinstance(send_to_addrs, tuple): + send_to_addrs = [send_to_addrs] + + for send_to in send_to_addrs: + self.run(host, ["--send=0.7", send_to, bind_intf]) + + return True + + +@retry(retry_timeout=62) +def verify_local_igmp_groups(tgen, dut, interface, group_addresses): + """ + Verify local IGMP groups are received from an intended interface + by running "show ip igmp join json" command + + Parameters + ---------- + * `tgen`: topogen object + * `dut`: device under test + * `interface`: interface, from which IGMP groups are configured + * `group_addresses`: IGMP group address + + Usage + ----- + dut = "r1" + interface = "r1-r0-eth0" + group_address = "225.1.1.1" + result = verify_local_igmp_groups(tgen, dut, interface, group_address) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + if dut not in tgen.routers(): + return False + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying local IGMP groups received:", dut) + show_ip_local_igmp_json = run_frr_cmd(rnode, "show ip igmp join json", isjson=True) + + if type(group_addresses) is not list: + group_addresses = [group_addresses] + + if interface not in show_ip_local_igmp_json: + + errormsg = ( + "[DUT %s]: Verifying local IGMP group received" + " from interface %s [FAILED]!! " % (dut, interface) + ) + return errormsg + + for grp_addr in group_addresses: + found = False + for index in show_ip_local_igmp_json[interface]["groups"]: + if index["group"] == grp_addr: + found = True + break + if not found: + errormsg = ( + "[DUT %s]: Verifying local IGMP group received" + " from interface %s [FAILED]!! " + " Expected: %s " % (dut, interface, grp_addr) + ) + return errormsg + + logger.info( + "[DUT %s]: Verifying local IGMP group %s received " + "from interface %s [PASSED]!! ", + dut, + grp_addr, + interface, + ) + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True + + +def verify_pim_interface_traffic(tgen, input_dict, return_stats=True, addr_type="ipv4"): + """ + Verify ip pim interface traffice by running + "show ip pim interface traffic" cli + + Parameters + ---------- + * `tgen`: topogen object + * `input_dict(dict)`: defines DUT, what and from which interfaces + traffic needs to be verified + * [optional]`addr_type`: specify address-family, default is ipv4 + + Usage + ----- + input_dict = { + "r1": { + "r1-r0-eth0": { + "helloRx": 0, + "helloTx": 1, + "joinRx": 0, + "joinTx": 0 + } + } + } + + result = verify_pim_interface_traffic(tgen, input_dict) + + Returns + ------- + errormsg(str) or True + """ + + logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + + output_dict = {} + for dut in input_dict.keys(): + if dut not in tgen.routers(): + continue + + rnode = tgen.routers()[dut] + + logger.info("[DUT: %s]: Verifying pim interface traffic", dut) + + if addr_type == "ipv4": + cmd = "show ip pim interface traffic json" + elif addr_type == "ipv6": + cmd = "show ipv6 pim interface traffic json" + + show_pim_intf_traffic_json = run_frr_cmd(rnode, cmd, isjson=True) + + output_dict[dut] = {} + for intf, data in input_dict[dut].items(): + interface_json = show_pim_intf_traffic_json[intf] + for state in data: + + # Verify Tx/Rx + if state in interface_json: + output_dict[dut][state] = interface_json[state] + else: + errormsg = ( + "[DUT %s]: %s is not present" + "for interface %s [FAILED]!! " % (dut, state, intf) + ) + return errormsg + + logger.debug("Exiting lib API: {}".format(sys._getframe().f_code.co_name)) + return True if return_stats == False else output_dict + + # def cleanup(self): + # super(McastTesterHelper, self).cleanup() + + # if not self.listen_sock: + # return + + # logger.debug("%s: closing listen socket %s", self, self.app_sock_path) + # self.listen_sock.close() + # self.listen_sock = None + + # if os.path.exists(self.app_sock_path): + # os.remove(self.app_sock_path) + + # def started_proc(self, host, p): + # logger.debug("%s: %s: accepting on socket %s", self, host, self.app_sock_path) + # try: + # conn = self.listen_sock.accept() + # return conn + # except Exception as error: + # logger.error("%s: %s: accept on socket failed: %s", self, host, error) + # if p.poll() is not None: + # logger.error("%s: %s: helper app quit: %s", self, host, comm_error(p)) + # raise + + # def stopping_proc(self, host, p, conn): + # logger.debug("%s: %s: closing socket %s", self, host, conn) + # conn[0].close() diff --git a/tests/topotests/lib/scapy_sendpkt.py b/tests/topotests/lib/scapy_sendpkt.py new file mode 100755 index 0000000..0bb6a72 --- /dev/null +++ b/tests/topotests/lib/scapy_sendpkt.py @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 eval: (blacken-mode 1) -*- +# +# July 29 2021, Christian Hopps <chopps@labn.net> +# +# Copyright (c) 2021, LabN Consulting, L.L.C. ("LabN") +# +# 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 re +import sys + +from scapy.all import conf, srp + +conf.verb = 0 + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("-i", "--interface", help="interface to send packet on.") + parser.add_argument("-I", "--imports", help="scapy symbols to import") + parser.add_argument( + "-t", "--timeout", type=float, default=2.0, help="timeout for reply receipts" + ) + parser.add_argument("pktdef", help="scapy packet definition to send") + args = parser.parse_args() + + if args.imports: + i = args.imports.replace("\n", "").strip() + if not re.match("[a-zA-Z0-9_ \t,]", i): + logging.critical('Invalid imports specified: "%s"', i) + sys.exit(1) + exec("from scapy.all import " + i, globals(), locals()) + + ans, unans = srp(eval(args.pktdef), iface=args.interface, timeout=args.timeout) + if not ans: + sys.exit(2) + for pkt in ans: + print(pkt.answer.show(dump=True)) + + +if __name__ == "__main__": + main() diff --git a/tests/topotests/lib/send_bsr_packet.py b/tests/topotests/lib/send_bsr_packet.py new file mode 100755 index 0000000..c226899 --- /dev/null +++ b/tests/topotests/lib/send_bsr_packet.py @@ -0,0 +1,58 @@ +# +# Copyright (c) 2019 by VMware, Inc. ("VMware") +# Used Copyright (c) 2018 by Network Device Education Foundation, Inc. +# ("NetDEF") in this file. +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import sys +import argparse +from scapy.all import Raw, sendp +import binascii + + +def send_packet(packet, iface, interval, count): + """ + Read BSR packet in Raw format and send it to specified interface + + Parameter: + --------- + * `packet` : BSR packet in raw format + * `interface` : Interface from which packet would be send + * `interval` : Interval between the packets + * `count` : Number of packets to be sent + """ + + data = binascii.a2b_hex(packet) + p = Raw(load=data) + p.show() + sendp(p, inter=int(interval), iface=iface, count=int(count)) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Send BSR Raw packet") + parser.add_argument("packet", help="Packet in raw format") + parser.add_argument("iface", help="Packet send to this ineterface") + parser.add_argument("--interval", help="Interval between packets", default=0) + parser.add_argument( + "--count", help="Number of times packet is sent repetitively", default=0 + ) + args = parser.parse_args() + + if not args.packet or not args.iface: + sys.exit(1) + + send_packet(args.packet, args.iface, args.interval, args.count) diff --git a/tests/topotests/lib/snmptest.py b/tests/topotests/lib/snmptest.py new file mode 100644 index 0000000..fe5ff28 --- /dev/null +++ b/tests/topotests/lib/snmptest.py @@ -0,0 +1,155 @@ +# +# topogen.py +# Library of helper functions for NetDEF Topology Tests +# +# Copyright (c) 2020 by Volta Networks +# +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +SNMP library to test snmp walks and gets + +Basic usage instructions: + +* define an SnmpTester class giving a router, address, community and version +* use test_oid or test_walk to check values in MIBS +* see tests/topotest/simple-snmp-test/test_simple_snmp.py for example +""" + +from lib.topolog import logger + + +class SnmpTester(object): + "A helper class for testing SNMP" + + def __init__(self, router, iface, community, version): + self.community = community + self.version = version + self.router = router + self.iface = iface + logger.info( + "created SNMP tester: SNMPv{0} community:{1}".format( + self.version, self.community + ) + ) + + def _snmp_config(self): + """ + Helper function to build a string with SNMP + configuration for commands. + """ + return "-v {0} -c {1} {2}".format(self.version, self.community, self.iface) + + @staticmethod + def _get_snmp_value(snmp_output): + tokens = snmp_output.strip().split() + + num_value_tokens = len(tokens) - 3 + + # this copes with the emptys string return + if num_value_tokens == 0: + return tokens[2] + + if num_value_tokens > 1: + output = "" + index = 3 + while index < len(tokens) - 1: + output += "{} ".format(tokens[index]) + index += 1 + output += "{}".format(tokens[index]) + return output + # third token is the value of the object + return tokens[3] + + @staticmethod + def _get_snmp_oid(snmp_output): + tokens = snmp_output.strip().split() + + # third token onwards is the value of the object + return tokens[0].split(".", 1)[1] + + @staticmethod + def _get_snmp_oid(snmp_output): + tokens = snmp_output.strip().split() + + # if len(tokens) > 5: + # return None + + # third token is the value of the object + return tokens[0].split(".", 1)[1] + + def _parse_multiline(self, snmp_output): + results = snmp_output.strip().split("\n") + + out_dict = {} + out_list = [] + for response in results: + out_dict[self._get_snmp_oid(response)] = self._get_snmp_value(response) + out_list.append(self._get_snmp_value(response)) + + return out_dict, out_list + + def get(self, oid): + cmd = "snmpget {0} {1}".format(self._snmp_config(), oid) + + result = self.router.cmd(cmd) + if "not found" in result: + return None + return self._get_snmp_value(result) + + def get_next(self, oid): + cmd = "snmpgetnext {0} {1}".format(self._snmp_config(), oid) + + result = self.router.cmd(cmd) + print("get_next: {}".format(result)) + if "not found" in result: + return None + return self._get_snmp_value(result) + + def walk(self, oid): + cmd = "snmpwalk {0} {1}".format(self._snmp_config(), oid) + + result = self.router.cmd(cmd) + return self._parse_multiline(result) + + def test_oid(self, oid, value): + print("oid: {}".format(self.get_next(oid))) + return self.get_next(oid) == value + + def test_oid_walk(self, oid, values, oids=None): + results_dict, results_list = self.walk(oid) + print("test_oid_walk: {} {}".format(oid, results_dict)) + if oids is not None: + index = 0 + for oid in oids: + # avoid key error for missing keys + if not oid in results_dict.keys(): + print("FAIL: missing oid key {}".format(oid)) + return False + if results_dict[oid] != values[index]: + print( + "FAIL{} {} |{}| == |{}|".format( + oid, index, results_dict[oid], values[index] + ) + ) + return False + index += 1 + return True + + # Return true if 'values' is a subset of 'results_list' + print("test {} == {}".format(results_list[: len(values)], values)) + return results_list[: len(values)] == values diff --git a/tests/topotests/lib/test/__init__.py b/tests/topotests/lib/test/__init__.py new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/tests/topotests/lib/test/__init__.py diff --git a/tests/topotests/lib/test/test_json.py b/tests/topotests/lib/test/test_json.py new file mode 100755 index 0000000..7b3c859 --- /dev/null +++ b/tests/topotests/lib/test/test_json.py @@ -0,0 +1,641 @@ +#!/usr/bin/env python + +# +# test_json.py +# Tests for library function: json_cmp(). +# +# Copyright (c) 2017 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Tests for the json_cmp() function. +""" + +import os +import sys +import pytest + +# Save the Current Working Directory to find lib files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../../")) + +# pylint: disable=C0413 +from lib.topotest import json_cmp + + +def test_json_intersect_true(): + "Test simple correct JSON intersections" + + dcomplete = { + "i1": "item1", + "i2": "item2", + "i3": "item3", + "i100": "item4", + } + + dsub1 = { + "i1": "item1", + "i3": "item3", + } + dsub2 = { + "i1": "item1", + "i2": "item2", + } + dsub3 = { + "i100": "item4", + "i2": "item2", + } + dsub4 = { + "i50": None, + "i100": "item4", + } + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + assert json_cmp(dcomplete, dsub4) is None + + +def test_json_intersect_false(): + "Test simple incorrect JSON intersections" + + dcomplete = { + "i1": "item1", + "i2": "item2", + "i3": "item3", + "i100": "item4", + } + + # Incorrect value for 'i1' + dsub1 = { + "i1": "item3", + "i3": "item3", + } + # Non-existing key 'i5' + dsub2 = { + "i1": "item1", + "i5": "item2", + } + # Key should not exist + dsub3 = { + "i100": None, + } + + assert json_cmp(dcomplete, dsub1) is not None + assert json_cmp(dcomplete, dsub2) is not None + assert json_cmp(dcomplete, dsub3) is not None + + +def test_json_intersect_multilevel_true(): + "Test multi level correct JSON intersections" + + dcomplete = { + "i1": "item1", + "i2": "item2", + "i3": { + "i100": "item100", + }, + "i4": { + "i41": { + "i411": "item411", + }, + "i42": { + "i421": "item421", + "i422": "item422", + }, + }, + } + + dsub1 = { + "i1": "item1", + "i3": { + "i100": "item100", + }, + "i10": None, + } + dsub2 = { + "i1": "item1", + "i2": "item2", + "i3": {}, + } + dsub3 = { + "i2": "item2", + "i4": { + "i41": { + "i411": "item411", + }, + "i42": { + "i422": "item422", + "i450": None, + }, + }, + } + dsub4 = { + "i2": "item2", + "i4": { + "i41": {}, + "i42": { + "i450": None, + }, + }, + } + dsub5 = { + "i2": "item2", + "i3": { + "i100": "item100", + }, + "i4": { + "i42": { + "i450": None, + } + }, + } + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + assert json_cmp(dcomplete, dsub4) is None + assert json_cmp(dcomplete, dsub5) is None + + +def test_json_intersect_multilevel_false(): + "Test multi level incorrect JSON intersections" + + dcomplete = { + "i1": "item1", + "i2": "item2", + "i3": { + "i100": "item100", + }, + "i4": { + "i41": { + "i411": "item411", + }, + "i42": { + "i421": "item421", + "i422": "item422", + }, + }, + } + + # Incorrect sub-level value + dsub1 = { + "i1": "item1", + "i3": { + "i100": "item00", + }, + "i10": None, + } + # Inexistent sub-level + dsub2 = { + "i1": "item1", + "i2": "item2", + "i3": None, + } + # Inexistent sub-level value + dsub3 = { + "i1": "item1", + "i3": { + "i100": None, + }, + } + # Inexistent sub-sub-level value + dsub4 = { + "i4": { + "i41": { + "i412": "item412", + }, + "i42": { + "i421": "item421", + }, + } + } + # Invalid sub-sub-level value + dsub5 = { + "i4": { + "i41": { + "i411": "item411", + }, + "i42": { + "i421": "item420000", + }, + } + } + # sub-sub-level should be value + dsub6 = { + "i4": { + "i41": { + "i411": "item411", + }, + "i42": "foobar", + } + } + + assert json_cmp(dcomplete, dsub1) is not None + assert json_cmp(dcomplete, dsub2) is not None + assert json_cmp(dcomplete, dsub3) is not None + assert json_cmp(dcomplete, dsub4) is not None + assert json_cmp(dcomplete, dsub5) is not None + assert json_cmp(dcomplete, dsub6) is not None + + +def test_json_with_list_sucess(): + "Test successful json comparisons that have lists." + + dcomplete = { + "list": [ + { + "i1": "item 1", + "i2": "item 2", + }, + { + "i10": "item 10", + }, + ], + "i100": "item 100", + } + + # Test list type + dsub1 = { + "list": [], + } + # Test list correct list items + dsub2 = { + "list": [ + { + "i1": "item 1", + }, + ], + "i100": "item 100", + } + # Test list correct list size + dsub3 = { + "list": [ + {}, + {}, + ], + } + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + + +def test_json_with_list_failure(): + "Test failed json comparisons that have lists." + + dcomplete = { + "list": [ + { + "i1": "item 1", + "i2": "item 2", + }, + { + "i10": "item 10", + }, + ], + "i100": "item 100", + } + + # Test list type + dsub1 = { + "list": {}, + } + # Test list incorrect list items + dsub2 = { + "list": [ + { + "i1": "item 2", + }, + ], + "i100": "item 100", + } + # Test list correct list size + dsub3 = { + "list": [ + {}, + {}, + {}, + ], + } + + assert json_cmp(dcomplete, dsub1) is not None + assert json_cmp(dcomplete, dsub2) is not None + assert json_cmp(dcomplete, dsub3) is not None + + +def test_json_list_start_success(): + "Test JSON encoded data that starts with a list that should succeed." + + dcomplete = [ + { + "id": 100, + "value": "abc", + }, + { + "id": 200, + "value": "abcd", + }, + { + "id": 300, + "value": "abcde", + }, + ] + + dsub1 = [ + { + "id": 100, + "value": "abc", + } + ] + + dsub2 = [ + { + "id": 100, + "value": "abc", + }, + { + "id": 200, + "value": "abcd", + }, + ] + + dsub3 = [ + { + "id": 300, + "value": "abcde", + } + ] + + dsub4 = [] + + dsub5 = [ + { + "id": 100, + } + ] + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + assert json_cmp(dcomplete, dsub4) is None + assert json_cmp(dcomplete, dsub5) is None + + +def test_json_list_start_failure(): + "Test JSON encoded data that starts with a list that should fail." + + dcomplete = [ + {"id": 100, "value": "abc"}, + {"id": 200, "value": "abcd"}, + {"id": 300, "value": "abcde"}, + ] + + dsub1 = [ + { + "id": 100, + "value": "abcd", + } + ] + + dsub2 = [ + { + "id": 100, + "value": "abc", + }, + { + "id": 200, + "value": "abc", + }, + ] + + dsub3 = [ + { + "id": 100, + "value": "abc", + }, + { + "id": 350, + "value": "abcde", + }, + ] + + dsub4 = [ + { + "value": "abcx", + }, + { + "id": 300, + "value": "abcde", + }, + ] + + assert json_cmp(dcomplete, dsub1) is not None + assert json_cmp(dcomplete, dsub2) is not None + assert json_cmp(dcomplete, dsub3) is not None + assert json_cmp(dcomplete, dsub4) is not None + + +def test_json_list_ordered(): + "Test JSON encoded data that should be ordered using the '__ordered__' tag." + + dcomplete = [ + {"id": 1, "value": "abc"}, + "some string", + 123, + ] + + dsub1 = [ + "__ordered__", + "some string", + {"id": 1, "value": "abc"}, + 123, + ] + + assert json_cmp(dcomplete, dsub1) is not None + + +def test_json_list_exact_matching(): + "Test JSON array on exact matching using the 'exact' parameter." + + dcomplete = [ + {"id": 1, "value": "abc"}, + "some string", + 123, + [1, 2, 3], + ] + + dsub1 = [ + "some string", + {"id": 1, "value": "abc"}, + 123, + [1, 2, 3], + ] + + dsub2 = [ + {"id": 1}, + "some string", + 123, + [1, 2, 3], + ] + + dsub3 = [ + {"id": 1, "value": "abc"}, + "some string", + 123, + [1, 3, 2], + ] + + assert json_cmp(dcomplete, dsub1, exact=True) is not None + assert json_cmp(dcomplete, dsub2, exact=True) is not None + + +def test_json_object_exact_matching(): + "Test JSON object on exact matching using the 'exact' parameter." + + dcomplete = { + "a": {"id": 1, "value": "abc"}, + "b": "some string", + "c": 123, + "d": [1, 2, 3], + } + + dsub1 = { + "a": {"id": 1, "value": "abc"}, + "c": 123, + "d": [1, 2, 3], + } + + dsub2 = { + "a": {"id": 1}, + "b": "some string", + "c": 123, + "d": [1, 2, 3], + } + + dsub3 = { + "a": {"id": 1, "value": "abc"}, + "b": "some string", + "c": 123, + "d": [1, 3], + } + + assert json_cmp(dcomplete, dsub1, exact=True) is not None + assert json_cmp(dcomplete, dsub2, exact=True) is not None + assert json_cmp(dcomplete, dsub3, exact=True) is not None + + +def test_json_list_asterisk_matching(): + "Test JSON array elements on matching '*' as a placeholder for arbitrary data." + + dcomplete = [ + {"id": 1, "value": "abc"}, + "some string", + 123, + [1, 2, 3], + ] + + dsub1 = [ + "*", + "some string", + 123, + [1, 2, 3], + ] + + dsub2 = [ + {"id": "*", "value": "abc"}, + "some string", + 123, + [1, 2, 3], + ] + + dsub3 = [ + {"id": 1, "value": "abc"}, + "some string", + 123, + [1, "*", 3], + ] + + dsub4 = [ + "*", + "some string", + "*", + [1, 2, 3], + ] + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + assert json_cmp(dcomplete, dsub4) is None + + +def test_json_object_asterisk_matching(): + "Test JSON object value elements on matching '*' as a placeholder for arbitrary data." + + dcomplete = { + "a": {"id": 1, "value": "abc"}, + "b": "some string", + "c": 123, + "d": [1, 2, 3], + } + + dsub1 = { + "a": "*", + "b": "some string", + "c": 123, + "d": [1, 2, 3], + } + + dsub2 = { + "a": {"id": 1, "value": "abc"}, + "b": "some string", + "c": 123, + "d": [1, "*", 3], + } + + dsub3 = { + "a": {"id": "*", "value": "abc"}, + "b": "some string", + "c": 123, + "d": [1, 2, 3], + } + + dsub4 = { + "a": "*", + "b": "some string", + "c": "*", + "d": [1, 2, 3], + } + + assert json_cmp(dcomplete, dsub1) is None + assert json_cmp(dcomplete, dsub2) is None + assert json_cmp(dcomplete, dsub3) is None + assert json_cmp(dcomplete, dsub4) is None + + +def test_json_list_nested_with_objects(): + + dcomplete = [{"key": 1, "list": [123]}, {"key": 2, "list": [123]}] + + dsub1 = [{"key": 2, "list": [123]}, {"key": 1, "list": [123]}] + + assert json_cmp(dcomplete, dsub1) is None + + +if __name__ == "__main__": + sys.exit(pytest.main()) diff --git a/tests/topotests/lib/test/test_run_and_expect.py b/tests/topotests/lib/test/test_run_and_expect.py new file mode 100755 index 0000000..d65d5ba --- /dev/null +++ b/tests/topotests/lib/test/test_run_and_expect.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python + +# +# test_run_and_expect.py +# Tests for library function: run_and_expect(_type)(). +# +# Copyright (c) 2019 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Tests for the `run_and_expect(_type)()` functions. +""" + +import os +import sys +import pytest + +# Save the Current Working Directory to find lib files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../../")) + +# pylint: disable=C0413 +from lib.topotest import run_and_expect_type + + +def test_run_and_expect_type(): + "Test basic `run_and_expect_type` functionality." + + def return_true(): + "Test function that returns `True`." + return True + + # Test value success. + success, value = run_and_expect_type( + return_true, bool, count=1, wait=0, avalue=True + ) + assert success is True + assert value is True + + # Test value failure. + success, value = run_and_expect_type( + return_true, bool, count=1, wait=0, avalue=False + ) + assert success is False + assert value is True + + # Test type success. + success, value = run_and_expect_type(return_true, bool, count=1, wait=0) + assert success is True + assert value is True + + # Test type failure. + success, value = run_and_expect_type(return_true, str, count=1, wait=0) + assert success is False + assert value is True + + # Test type failure, return correct type. + success, value = run_and_expect_type(return_true, str, count=1, wait=0, avalue=True) + assert success is False + assert value is True + + +if __name__ == "__main__": + sys.exit(pytest.main()) diff --git a/tests/topotests/lib/test/test_version.py b/tests/topotests/lib/test/test_version.py new file mode 100755 index 0000000..7c2df00 --- /dev/null +++ b/tests/topotests/lib/test/test_version.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python + +# +# test_version.py +# Tests for library function: version_cmp(). +# +# Copyright (c) 2017 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Tests for the version_cmp() function. +""" + +import os +import sys +import pytest + +# Save the Current Working Directory to find lib files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../../")) + +# pylint: disable=C0413 +from lib.topotest import version_cmp + + +def test_valid_versions(): + "Test valid version compare results" + + curver = "3.0" + samever = "3" + oldver = "2.0" + newver = "3.0.1" + newerver = "3.0.11" + vercustom = "3.0-dev" + verysmallinc = "3.0.0.0.0.0.0.1" + + assert version_cmp(curver, oldver) == 1 + assert version_cmp(curver, newver) == -1 + assert version_cmp(curver, curver) == 0 + assert version_cmp(curver, newerver) == -1 + assert version_cmp(newver, newerver) == -1 + assert version_cmp(curver, samever) == 0 + assert version_cmp(curver, vercustom) == 0 + assert version_cmp(vercustom, vercustom) == 0 + assert version_cmp(vercustom, oldver) == 1 + assert version_cmp(vercustom, newver) == -1 + assert version_cmp(vercustom, samever) == 0 + assert version_cmp(curver, verysmallinc) == -1 + assert version_cmp(newver, verysmallinc) == 1 + assert version_cmp(verysmallinc, verysmallinc) == 0 + assert version_cmp(vercustom, verysmallinc) == -1 + + +def test_invalid_versions(): + "Test invalid version strings" + + curver = "3.0" + badver1 = ".1" + badver2 = "-1.0" + badver3 = "." + badver4 = "3.-0.3" + + with pytest.raises(ValueError): + assert version_cmp(curver, badver1) + assert version_cmp(curver, badver2) + assert version_cmp(curver, badver3) + assert version_cmp(curver, badver4) + + +def test_regression_1(): + """ + Test regression on the following type of comparison: '3.0.2' > '3' + Expected result is 1. + """ + assert version_cmp("3.0.2", "3") == 1 diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py new file mode 100644 index 0000000..04712ed --- /dev/null +++ b/tests/topotests/lib/topogen.py @@ -0,0 +1,1338 @@ +# +# topogen.py +# Library of helper functions for NetDEF Topology Tests +# +# Copyright (c) 2017 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Topogen (Topology Generator) is an abstraction around Topotest and Mininet to +help reduce boilerplate code and provide a stable interface to build topology +tests on. + +Basic usage instructions: + +* Define a Topology class with a build method using mininet.topo.Topo. + See examples/test_template.py. +* Use Topogen inside the build() method with get_topogen. + e.g. get_topogen(self). +* Start up your topology with: Topogen(YourTopology) +* Initialize the Mininet with your topology with: tgen.start_topology() +* Configure your routers/hosts and start them +* Run your tests / mininet cli. +* After running stop Mininet with: tgen.stop_topology() +""" + +import grp +import inspect +import json +import logging +import os +import platform +import pwd +import re +import subprocess +import sys +from collections import OrderedDict + +if sys.version_info[0] > 2: + import configparser +else: + import ConfigParser as configparser + +import lib.topolog as topolog +from lib.micronet import Commander +from lib.micronet_compat import Mininet +from lib.topolog import logger +from lib.topotest import g_extra_config + +from lib import topotest + +CWD = os.path.dirname(os.path.realpath(__file__)) + +# pylint: disable=C0103 +# Global Topogen variable. This is being used to keep the Topogen available on +# all test functions without declaring a test local variable. +global_tgen = None + + +def get_topogen(topo=None): + """ + Helper function to retrieve Topogen. Must be called with `topo` when called + inside the build() method of Topology class. + """ + if topo is not None: + global_tgen.topo = topo + return global_tgen + + +def set_topogen(tgen): + "Helper function to set Topogen" + # pylint: disable=W0603 + global global_tgen + global_tgen = tgen + + +def is_string(value): + """Return True if value is a string.""" + try: + return isinstance(value, basestring) # type: ignore + except NameError: + return isinstance(value, str) + + +def get_exabgp_cmd(commander=None): + """Return the command to use for ExaBGP version < 4.""" + + if commander is None: + commander = Commander("topogen") + + def exacmd_version_ok(exacmd): + logger.debug("checking %s for exabgp < version 4", exacmd) + _, stdout, _ = commander.cmd_status(exacmd + " -v", warn=False) + m = re.search(r"ExaBGP\s*:\s*((\d+)\.(\d+)(?:\.(\d+))?)", stdout) + if not m: + return False + version = m.group(1) + if topotest.version_cmp(version, "4") >= 0: + logging.debug("found exabgp version >= 4 in %s will keep looking", exacmd) + return False + logger.info("Using ExaBGP version %s in %s", version, exacmd) + return True + + exacmd = commander.get_exec_path("exabgp") + if exacmd and exacmd_version_ok(exacmd): + return exacmd + py2_path = commander.get_exec_path("python2") + if py2_path: + exacmd = py2_path + " -m exabgp" + if exacmd_version_ok(exacmd): + return exacmd + py2_path = commander.get_exec_path("python") + if py2_path: + exacmd = py2_path + " -m exabgp" + if exacmd_version_ok(exacmd): + return exacmd + return None + + +# +# Main class: topology builder +# + +# Topogen configuration defaults +tgen_defaults = { + "verbosity": "info", + "frrdir": "/usr/lib/frr", + "routertype": "frr", + "memleak_path": "", +} + + +class Topogen(object): + "A topology test builder helper." + + CONFIG_SECTION = "topogen" + + def __init__(self, topodef, modname="unnamed"): + """ + Topogen initialization function, takes the following arguments: + * `cls`: OLD:uthe topology class that is child of mininet.topo or a build function. + * `topodef`: A dictionary defining the topology, a filename of a json file, or a + function that will do the same + * `modname`: module name must be a unique name to identify logs later. + """ + self.config = None + self.net = None + self.gears = {} + self.routern = 1 + self.switchn = 1 + self.modname = modname + self.errorsd = {} + self.errors = "" + self.peern = 1 + self.cfg_gen = 0 + self.exabgp_cmd = None + self._init_topo(topodef) + + logger.info("loading topology: {}".format(self.modname)) + + # @staticmethod + # def _mininet_reset(): + # "Reset the mininet environment" + # # Clean up the mininet environment + # os.system("mn -c > /dev/null 2>&1") + + def __str__(self): + return "Topogen()" + + def _init_topo(self, topodef): + """ + Initialize the topogily provided by the user. The user topology class + must call get_topogen() during build() to get the topogen object. + """ + # Set the global variable so the test cases can access it anywhere + set_topogen(self) + + # Increase host based limits + topotest.fix_host_limits() + + # Test for MPLS Kernel modules available + self.hasmpls = False + if not topotest.module_present("mpls-router"): + logger.info("MPLS tests will not run (missing mpls-router kernel module)") + elif not topotest.module_present("mpls-iptunnel"): + logger.info("MPLS tests will not run (missing mpls-iptunnel kernel module)") + else: + self.hasmpls = True + + # Load the default topology configurations + self._load_config() + + # Create new log directory + self.logdir = topotest.get_logs_path(g_extra_config["rundir"]) + subprocess.check_call( + "mkdir -p {0} && chmod 1777 {0}".format(self.logdir), shell=True + ) + try: + routertype = self.config.get(self.CONFIG_SECTION, "routertype") + # Only allow group, if it exist. + gid = grp.getgrnam(routertype)[2] + os.chown(self.logdir, 0, gid) + os.chmod(self.logdir, 0o775) + except KeyError: + # Allow anyone, but set the sticky bit to avoid file deletions + os.chmod(self.logdir, 0o1777) + + # Remove old twisty way of creating sub-classed topology object which has it's + # build method invoked which calls Topogen methods which then call Topo methods + # to create a topology within the Topo object, which is then used by + # Mininet(Micronet) to build the actual topology. + assert not inspect.isclass(topodef) + + self.net = Mininet(controller=None) + + # New direct way: Either a dictionary defines the topology or a build function + # is supplied, or a json filename all of which build the topology by calling + # Topogen methods which call Mininet(Micronet) methods to create the actual + # topology. + if not inspect.isclass(topodef): + if callable(topodef): + topodef(self) + self.net.configure_hosts() + elif is_string(topodef): + # topojson imports topogen in one function too, + # switch away from this use here to the topojson + # fixutre and remove this case + from lib.topojson import build_topo_from_json + + with open(topodef, "r") as topof: + self.json_topo = json.load(topof) + build_topo_from_json(self, self.json_topo) + self.net.configure_hosts() + elif topodef: + self.add_topology_from_dict(topodef) + + def add_topology_from_dict(self, topodef): + + keylist = ( + topodef.keys() + if isinstance(topodef, OrderedDict) + else sorted(topodef.keys()) + ) + # --------------------------- + # Create all referenced hosts + # --------------------------- + for oname in keylist: + tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname] + for e in tup: + desc = e.split(":") + name = desc[0] + if name not in self.gears: + logging.debug("Adding router: %s", name) + self.add_router(name) + + # ------------------------------ + # Create all referenced switches + # ------------------------------ + for oname in keylist: + if oname is not None and oname not in self.gears: + logging.debug("Adding switch: %s", oname) + self.add_switch(oname) + + # ---------------- + # Create all links + # ---------------- + for oname in keylist: + if oname is None: + continue + tup = (topodef[oname],) if is_string(topodef[oname]) else topodef[oname] + for e in tup: + desc = e.split(":") + name = desc[0] + ifname = desc[1] if len(desc) > 1 else None + sifname = desc[2] if len(desc) > 2 else None + self.add_link(self.gears[oname], self.gears[name], sifname, ifname) + + self.net.configure_hosts() + + def _load_config(self): + """ + Loads the configuration file `pytest.ini` located at the root dir of + topotests. + """ + self.config = configparser.ConfigParser(tgen_defaults) + pytestini_path = os.path.join(CWD, "../pytest.ini") + self.config.read(pytestini_path) + + def add_router(self, name=None, cls=None, **params): + """ + Adds a new router to the topology. This function has the following + options: + * `name`: (optional) select the router name + * `daemondir`: (optional) custom daemon binary directory + * `routertype`: (optional) `frr` + Returns a TopoRouter. + """ + if cls is None: + cls = topotest.Router + if name is None: + name = "r{}".format(self.routern) + if name in self.gears: + raise KeyError("router already exists") + + params["frrdir"] = self.config.get(self.CONFIG_SECTION, "frrdir") + params["memleak_path"] = self.config.get(self.CONFIG_SECTION, "memleak_path") + if "routertype" not in params: + params["routertype"] = self.config.get(self.CONFIG_SECTION, "routertype") + + self.gears[name] = TopoRouter(self, cls, name, **params) + self.routern += 1 + return self.gears[name] + + def add_switch(self, name=None): + """ + Adds a new switch to the topology. This function has the following + options: + name: (optional) select the switch name + Returns the switch name and number. + """ + if name is None: + name = "s{}".format(self.switchn) + if name in self.gears: + raise KeyError("switch already exists") + + self.gears[name] = TopoSwitch(self, name) + self.switchn += 1 + return self.gears[name] + + def add_exabgp_peer(self, name, ip, defaultRoute): + """ + Adds a new ExaBGP peer to the topology. This function has the following + parameters: + * `ip`: the peer address (e.g. '1.2.3.4/24') + * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1') + """ + if name is None: + name = "peer{}".format(self.peern) + if name in self.gears: + raise KeyError("exabgp peer already exists") + + self.gears[name] = TopoExaBGP(self, name, ip=ip, defaultRoute=defaultRoute) + self.peern += 1 + return self.gears[name] + + def add_host(self, name, ip, defaultRoute): + """ + Adds a new host to the topology. This function has the following + parameters: + * `ip`: the peer address (e.g. '1.2.3.4/24') + * `defaultRoute`: the peer default route (e.g. 'via 1.2.3.1') + """ + if name is None: + name = "host{}".format(self.peern) + if name in self.gears: + raise KeyError("host already exists") + + self.gears[name] = TopoHost(self, name, ip=ip, defaultRoute=defaultRoute) + self.peern += 1 + return self.gears[name] + + def add_link(self, node1, node2, ifname1=None, ifname2=None): + """ + Creates a connection between node1 and node2. The nodes can be the + following: + * TopoGear + * TopoRouter + * TopoSwitch + """ + if not isinstance(node1, TopoGear): + raise ValueError("invalid node1 type") + if not isinstance(node2, TopoGear): + raise ValueError("invalid node2 type") + + if ifname1 is None: + ifname1 = node1.new_link() + if ifname2 is None: + ifname2 = node2.new_link() + + node1.register_link(ifname1, node2, ifname2) + node2.register_link(ifname2, node1, ifname1) + self.net.add_link(node1.name, node2.name, ifname1, ifname2) + + def get_gears(self, geartype): + """ + Returns a dictionary of all gears of type `geartype`. + + Normal usage: + * Dictionary iteration: + ```py + tgen = get_topogen() + router_dict = tgen.get_gears(TopoRouter) + for router_name, router in router_dict.items(): + # Do stuff + ``` + * List iteration: + ```py + tgen = get_topogen() + peer_list = tgen.get_gears(TopoExaBGP).values() + for peer in peer_list: + # Do stuff + ``` + """ + return dict( + (name, gear) + for name, gear in self.gears.items() + if isinstance(gear, geartype) + ) + + def routers(self): + """ + Returns the router dictionary (key is the router name and value is the + router object itself). + """ + return self.get_gears(TopoRouter) + + def exabgp_peers(self): + """ + Returns the exabgp peer dictionary (key is the peer name and value is + the peer object itself). + """ + return self.get_gears(TopoExaBGP) + + def start_topology(self): + """Starts the topology class.""" + logger.info("starting topology: {}".format(self.modname)) + self.net.start() + + def start_router(self, router=None): + """ + Call the router startRouter method. + If no router is specified it is called for all registered routers. + """ + if router is None: + # pylint: disable=r1704 + # XXX should be hosts? + for _, router in self.routers().items(): + router.start() + else: + if isinstance(router, str): + router = self.gears[router] + + router.start() + + def stop_topology(self): + """ + Stops the network topology. This function will call the stop() function + of all gears before calling the mininet stop function, so they can have + their oportunity to do a graceful shutdown. stop() is called twice. The + first is a simple kill with no sleep, the second will sleep if not + killed and try with a different signal. + """ + logger.info("stopping topology: {}".format(self.modname)) + errors = "" + for gear in self.gears.values(): + errors += gear.stop() + if len(errors) > 0: + logger.error( + "Errors found post shutdown - details follow: {}".format(errors) + ) + + self.net.stop() + + def get_exabgp_cmd(self): + if not self.exabgp_cmd: + self.exabgp_cmd = get_exabgp_cmd(self.net) + return self.exabgp_cmd + + def cli(self): + """ + Interrupt the test and call the command line interface for manual + inspection. Should be only used on non production code. + """ + self.net.cli() + + mininet_cli = cli + + def is_memleak_enabled(self): + "Returns `True` if memory leak report is enable, otherwise `False`." + # On router failure we can't run the memory leak test + if self.routers_have_failure(): + return False + + memleak_file = os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.config.get( + self.CONFIG_SECTION, "memleak_path" + ) + if memleak_file == "" or memleak_file == None: + return False + return True + + def report_memory_leaks(self, testname=None): + "Run memory leak test and reports." + if not self.is_memleak_enabled(): + return + + # If no name was specified, use the test module name + if testname is None: + testname = self.modname + + router_list = self.routers().values() + for router in router_list: + router.report_memory_leaks(self.modname) + + def set_error(self, message, code=None): + "Sets an error message and signal other tests to skip." + logger.info(message) + + # If no code is defined use a sequential number + if code is None: + code = len(self.errorsd) + + self.errorsd[code] = message + self.errors += "\n{}: {}".format(code, message) + + def has_errors(self): + "Returns whether errors exist or not." + return len(self.errorsd) > 0 + + def routers_have_failure(self): + "Runs an assertion to make sure that all routers are running." + if self.has_errors(): + return True + + errors = "" + router_list = self.routers().values() + for router in router_list: + result = router.check_router_running() + if result != "": + errors += result + "\n" + + if errors != "": + self.set_error(errors, "router_error") + assert False, errors + return True + return False + + +# +# Topology gears (equipment) +# + + +class TopoGear(object): + "Abstract class for type checking" + + def __init__(self, tgen, name, **params): + self.tgen = tgen + self.name = name + self.params = params + self.links = {} + self.linkn = 0 + + # Would be nice for this to point at the gears log directory rather than the + # test's. + self.logdir = tgen.logdir + self.gearlogdir = None + + def __str__(self): + links = "" + for myif, dest in self.links.items(): + _, destif = dest + if links != "": + links += "," + links += '"{}"<->"{}"'.format(myif, destif) + + return 'TopoGear<name="{}",links=[{}]>'.format(self.name, links) + + @property + def net(self): + return self.tgen.net[self.name] + + def start(self): + "Basic start function that just reports equipment start" + logger.info('starting "{}"'.format(self.name)) + + def stop(self, wait=True, assertOnError=True): + "Basic stop function that just reports equipment stop" + logger.info('"{}" base stop called'.format(self.name)) + return "" + + def cmd(self, command, **kwargs): + """ + Runs the provided command string in the router and returns a string + with the response. + """ + return self.net.cmd_legacy(command, **kwargs) + + def cmd_raises(self, command, **kwargs): + """ + Runs the provided command string in the router and returns a string + with the response. Raise an exception on any error. + """ + return self.net.cmd_raises(command, **kwargs) + + run = cmd + + def popen(self, *params, **kwargs): + """ + Creates a pipe with the given command. Same args as python Popen. + If `command` is a string then will be invoked with shell, otherwise + `command` is a list and will be invoked w/o shell. Returns a popen object. + """ + return self.net.popen(*params, **kwargs) + + def add_link(self, node, myif=None, nodeif=None): + """ + Creates a link (connection) between myself and the specified node. + Interfaces name can be speficied with: + myif: the interface name that will be created in this node + nodeif: the target interface name that will be created on the remote node. + """ + self.tgen.add_link(self, node, myif, nodeif) + + def link_enable(self, myif, enabled=True, netns=None): + """ + Set this node interface administrative state. + myif: this node interface name + enabled: whether we should enable or disable the interface + """ + if myif not in self.links.keys(): + raise KeyError("interface doesn't exists") + + if enabled is True: + operation = "up" + else: + operation = "down" + + logger.info( + 'setting node "{}" link "{}" to state "{}"'.format( + self.name, myif, operation + ) + ) + extract = "" + if netns is not None: + extract = "ip netns exec {} ".format(netns) + + return self.run("{}ip link set dev {} {}".format(extract, myif, operation)) + + def peer_link_enable(self, myif, enabled=True, netns=None): + """ + Set the peer interface administrative state. + myif: this node interface name + enabled: whether we should enable or disable the interface + + NOTE: this is used to simulate a link down on this node, since when the + peer disables their interface our interface status changes to no link. + """ + if myif not in self.links.keys(): + raise KeyError("interface doesn't exists") + + node, nodeif = self.links[myif] + node.link_enable(nodeif, enabled, netns) + + def new_link(self): + """ + Generates a new unique link name. + + NOTE: This function should only be called by Topogen. + """ + ifname = "{}-eth{}".format(self.name, self.linkn) + self.linkn += 1 + return ifname + + def register_link(self, myif, node, nodeif): + """ + Register link between this node interface and outside node. + + NOTE: This function should only be called by Topogen. + """ + if myif in self.links.keys(): + raise KeyError("interface already exists") + + self.links[myif] = (node, nodeif) + + def _setup_tmpdir(self): + topotest.setup_node_tmpdir(self.logdir, self.name) + self.gearlogdir = "{}/{}".format(self.logdir, self.name) + return "{}/{}.log".format(self.logdir, self.name) + + +class TopoRouter(TopoGear): + """ + Router abstraction. + """ + + # The default required directories by FRR + PRIVATE_DIRS = [ + "/etc/frr", + "/etc/snmp", + "/var/run/frr", + "/var/log", + ] + + # Router Daemon enumeration definition. + RD_FRR = 0 # not a daemon, but use to setup unified configs + RD_ZEBRA = 1 + RD_RIP = 2 + RD_RIPNG = 3 + RD_OSPF = 4 + RD_OSPF6 = 5 + RD_ISIS = 6 + RD_BGP = 7 + RD_LDP = 8 + RD_PIM = 9 + RD_EIGRP = 10 + RD_NHRP = 11 + RD_STATIC = 12 + RD_BFD = 13 + RD_SHARP = 14 + RD_BABEL = 15 + RD_PBRD = 16 + RD_PATH = 17 + RD_SNMP = 18 + RD_PIM6 = 19 + RD = { + RD_FRR: "frr", + RD_ZEBRA: "zebra", + RD_RIP: "ripd", + RD_RIPNG: "ripngd", + RD_OSPF: "ospfd", + RD_OSPF6: "ospf6d", + RD_ISIS: "isisd", + RD_BGP: "bgpd", + RD_PIM: "pimd", + RD_PIM6: "pim6d", + RD_LDP: "ldpd", + RD_EIGRP: "eigrpd", + RD_NHRP: "nhrpd", + RD_STATIC: "staticd", + RD_BFD: "bfdd", + RD_SHARP: "sharpd", + RD_BABEL: "babeld", + RD_PBRD: "pbrd", + RD_PATH: "pathd", + RD_SNMP: "snmpd", + } + + def __init__(self, tgen, cls, name, **params): + """ + The constructor has the following parameters: + * tgen: Topogen object + * cls: router class that will be used to instantiate + * name: router name + * daemondir: daemon binary directory + * routertype: 'frr' + """ + super(TopoRouter, self).__init__(tgen, name, **params) + self.routertype = params.get("routertype", "frr") + if "privateDirs" not in params: + params["privateDirs"] = self.PRIVATE_DIRS + + # Propagate the router log directory + logfile = self._setup_tmpdir() + params["logdir"] = self.logdir + + self.logger = topolog.get_logger(name, log_level="debug", target=logfile) + params["logger"] = self.logger + tgen.net.add_host(self.name, cls=cls, **params) + topotest.fix_netns_limits(tgen.net[name]) + + # Mount gear log directory on a common path + self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir") + + # Ensure pid file + with open(os.path.join(self.logdir, self.name + ".pid"), "w") as f: + f.write(str(self.net.pid) + "\n") + + def __str__(self): + gear = super(TopoRouter, self).__str__() + gear += " TopoRouter<>" + return gear + + def check_capability(self, daemon, param): + """ + Checks a capability daemon against an argument option + Return True if capability available. False otherwise + """ + daemonstr = self.RD.get(daemon) + self.logger.info('check capability {} for "{}"'.format(param, daemonstr)) + return self.net.checkCapability(daemonstr, param) + + def load_frr_config(self, source, daemons=None): + """ + Loads the unified configuration file source + Start the daemons in the list + If daemons is None, try to infer daemons from the config file + """ + self.load_config(self.RD_FRR, source) + if not daemons: + # Always add zebra + self.load_config(self.RD_ZEBRA) + for daemon in self.RD: + # This will not work for all daemons + daemonstr = self.RD.get(daemon).rstrip("d") + if daemonstr == "pim": + grep_cmd = "grep 'ip {}' {}".format(daemonstr, source) + else: + grep_cmd = "grep 'router {}' {}".format(daemonstr, source) + result = self.run(grep_cmd).strip() + if result: + self.load_config(daemon) + else: + for daemon in daemons: + self.load_config(daemon) + + def load_config(self, daemon, source=None, param=None): + """Loads daemon configuration from the specified source + Possible daemon values are: TopoRouter.RD_ZEBRA, TopoRouter.RD_RIP, + TopoRouter.RD_RIPNG, TopoRouter.RD_OSPF, TopoRouter.RD_OSPF6, + TopoRouter.RD_ISIS, TopoRouter.RD_BGP, TopoRouter.RD_LDP, + TopoRouter.RD_PIM, TopoRouter.RD_PIM6, TopoRouter.RD_PBR, + TopoRouter.RD_SNMP. + + Possible `source` values are `None` for an empty config file, a path name which is + used directly, or a file name with no path components which is first looked for + directly and then looked for under a sub-directory named after router. + + This API unfortunately allows for source to not exist for any and + all routers. + """ + daemonstr = self.RD.get(daemon) + self.logger.info('loading "{}" configuration: {}'.format(daemonstr, source)) + self.net.loadConf(daemonstr, source, param) + + def check_router_running(self): + """ + Run a series of checks and returns a status string. + """ + self.logger.info("checking if daemons are running") + return self.net.checkRouterRunning() + + def start(self): + """ + Start router: + * Load modules + * Clean up files + * Configure interfaces + * Start daemons (e.g. FRR) + * Configure daemon logging files + """ + + nrouter = self.net + result = nrouter.startRouter(self.tgen) + + # Enable command logging + + # Enable all daemon command logging, logging files + # and set them to the start dir. + for daemon, enabled in nrouter.daemons.items(): + if enabled and daemon != "snmpd": + self.vtysh_cmd( + "\n".join( + [ + "clear log cmdline-targets", + "conf t", + "log file {}.log debug".format(daemon), + "log commands", + "log timestamp precision 3", + ] + ), + daemon=daemon, + ) + + if result != "": + self.tgen.set_error(result) + elif nrouter.daemons["ldpd"] == 1 or nrouter.daemons["pathd"] == 1: + # Enable MPLS processing on all interfaces. + for interface in self.links: + topotest.sysctl_assure( + nrouter, "net.mpls.conf.{}.input".format(interface), 1 + ) + + return result + + def stop(self): + """ + Stop router cleanly: + * Signal daemons twice, once with SIGTERM, then with SIGKILL. + """ + self.logger.debug("stopping (no assert)") + return self.net.stopRouter(False) + + def startDaemons(self, daemons): + """ + Start Daemons: to start specific daemon(user defined daemon only) + * Start daemons (e.g. FRR) + * Configure daemon logging files + """ + self.logger.debug("starting") + nrouter = self.net + result = nrouter.startRouterDaemons(daemons) + + if daemons is None: + daemons = nrouter.daemons.keys() + + # Enable all daemon command logging, logging files + # and set them to the start dir. + for daemon in daemons: + enabled = nrouter.daemons[daemon] + if enabled and daemon != "snmpd": + self.vtysh_cmd( + "\n".join( + [ + "clear log cmdline-targets", + "conf t", + "log file {}.log debug".format(daemon), + "log commands", + "log timestamp precision 3", + ] + ), + daemon=daemon, + ) + + if result != "": + self.tgen.set_error(result) + + return result + + def killDaemons(self, daemons, wait=True, assertOnError=True): + """ + Kill specific daemon(user defined daemon only) + forcefully using SIGKILL + """ + self.logger.debug("Killing daemons using SIGKILL..") + return self.net.killRouterDaemons(daemons, wait, assertOnError) + + def vtysh_cmd(self, command, isjson=False, daemon=None): + """ + Runs the provided command string in the vty shell and returns a string + with the response. + + This function also accepts multiple commands, but this mode does not + return output for each command. See vtysh_multicmd() for more details. + """ + # Detect multi line commands + if command.find("\n") != -1: + return self.vtysh_multicmd(command, daemon=daemon) + + dparam = "" + if daemon is not None: + dparam += "-d {}".format(daemon) + + vtysh_command = 'vtysh {} -c "{}" 2>/dev/null'.format(dparam, command) + + self.logger.info('vtysh command => "{}"'.format(command)) + output = self.run(vtysh_command) + + dbgout = output.strip() + if dbgout: + if "\n" in dbgout: + dbgout = dbgout.replace("\n", "\n\t") + self.logger.info("vtysh result:\n\t{}".format(dbgout)) + else: + self.logger.info('vtysh result: "{}"'.format(dbgout)) + + if isjson is False: + return output + + try: + return json.loads(output) + except ValueError as error: + logger.warning( + "vtysh_cmd: %s: failed to convert json output: %s: %s", + self.name, + str(output), + str(error), + ) + return {} + + def vtysh_multicmd(self, commands, pretty_output=True, daemon=None): + """ + Runs the provided commands in the vty shell and return the result of + execution. + + pretty_output: defines how the return value will be presented. When + True it will show the command as they were executed in the vty shell, + otherwise it will only show lines that failed. + """ + # Prepare the temporary file that will hold the commands + fname = topotest.get_file(commands) + + dparam = "" + if daemon is not None: + dparam += "-d {}".format(daemon) + + # Run the commands and delete the temporary file + if pretty_output: + vtysh_command = "vtysh {} < {}".format(dparam, fname) + else: + vtysh_command = "vtysh {} -f {}".format(dparam, fname) + + dbgcmds = commands if is_string(commands) else "\n".join(commands) + dbgcmds = "\t" + dbgcmds.replace("\n", "\n\t") + self.logger.info("vtysh command => FILE:\n{}".format(dbgcmds)) + + res = self.run(vtysh_command) + os.unlink(fname) + + dbgres = res.strip() + if dbgres: + if "\n" in dbgres: + dbgres = dbgres.replace("\n", "\n\t") + self.logger.info("vtysh result:\n\t{}".format(dbgres)) + else: + self.logger.info('vtysh result: "{}"'.format(dbgres)) + return res + + def report_memory_leaks(self, testname): + """ + Runs the router memory leak check test. Has the following parameter: + testname: the test file name for identification + + NOTE: to run this you must have the environment variable + TOPOTESTS_CHECK_MEMLEAK set or memleak_path configured in `pytest.ini`. + """ + memleak_file = ( + os.environ.get("TOPOTESTS_CHECK_MEMLEAK") or self.params["memleak_path"] + ) + if memleak_file == "" or memleak_file == None: + return + + self.stop() + + self.logger.info("running memory leak report") + self.net.report_memory_leaks(memleak_file, testname) + + def version_info(self): + "Get equipment information from 'show version'." + output = self.vtysh_cmd("show version").split("\n")[0] + columns = topotest.normalize_text(output).split(" ") + try: + return { + "type": columns[0], + "version": columns[1], + } + except IndexError: + return { + "type": None, + "version": None, + } + + def has_version(self, cmpop, version): + """ + Compares router version using operation `cmpop` with `version`. + Valid `cmpop` values: + * `>=`: has the same version or greater + * '>': has greater version + * '=': has the same version + * '<': has a lesser version + * '<=': has the same version or lesser + + Usage example: router.has_version('>', '1.0') + """ + return self.net.checkRouterVersion(cmpop, version) + + def has_type(self, rtype): + """ + Compares router type with `rtype`. Returns `True` if the type matches, + otherwise `false`. + """ + curtype = self.version_info()["type"] + return rtype == curtype + + def has_mpls(self): + return self.net.hasmpls + + +class TopoSwitch(TopoGear): + """ + Switch abstraction. Has the following properties: + * cls: switch class that will be used to instantiate + * name: switch name + """ + + # pylint: disable=too-few-public-methods + + def __init__(self, tgen, name, **params): + super(TopoSwitch, self).__init__(tgen, name, **params) + tgen.net.add_switch(name) + + def __str__(self): + gear = super(TopoSwitch, self).__str__() + gear += " TopoSwitch<>" + return gear + + +class TopoHost(TopoGear): + "Host abstraction." + # pylint: disable=too-few-public-methods + + def __init__(self, tgen, name, **params): + """ + Mininet has the following known `params` for hosts: + * `ip`: the IP address (string) for the host interface + * `defaultRoute`: the default route that will be installed + (e.g. 'via 10.0.0.1') + * `privateDirs`: directories that will be mounted on a different domain + (e.g. '/etc/important_dir'). + """ + super(TopoHost, self).__init__(tgen, name, **params) + + # Propagate the router log directory + logfile = self._setup_tmpdir() + params["logdir"] = self.logdir + + # Odd to have 2 logfiles for each host + self.logger = topolog.get_logger(name, log_level="debug", target=logfile) + params["logger"] = self.logger + tgen.net.add_host(name, **params) + topotest.fix_netns_limits(tgen.net[name]) + + # Mount gear log directory on a common path + self.net.bind_mount(self.gearlogdir, "/tmp/gearlogdir") + + def __str__(self): + gear = super(TopoHost, self).__str__() + gear += ' TopoHost<ip="{}",defaultRoute="{}",privateDirs="{}">'.format( + self.params["ip"], + self.params["defaultRoute"], + str(self.params["privateDirs"]), + ) + return gear + + +class TopoExaBGP(TopoHost): + "ExaBGP peer abstraction." + # pylint: disable=too-few-public-methods + + PRIVATE_DIRS = [ + "/etc/exabgp", + "/var/run/exabgp", + "/var/log", + ] + + def __init__(self, tgen, name, **params): + """ + ExaBGP usually uses the following parameters: + * `ip`: the IP address (string) for the host interface + * `defaultRoute`: the default route that will be installed + (e.g. 'via 10.0.0.1') + + Note: the different between a host and a ExaBGP peer is that this class + has a privateDirs already defined and contains functions to handle ExaBGP + things. + """ + params["privateDirs"] = self.PRIVATE_DIRS + super(TopoExaBGP, self).__init__(tgen, name, **params) + + def __str__(self): + gear = super(TopoExaBGP, self).__str__() + gear += " TopoExaBGP<>".format() + return gear + + def start(self, peer_dir, env_file=None): + """ + Start running ExaBGP daemon: + * Copy all peer* folder contents into /etc/exabgp + * Copy exabgp env file if specified + * Make all python files runnable + * Run ExaBGP with env file `env_file` and configuration peer*/exabgp.cfg + """ + exacmd = self.tgen.get_exabgp_cmd() + assert exacmd, "Can't find a usabel ExaBGP (must be < version 4)" + + self.run("mkdir -p /etc/exabgp") + self.run("chmod 755 /etc/exabgp") + self.run("cp {}/exa-* /etc/exabgp/".format(CWD)) + self.run("cp {}/* /etc/exabgp/".format(peer_dir)) + if env_file is not None: + self.run("cp {} /etc/exabgp/exabgp.env".format(env_file)) + self.run("chmod 644 /etc/exabgp/*") + self.run("chmod a+x /etc/exabgp/*.py") + self.run("chown -R exabgp:exabgp /etc/exabgp") + + output = self.run(exacmd + " -e /etc/exabgp/exabgp.env /etc/exabgp/exabgp.cfg") + if output == None or len(output) == 0: + output = "<none>" + + logger.info("{} exabgp started, output={}".format(self.name, output)) + + def stop(self, wait=True, assertOnError=True): + "Stop ExaBGP peer and kill the daemon" + self.run("kill `cat /var/run/exabgp/exabgp.pid`") + return "" + + +# +# Diagnostic function +# + +# Disable linter branch warning. It is expected to have these here. +# pylint: disable=R0912 +def diagnose_env_linux(rundir): + """ + Run diagnostics in the running environment. Returns `True` when everything + is ok, otherwise `False`. + """ + ret = True + + # Load configuration + config = configparser.ConfigParser(defaults=tgen_defaults) + pytestini_path = os.path.join(CWD, "../pytest.ini") + config.read(pytestini_path) + + # Test log path exists before installing handler. + os.system("mkdir -p " + rundir) + # Log diagnostics to file so it can be examined later. + fhandler = logging.FileHandler(filename="{}/diagnostics.txt".format(rundir)) + fhandler.setLevel(logging.DEBUG) + fhandler.setFormatter(logging.Formatter(fmt=topolog.FORMAT)) + logger.addHandler(fhandler) + + logger.info("Running environment diagnostics") + + # Assert that we are running as root + if os.getuid() != 0: + logger.error("you must run topotest as root") + ret = False + + # Assert that we have mininet + # if os.system("which mn >/dev/null 2>/dev/null") != 0: + # logger.error("could not find mininet binary (mininet is not installed)") + # ret = False + + # Assert that we have iproute installed + if os.system("which ip >/dev/null 2>/dev/null") != 0: + logger.error("could not find ip binary (iproute is not installed)") + ret = False + + # Assert that we have gdb installed + if os.system("which gdb >/dev/null 2>/dev/null") != 0: + logger.error("could not find gdb binary (gdb is not installed)") + ret = False + + # Assert that FRR utilities exist + frrdir = config.get("topogen", "frrdir") + if not os.path.isdir(frrdir): + logger.error("could not find {} directory".format(frrdir)) + ret = False + else: + try: + pwd.getpwnam("frr")[2] + except KeyError: + logger.warning('could not find "frr" user') + + try: + grp.getgrnam("frr")[2] + except KeyError: + logger.warning('could not find "frr" group') + + try: + if "frr" not in grp.getgrnam("frrvty").gr_mem: + logger.error( + '"frr" user and group exist, but user is not under "frrvty"' + ) + except KeyError: + logger.warning('could not find "frrvty" group') + + for fname in [ + "zebra", + "ospfd", + "ospf6d", + "bgpd", + "ripd", + "ripngd", + "isisd", + "pimd", + "pim6d", + "ldpd", + "pbrd", + ]: + path = os.path.join(frrdir, fname) + if not os.path.isfile(path): + # LDPd is an exception + if fname == "ldpd": + logger.info( + "could not find {} in {}".format(fname, frrdir) + + "(LDPd tests will not run)" + ) + continue + + logger.error("could not find {} in {}".format(fname, frrdir)) + ret = False + else: + if fname != "zebra": + continue + + os.system("{} -v 2>&1 >{}/frr_zebra.txt".format(path, rundir)) + + # Test MPLS availability + krel = platform.release() + if topotest.version_cmp(krel, "4.5") < 0: + logger.info( + 'LDPd tests will not run (have kernel "{}", but it requires 4.5)'.format( + krel + ) + ) + + # Test for MPLS Kernel modules available + if not topotest.module_present("mpls-router", load=False) != 0: + logger.info("LDPd tests will not run (missing mpls-router kernel module)") + if not topotest.module_present("mpls-iptunnel", load=False) != 0: + logger.info("LDPd tests will not run (missing mpls-iptunnel kernel module)") + + if not get_exabgp_cmd(): + logger.warning("Failed to find exabgp < 4") + + logger.removeHandler(fhandler) + fhandler.close() + + return ret + + +def diagnose_env_freebsd(): + return True + + +def diagnose_env(rundir): + if sys.platform.startswith("linux"): + return diagnose_env_linux(rundir) + elif sys.platform.startswith("freebsd"): + return diagnose_env_freebsd() + + return False diff --git a/tests/topotests/lib/topojson.py b/tests/topotests/lib/topojson.py new file mode 100644 index 0000000..b49b09e --- /dev/null +++ b/tests/topotests/lib/topojson.py @@ -0,0 +1,420 @@ +# +# Modified work Copyright (c) 2019 by VMware, Inc. ("VMware") +# Original work Copyright (c) 2018 by Network Device Education +# Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND VMWARE DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL VMWARE BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import json +import ipaddress +import os +from collections import OrderedDict +from copy import deepcopy +from re import search as re_search + +import pytest + +from lib.bgp import create_router_bgp +from lib.common_config import ( + create_bgp_community_lists, + create_interfaces_cfg, + create_prefix_lists, + create_route_maps, + create_static_routes, + create_vrf_cfg, + load_config_to_routers, + start_topology, + topo_daemons, + number_to_column, +) +from lib.ospf import create_router_ospf +from lib.pim import ( + create_igmp_config, + create_pim_config, + create_mld_config, +) +from lib.topolog import logger + + +def build_topo_from_json(tgen, topo=None): + """ + Reads configuration from JSON file. Adds routers, creates interface + names dynamically and link routers as defined in JSON to create + topology. Assigns IPs dynamically to all interfaces of each router. + * `tgen`: Topogen object + * `topo`: json file data, or use tgen.json_topo if None + """ + if topo is None: + topo = tgen.json_topo + + router_list = sorted( + topo["routers"].keys(), key=lambda x: int(re_search(r"\d+", x).group(0)) + ) + + switch_list = [] + if "switches" in topo: + switch_list = sorted( + topo["switches"].keys(), key=lambda x: int(re_search(r"\d+", x).group(0)) + ) + + listRouters = sorted(router_list[:]) + listSwitches = sorted(switch_list[:]) + listAllRouters = deepcopy(listRouters) + dictSwitches = {} + + for routerN in router_list: + logger.info("Topo: Add router {}".format(routerN)) + tgen.add_router(routerN) + + for switchN in switch_list: + logger.info("Topo: Add switch {}".format(switchN)) + dictSwitches[switchN] = tgen.add_switch(switchN) + + if "ipv4base" in topo: + ipv4Next = ipaddress.IPv4Address(topo["link_ip_start"]["ipv4"]) + ipv4Step = 2 ** (32 - topo["link_ip_start"]["v4mask"]) + if topo["link_ip_start"]["v4mask"] < 32: + ipv4Next += 1 + if "ipv6base" in topo: + ipv6Next = ipaddress.IPv6Address(topo["link_ip_start"]["ipv6"]) + ipv6Step = 2 ** (128 - topo["link_ip_start"]["v6mask"]) + if topo["link_ip_start"]["v6mask"] < 127: + ipv6Next += 1 + for router in listRouters: + topo["routers"][router]["nextIfname"] = 0 + + router_count = 0 + while listRouters != []: + curRouter = listRouters.pop(0) + # Physical Interfaces + if "links" in topo["routers"][curRouter]: + for destRouterLink, data in sorted( + topo["routers"][curRouter]["links"].items() + ): + currRouter_lo_json = topo["routers"][curRouter]["links"][destRouterLink] + # Loopback interfaces + if "type" in data and data["type"] == "loopback": + router_count += 1 + if ( + "ipv4" in currRouter_lo_json + and currRouter_lo_json["ipv4"] == "auto" + ): + currRouter_lo_json["ipv4"] = "{}{}.{}/{}".format( + topo["lo_prefix"]["ipv4"], + router_count, + number_to_column(curRouter), + topo["lo_prefix"]["v4mask"], + ) + if ( + "ipv6" in currRouter_lo_json + and currRouter_lo_json["ipv6"] == "auto" + ): + currRouter_lo_json["ipv6"] = "{}{}:{}/{}".format( + topo["lo_prefix"]["ipv6"], + router_count, + number_to_column(curRouter), + topo["lo_prefix"]["v6mask"], + ) + + if "-" in destRouterLink: + # Spliting and storing destRouterLink data in tempList + tempList = destRouterLink.split("-") + + # destRouter + destRouter = tempList.pop(0) + + # Current Router Link + tempList.insert(0, curRouter) + curRouterLink = "-".join(tempList) + else: + destRouter = destRouterLink + curRouterLink = curRouter + + if destRouter in listRouters: + currRouter_link_json = topo["routers"][curRouter]["links"][ + destRouterLink + ] + destRouter_link_json = topo["routers"][destRouter]["links"][ + curRouterLink + ] + + # Assigning name to interfaces + currRouter_link_json["interface"] = "{}-{}-eth{}".format( + curRouter, destRouter, topo["routers"][curRouter]["nextIfname"] + ) + destRouter_link_json["interface"] = "{}-{}-eth{}".format( + destRouter, curRouter, topo["routers"][destRouter]["nextIfname"] + ) + + # add link interface + destRouter_link_json["peer-interface"] = "{}-{}-eth{}".format( + curRouter, destRouter, topo["routers"][curRouter]["nextIfname"] + ) + currRouter_link_json["peer-interface"] = "{}-{}-eth{}".format( + destRouter, curRouter, topo["routers"][destRouter]["nextIfname"] + ) + + topo["routers"][curRouter]["nextIfname"] += 1 + topo["routers"][destRouter]["nextIfname"] += 1 + + # Linking routers to each other as defined in JSON file + tgen.gears[curRouter].add_link( + tgen.gears[destRouter], + topo["routers"][curRouter]["links"][destRouterLink][ + "interface" + ], + topo["routers"][destRouter]["links"][curRouterLink][ + "interface" + ], + ) + + # IPv4 + if "ipv4" in currRouter_link_json: + if currRouter_link_json["ipv4"] == "auto": + currRouter_link_json["ipv4"] = "{}/{}".format( + ipv4Next, topo["link_ip_start"]["v4mask"] + ) + destRouter_link_json["ipv4"] = "{}/{}".format( + ipv4Next + 1, topo["link_ip_start"]["v4mask"] + ) + ipv4Next += ipv4Step + # IPv6 + if "ipv6" in currRouter_link_json: + if currRouter_link_json["ipv6"] == "auto": + currRouter_link_json["ipv6"] = "{}/{}".format( + ipv6Next, topo["link_ip_start"]["v6mask"] + ) + destRouter_link_json["ipv6"] = "{}/{}".format( + ipv6Next + 1, topo["link_ip_start"]["v6mask"] + ) + ipv6Next = ipaddress.IPv6Address(int(ipv6Next) + ipv6Step) + + logger.debug( + "Generated link data for router: %s\n%s", + curRouter, + json.dumps( + topo["routers"][curRouter]["links"], indent=4, sort_keys=True + ), + ) + + switch_count = 0 + add_switch_to_topo = [] + while listSwitches != []: + curSwitch = listSwitches.pop(0) + # Physical Interfaces + if "links" in topo["switches"][curSwitch]: + for destRouterLink, data in sorted( + topo["switches"][curSwitch]["links"].items() + ): + + # Loopback interfaces + if "dst_node" in data: + destRouter = data["dst_node"] + + elif "-" in destRouterLink: + # Spliting and storing destRouterLink data in tempList + tempList = destRouterLink.split("-") + # destRouter + destRouter = tempList.pop(0) + else: + destRouter = destRouterLink + + if destRouter in listAllRouters: + + topo["routers"][destRouter]["links"][curSwitch] = deepcopy( + topo["switches"][curSwitch]["links"][destRouterLink] + ) + + # Assigning name to interfaces + topo["routers"][destRouter]["links"][curSwitch][ + "interface" + ] = "{}-{}-eth{}".format( + destRouter, curSwitch, topo["routers"][destRouter]["nextIfname"] + ) + + topo["switches"][curSwitch]["links"][destRouter][ + "interface" + ] = "{}-{}-eth{}".format( + curSwitch, destRouter, topo["routers"][destRouter]["nextIfname"] + ) + + topo["routers"][destRouter]["nextIfname"] += 1 + + # Add links + dictSwitches[curSwitch].add_link( + tgen.gears[destRouter], + topo["switches"][curSwitch]["links"][destRouter]["interface"], + topo["routers"][destRouter]["links"][curSwitch]["interface"], + ) + + # IPv4 + if "ipv4" in topo["routers"][destRouter]["links"][curSwitch]: + if ( + topo["routers"][destRouter]["links"][curSwitch]["ipv4"] + == "auto" + ): + topo["routers"][destRouter]["links"][curSwitch][ + "ipv4" + ] = "{}/{}".format( + ipv4Next, topo["link_ip_start"]["v4mask"] + ) + ipv4Next += 1 + # IPv6 + if "ipv6" in topo["routers"][destRouter]["links"][curSwitch]: + if ( + topo["routers"][destRouter]["links"][curSwitch]["ipv6"] + == "auto" + ): + topo["routers"][destRouter]["links"][curSwitch][ + "ipv6" + ] = "{}/{}".format( + ipv6Next, topo["link_ip_start"]["v6mask"] + ) + ipv6Next = ipaddress.IPv6Address(int(ipv6Next) + ipv6Step) + + logger.debug( + "Generated link data for router: %s\n%s", + curRouter, + json.dumps( + topo["routers"][curRouter]["links"], indent=4, sort_keys=True + ), + ) + + +def linux_intf_config_from_json(tgen, topo=None): + """Configure interfaces from linux based on topo.""" + if topo is None: + topo = tgen.json_topo + + routers = topo["routers"] + for rname in routers: + router = tgen.net[rname] + links = routers[rname]["links"] + for rrname in links: + link = links[rrname] + if rrname == "lo": + lname = "lo" + else: + lname = link["interface"] + if "ipv4" in link: + router.cmd_raises("ip addr add {} dev {}".format(link["ipv4"], lname)) + if "ipv6" in link: + router.cmd_raises( + "ip -6 addr add {} dev {}".format(link["ipv6"], lname) + ) + + +def build_config_from_json(tgen, topo=None, save_bkup=True): + """ + Reads initial configuraiton from JSON for each router, builds + configuration and loads its to router. + + * `tgen`: Topogen object + * `topo`: json file data, or use tgen.json_topo if None + """ + + func_dict = OrderedDict( + [ + ("vrfs", create_vrf_cfg), + ("links", create_interfaces_cfg), + ("static_routes", create_static_routes), + ("prefix_lists", create_prefix_lists), + ("bgp_community_list", create_bgp_community_lists), + ("route_maps", create_route_maps), + ("pim", create_pim_config), + ("igmp", create_igmp_config), + ("mld", create_mld_config), + ("bgp", create_router_bgp), + ("ospf", create_router_ospf), + ] + ) + + if topo is None: + topo = tgen.json_topo + + data = topo["routers"] + for func_type in func_dict.keys(): + logger.info("Checking for {} configuration in input data".format(func_type)) + + func_dict.get(func_type)(tgen, data, build=True) + + routers = sorted(topo["routers"].keys()) + result = load_config_to_routers(tgen, routers, save_bkup) + if not result: + logger.info("build_config_from_json: failed to configure topology") + pytest.exit(1) + + logger.info( + "Built config now clearing ospf neighbors as that router-id might not be what is used" + ) + for ospf in ["ospf", "ospf6"]: + for router in data: + if ospf not in data[router]: + continue + + r = tgen.gears[router] + if ospf == "ospf": + r.vtysh_cmd("clear ip ospf process") + else: + r.vtysh_cmd("clear ipv6 ospf6 process") + + +def create_tgen_from_json(testfile, json_file=None): + """Create a topogen object given a testfile. + + - `testfile` : The path to the testfile. + - `json_file` : The path to the json config file. If None the pathname is derived + from the `testfile` first by trying to replace `.py` by `.json` and if that isn't + present then by removing `test_` prefix as well. + """ + from lib.topogen import Topogen # Topogen imports this module too + + thisdir = os.path.dirname(os.path.realpath(testfile)) + basename = os.path.basename(testfile) + logger.debug("starting standard JSON based module setup for %s", basename) + + assert basename.startswith("test_") + assert basename.endswith(".py") + json_file = os.path.join(thisdir, basename[:-3] + ".json") + if not os.path.exists(json_file): + json_file = os.path.join(thisdir, basename[5:-3] + ".json") + assert os.path.exists(json_file) + with open(json_file, "r") as topof: + topo = json.load(topof) + + # Create topology + tgen = Topogen(lambda tgen: build_topo_from_json(tgen, topo), basename[:-3]) + tgen.json_topo = topo + return tgen + + +def setup_module_from_json(testfile, json_file=None): + """Do the standard module setup for JSON based test. + + * `testfile` : The path to the testfile. The name is used to derive the json config + file name as well (removing `test_` prefix and replacing `.py` suffix with `.json` + """ + # Create topology object + tgen = create_tgen_from_json(testfile, json_file) + + # Start routers (and their daemons) + start_topology(tgen, topo_daemons(tgen)) + + # Configure routers + build_config_from_json(tgen) + assert not tgen.routers_have_failure() + + return tgen diff --git a/tests/topotests/lib/topolog.py b/tests/topotests/lib/topolog.py new file mode 100644 index 0000000..9cc3386 --- /dev/null +++ b/tests/topotests/lib/topolog.py @@ -0,0 +1,178 @@ +# +# topolog.py +# Library of helper functions for NetDEF Topology Tests +# +# Copyright (c) 2017 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +""" +Logging utilities for topology tests. + +This file defines our logging abstraction. +""" + +import logging +import os +import subprocess +import sys + +if sys.version_info[0] > 2: + pass +else: + pass + +try: + from xdist import is_xdist_controller +except ImportError: + + def is_xdist_controller(): + return False + + +BASENAME = "topolog" + +# Helper dictionary to convert Topogen logging levels to Python's logging. +DEBUG_TOPO2LOGGING = { + "debug": logging.DEBUG, + "info": logging.INFO, + "output": logging.INFO, + "warning": logging.WARNING, + "error": logging.ERROR, + "critical": logging.CRITICAL, +} +FORMAT = "%(asctime)s.%(msecs)03d %(levelname)s: %(name)s: %(message)s" + +handlers = {} +logger = logging.getLogger("topolog") + + +def set_handler(l, target=None): + if target is None: + h = logging.NullHandler() + else: + if isinstance(target, str): + h = logging.FileHandler(filename=target, mode="w") + else: + h = logging.StreamHandler(stream=target) + h.setFormatter(logging.Formatter(fmt=FORMAT)) + # Don't filter anything at the handler level + h.setLevel(logging.DEBUG) + l.addHandler(h) + return h + + +def set_log_level(l, level): + "Set the logging level." + # Messages sent to this logger only are created if this level or above. + log_level = DEBUG_TOPO2LOGGING.get(level, level) + l.setLevel(log_level) + + +def get_logger(name, log_level=None, target=None): + l = logging.getLogger("{}.{}".format(BASENAME, name)) + + if log_level is not None: + set_log_level(l, log_level) + + if target is not None: + set_handler(l, target) + + return l + + +# nodeid: all_protocol_startup/test_all_protocol_startup.py::test_router_running + + +def get_test_logdir(nodeid=None): + """Get log directory relative pathname.""" + xdist_worker = os.getenv("PYTEST_XDIST_WORKER", "") + mode = os.getenv("PYTEST_XDIST_MODE", "no") + + if not nodeid: + nodeid = os.environ["PYTEST_CURRENT_TEST"].split(" ")[0] + + cur_test = nodeid.replace("[", "_").replace("]", "_") + path, testname = cur_test.split("::") + path = path[:-3].replace("/", ".") + + # We use different logdir paths based on how xdist is running. + if mode == "each": + return os.path.join(path, testname, xdist_worker) + elif mode == "load": + return os.path.join(path, testname) + else: + assert ( + mode == "no" or mode == "loadfile" or mode == "loadscope" + ), "Unknown dist mode {}".format(mode) + + return path + + +def logstart(nodeid, location, rundir): + """Called from pytest before module setup.""" + + mode = os.getenv("PYTEST_XDIST_MODE", "no") + worker = os.getenv("PYTEST_TOPOTEST_WORKER", "") + + # We only per-test log in the workers (or non-dist) + if not worker and mode != "no": + return + + handler_id = nodeid + worker + assert handler_id not in handlers + + rel_log_dir = get_test_logdir(nodeid) + exec_log_dir = os.path.join(rundir, rel_log_dir) + subprocess.check_call( + "mkdir -p {0} && chmod 1777 {0}".format(exec_log_dir), shell=True + ) + exec_log_path = os.path.join(exec_log_dir, "exec.log") + + # Add test based exec log handler + h = set_handler(logger, exec_log_path) + handlers[handler_id] = h + + if worker: + logger.info( + "Logging on worker %s for %s into %s", worker, handler_id, exec_log_path + ) + else: + logger.info("Logging for %s into %s", handler_id, exec_log_path) + + +def logfinish(nodeid, location): + """Called from pytest after module teardown.""" + # This function may not be called if pytest is interrupted. + + worker = os.getenv("PYTEST_TOPOTEST_WORKER", "") + handler_id = nodeid + worker + + if handler_id in handlers: + # Remove test based exec log handler + if worker: + logger.info("Closing logs for %s", handler_id) + + h = handlers[handler_id] + logger.removeHandler(handlers[handler_id]) + h.flush() + h.close() + del handlers[handler_id] + + +console_handler = set_handler(logger, None) +set_log_level(logger, "debug") diff --git a/tests/topotests/lib/topotest.py b/tests/topotests/lib/topotest.py new file mode 100644 index 0000000..5a3f586 --- /dev/null +++ b/tests/topotests/lib/topotest.py @@ -0,0 +1,2175 @@ +#!/usr/bin/env python + +# +# topotest.py +# Library of helper functions for NetDEF Topology Tests +# +# Copyright (c) 2016 by +# Network Device Education Foundation, Inc. ("NetDEF") +# +# Permission to use, copy, modify, and/or distribute this software +# for any purpose with or without fee is hereby granted, provided +# that the above copyright notice and this permission notice appear +# in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NETDEF DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NETDEF BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# + +import difflib +import errno +import functools +import glob +import json +import os +import pdb +import platform +import re +import resource +import signal +import subprocess +import sys +import tempfile +import time +from copy import deepcopy + +import lib.topolog as topolog +from lib.topolog import logger + +if sys.version_info[0] > 2: + import configparser + from collections.abc import Mapping +else: + import ConfigParser as configparser + from collections import Mapping + +from lib import micronet +from lib.micronet_compat import Node + +g_extra_config = {} + + +def get_logs_path(rundir): + logspath = topolog.get_test_logdir() + return os.path.join(rundir, logspath) + + +def gdb_core(obj, daemon, corefiles): + gdbcmds = """ + info threads + bt full + disassemble + up + disassemble + up + disassemble + up + disassemble + up + disassemble + up + disassemble + """ + gdbcmds = [["-ex", i.strip()] for i in gdbcmds.strip().split("\n")] + gdbcmds = [item for sl in gdbcmds for item in sl] + + daemon_path = os.path.join(obj.daemondir, daemon) + backtrace = subprocess.check_output( + ["gdb", daemon_path, corefiles[0], "--batch"] + gdbcmds + ) + sys.stderr.write( + "\n%s: %s crashed. Core file found - Backtrace follows:\n" % (obj.name, daemon) + ) + sys.stderr.write("%s" % backtrace) + return backtrace + + +class json_cmp_result(object): + "json_cmp result class for better assertion messages" + + def __init__(self): + self.errors = [] + + def add_error(self, error): + "Append error message to the result" + for line in error.splitlines(): + self.errors.append(line) + + def has_errors(self): + "Returns True if there were errors, otherwise False." + return len(self.errors) > 0 + + def gen_report(self): + headline = ["Generated JSON diff error report:", ""] + return headline + self.errors + + def __str__(self): + return ( + "Generated JSON diff error report:\n\n\n" + "\n".join(self.errors) + "\n\n" + ) + + +def gen_json_diff_report(d1, d2, exact=False, path="> $", acc=(0, "")): + """ + Internal workhorse which compares two JSON data structures and generates an error report suited to be read by a human eye. + """ + + def dump_json(v): + if isinstance(v, (dict, list)): + return "\t" + "\t".join( + json.dumps(v, indent=4, separators=(",", ": ")).splitlines(True) + ) + else: + return "'{}'".format(v) + + def json_type(v): + if isinstance(v, (list, tuple)): + return "Array" + elif isinstance(v, dict): + return "Object" + elif isinstance(v, (int, float)): + return "Number" + elif isinstance(v, bool): + return "Boolean" + elif isinstance(v, str): + return "String" + elif v == None: + return "null" + + def get_errors(other_acc): + return other_acc[1] + + def get_errors_n(other_acc): + return other_acc[0] + + def add_error(acc, msg, points=1): + return (acc[0] + points, acc[1] + "{}: {}\n".format(path, msg)) + + def merge_errors(acc, other_acc): + return (acc[0] + other_acc[0], acc[1] + other_acc[1]) + + def add_idx(idx): + return "{}[{}]".format(path, idx) + + def add_key(key): + return "{}->{}".format(path, key) + + def has_errors(other_acc): + return other_acc[0] > 0 + + if d2 == "*" or ( + not isinstance(d1, (list, dict)) + and not isinstance(d2, (list, dict)) + and d1 == d2 + ): + return acc + elif ( + not isinstance(d1, (list, dict)) + and not isinstance(d2, (list, dict)) + and d1 != d2 + ): + acc = add_error( + acc, + "d1 has element with value '{}' but in d2 it has value '{}'".format(d1, d2), + ) + elif ( + isinstance(d1, list) + and isinstance(d2, list) + and ((len(d2) > 0 and d2[0] == "__ordered__") or exact) + ): + if not exact: + del d2[0] + if len(d1) != len(d2): + acc = add_error( + acc, + "d1 has Array of length {} but in d2 it is of length {}".format( + len(d1), len(d2) + ), + ) + else: + for idx, v1, v2 in zip(range(0, len(d1)), d1, d2): + acc = merge_errors( + acc, gen_json_diff_report(v1, v2, exact=exact, path=add_idx(idx)) + ) + elif isinstance(d1, list) and isinstance(d2, list): + if len(d1) < len(d2): + acc = add_error( + acc, + "d1 has Array of length {} but in d2 it is of length {}".format( + len(d1), len(d2) + ), + ) + else: + for idx2, v2 in zip(range(0, len(d2)), d2): + found_match = False + closest_diff = None + closest_idx = None + for idx1, v1 in zip(range(0, len(d1)), d1): + tmp_v1 = deepcopy(v1) + tmp_v2 = deepcopy(v2) + tmp_diff = gen_json_diff_report(tmp_v1, tmp_v2, path=add_idx(idx1)) + if not has_errors(tmp_diff): + found_match = True + del d1[idx1] + break + elif not closest_diff or get_errors_n(tmp_diff) < get_errors_n( + closest_diff + ): + closest_diff = tmp_diff + closest_idx = idx1 + if not found_match and isinstance(v2, (list, dict)): + sub_error = "\n\n\t{}".format( + "\t".join(get_errors(closest_diff).splitlines(True)) + ) + acc = add_error( + acc, + ( + "d2 has the following element at index {} which is not present in d1: " + + "\n\n{}\n\n\tClosest match in d1 is at index {} with the following errors: {}" + ).format(idx2, dump_json(v2), closest_idx, sub_error), + ) + if not found_match and not isinstance(v2, (list, dict)): + acc = add_error( + acc, + "d2 has the following element at index {} which is not present in d1: {}".format( + idx2, dump_json(v2) + ), + ) + elif isinstance(d1, dict) and isinstance(d2, dict) and exact: + invalid_keys_d1 = [k for k in d1.keys() if k not in d2.keys()] + invalid_keys_d2 = [k for k in d2.keys() if k not in d1.keys()] + for k in invalid_keys_d1: + acc = add_error(acc, "d1 has key '{}' which is not present in d2".format(k)) + for k in invalid_keys_d2: + acc = add_error(acc, "d2 has key '{}' which is not present in d1".format(k)) + valid_keys_intersection = [k for k in d1.keys() if k in d2.keys()] + for k in valid_keys_intersection: + acc = merge_errors( + acc, gen_json_diff_report(d1[k], d2[k], exact=exact, path=add_key(k)) + ) + elif isinstance(d1, dict) and isinstance(d2, dict): + none_keys = [k for k, v in d2.items() if v == None] + none_keys_present = [k for k in d1.keys() if k in none_keys] + for k in none_keys_present: + acc = add_error( + acc, "d1 has key '{}' which is not supposed to be present".format(k) + ) + keys = [k for k, v in d2.items() if v != None] + invalid_keys_intersection = [k for k in keys if k not in d1.keys()] + for k in invalid_keys_intersection: + acc = add_error(acc, "d2 has key '{}' which is not present in d1".format(k)) + valid_keys_intersection = [k for k in keys if k in d1.keys()] + for k in valid_keys_intersection: + acc = merge_errors( + acc, gen_json_diff_report(d1[k], d2[k], exact=exact, path=add_key(k)) + ) + else: + acc = add_error( + acc, + "d1 has element of type '{}' but the corresponding element in d2 is of type '{}'".format( + json_type(d1), json_type(d2) + ), + points=2, + ) + + return acc + + +def json_cmp(d1, d2, exact=False): + """ + JSON compare function. Receives two parameters: + * `d1`: parsed JSON data structure + * `d2`: parsed JSON data structure + + Returns 'None' when all JSON Object keys and all Array elements of d2 have a match + in d1, i.e., when d2 is a "subset" of d1 without honoring any order. Otherwise an + error report is generated and wrapped in a 'json_cmp_result()'. There are special + parameters and notations explained below which can be used to cover rather unusual + cases: + + * when 'exact is set to 'True' then d1 and d2 are tested for equality (including + order within JSON Arrays) + * using 'null' (or 'None' in Python) as JSON Object value is checking for key + absence in d1 + * using '*' as JSON Object value or Array value is checking for presence in d1 + without checking the values + * using '__ordered__' as first element in a JSON Array in d2 will also check the + order when it is compared to an Array in d1 + """ + + (errors_n, errors) = gen_json_diff_report(deepcopy(d1), deepcopy(d2), exact=exact) + + if errors_n > 0: + result = json_cmp_result() + result.add_error(errors) + return result + else: + return None + + +def router_output_cmp(router, cmd, expected): + """ + Runs `cmd` in router and compares the output with `expected`. + """ + return difflines( + normalize_text(router.vtysh_cmd(cmd)), + normalize_text(expected), + title1="Current output", + title2="Expected output", + ) + + +def router_json_cmp(router, cmd, data, exact=False): + """ + Runs `cmd` that returns JSON data (normally the command ends with 'json') + and compare with `data` contents. + """ + return json_cmp(router.vtysh_cmd(cmd, isjson=True), data, exact) + + +def run_and_expect(func, what, count=20, wait=3): + """ + Run `func` and compare the result with `what`. Do it for `count` times + waiting `wait` seconds between tries. By default it tries 20 times with + 3 seconds delay between tries. + + Returns (True, func-return) on success or + (False, func-return) on failure. + + --- + + Helper functions to use with this function: + - router_output_cmp + - router_json_cmp + """ + start_time = time.time() + func_name = "<unknown>" + if func.__class__ == functools.partial: + func_name = func.func.__name__ + else: + func_name = func.__name__ + + logger.info( + "'{}' polling started (interval {} secs, maximum {} tries)".format( + func_name, wait, count + ) + ) + + while count > 0: + result = func() + if result != what: + time.sleep(wait) + count -= 1 + continue + + end_time = time.time() + logger.info( + "'{}' succeeded after {:.2f} seconds".format( + func_name, end_time - start_time + ) + ) + return (True, result) + + end_time = time.time() + logger.error( + "'{}' failed after {:.2f} seconds".format(func_name, end_time - start_time) + ) + return (False, result) + + +def run_and_expect_type(func, etype, count=20, wait=3, avalue=None): + """ + Run `func` and compare the result with `etype`. Do it for `count` times + waiting `wait` seconds between tries. By default it tries 20 times with + 3 seconds delay between tries. + + This function is used when you want to test the return type and, + optionally, the return value. + + Returns (True, func-return) on success or + (False, func-return) on failure. + """ + start_time = time.time() + func_name = "<unknown>" + if func.__class__ == functools.partial: + func_name = func.func.__name__ + else: + func_name = func.__name__ + + logger.info( + "'{}' polling started (interval {} secs, maximum wait {} secs)".format( + func_name, wait, int(wait * count) + ) + ) + + while count > 0: + result = func() + if not isinstance(result, etype): + logger.debug( + "Expected result type '{}' got '{}' instead".format(etype, type(result)) + ) + time.sleep(wait) + count -= 1 + continue + + if etype != type(None) and avalue != None and result != avalue: + logger.debug("Expected value '{}' got '{}' instead".format(avalue, result)) + time.sleep(wait) + count -= 1 + continue + + end_time = time.time() + logger.info( + "'{}' succeeded after {:.2f} seconds".format( + func_name, end_time - start_time + ) + ) + return (True, result) + + end_time = time.time() + logger.error( + "'{}' failed after {:.2f} seconds".format(func_name, end_time - start_time) + ) + return (False, result) + + +def router_json_cmp_retry(router, cmd, data, exact=False, retry_timeout=10.0): + """ + Runs `cmd` that returns JSON data (normally the command ends with 'json') + and compare with `data` contents. Retry by default for 10 seconds + """ + + def test_func(): + return router_json_cmp(router, cmd, data, exact) + + ok, _ = run_and_expect(test_func, None, int(retry_timeout), 1) + return ok + + +def int2dpid(dpid): + "Converting Integer to DPID" + + try: + dpid = hex(dpid)[2:] + dpid = "0" * (16 - len(dpid)) + dpid + return dpid + except IndexError: + raise Exception( + "Unable to derive default datapath ID - " + "please either specify a dpid or use a " + "canonical switch name such as s23." + ) + + +def pid_exists(pid): + "Check whether pid exists in the current process table." + + if pid <= 0: + return False + try: + os.waitpid(pid, os.WNOHANG) + except: + pass + try: + os.kill(pid, 0) + except OSError as err: + if err.errno == errno.ESRCH: + # ESRCH == No such process + return False + elif err.errno == errno.EPERM: + # EPERM clearly means there's a process to deny access to + return True + else: + # According to "man 2 kill" possible error values are + # (EINVAL, EPERM, ESRCH) + raise + else: + return True + + +def get_textdiff(text1, text2, title1="", title2="", **opts): + "Returns empty string if same or formatted diff" + + diff = "\n".join( + difflib.unified_diff(text1, text2, fromfile=title1, tofile=title2, **opts) + ) + # Clean up line endings + diff = os.linesep.join([s for s in diff.splitlines() if s]) + return diff + + +def difflines(text1, text2, title1="", title2="", **opts): + "Wrapper for get_textdiff to avoid string transformations." + text1 = ("\n".join(text1.rstrip().splitlines()) + "\n").splitlines(1) + text2 = ("\n".join(text2.rstrip().splitlines()) + "\n").splitlines(1) + return get_textdiff(text1, text2, title1, title2, **opts) + + +def get_file(content): + """ + Generates a temporary file in '/tmp' with `content` and returns the file name. + """ + if isinstance(content, list) or isinstance(content, tuple): + content = "\n".join(content) + fde = tempfile.NamedTemporaryFile(mode="w", delete=False) + fname = fde.name + fde.write(content) + fde.close() + return fname + + +def normalize_text(text): + """ + Strips formating spaces/tabs, carriage returns and trailing whitespace. + """ + text = re.sub(r"[ \t]+", " ", text) + text = re.sub(r"\r", "", text) + + # Remove whitespace in the middle of text. + text = re.sub(r"[ \t]+\n", "\n", text) + # Remove whitespace at the end of the text. + text = text.rstrip() + + return text + + +def is_linux(): + """ + Parses unix name output to check if running on GNU/Linux. + + Returns True if running on Linux, returns False otherwise. + """ + + if os.uname()[0] == "Linux": + return True + return False + + +def iproute2_is_vrf_capable(): + """ + Checks if the iproute2 version installed on the system is capable of + handling VRFs by interpreting the output of the 'ip' utility found in PATH. + + Returns True if capability can be detected, returns False otherwise. + """ + + if is_linux(): + try: + subp = subprocess.Popen( + ["ip", "route", "show", "vrf"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + stdin=subprocess.PIPE, + ) + iproute2_err = subp.communicate()[1].splitlines()[0].split()[0] + + if iproute2_err != "Error:": + return True + except Exception: + pass + return False + + +def module_present_linux(module, load): + """ + Returns whether `module` is present. + + If `load` is true, it will try to load it via modprobe. + """ + with open("/proc/modules", "r") as modules_file: + if module.replace("-", "_") in modules_file.read(): + return True + cmd = "/sbin/modprobe {}{}".format("" if load else "-n ", module) + if os.system(cmd) != 0: + return False + else: + return True + + +def module_present_freebsd(module, load): + return True + + +def module_present(module, load=True): + if sys.platform.startswith("linux"): + return module_present_linux(module, load) + elif sys.platform.startswith("freebsd"): + return module_present_freebsd(module, load) + + +def version_cmp(v1, v2): + """ + Compare two version strings and returns: + + * `-1`: if `v1` is less than `v2` + * `0`: if `v1` is equal to `v2` + * `1`: if `v1` is greater than `v2` + + Raises `ValueError` if versions are not well formated. + """ + vregex = r"(?P<whole>\d+(\.(\d+))*)" + v1m = re.match(vregex, v1) + v2m = re.match(vregex, v2) + if v1m is None or v2m is None: + raise ValueError("got a invalid version string") + + # Split values + v1g = v1m.group("whole").split(".") + v2g = v2m.group("whole").split(".") + + # Get the longest version string + vnum = len(v1g) + if len(v2g) > vnum: + vnum = len(v2g) + + # Reverse list because we are going to pop the tail + v1g.reverse() + v2g.reverse() + for _ in range(vnum): + try: + v1n = int(v1g.pop()) + except IndexError: + while v2g: + v2n = int(v2g.pop()) + if v2n > 0: + return -1 + break + + try: + v2n = int(v2g.pop()) + except IndexError: + if v1n > 0: + return 1 + while v1g: + v1n = int(v1g.pop()) + if v1n > 0: + return 1 + break + + if v1n > v2n: + return 1 + if v1n < v2n: + return -1 + return 0 + + +def interface_set_status(node, ifacename, ifaceaction=False, vrf_name=None): + if ifaceaction: + str_ifaceaction = "no shutdown" + else: + str_ifaceaction = "shutdown" + if vrf_name == None: + cmd = 'vtysh -c "configure terminal" -c "interface {0}" -c "{1}"'.format( + ifacename, str_ifaceaction + ) + else: + cmd = ( + 'vtysh -c "configure terminal" -c "interface {0} vrf {1}" -c "{2}"'.format( + ifacename, vrf_name, str_ifaceaction + ) + ) + node.run(cmd) + + +def ip4_route_zebra(node, vrf_name=None): + """ + Gets an output of 'show ip route' command. It can be used + with comparing the output to a reference + """ + if vrf_name == None: + tmp = node.vtysh_cmd("show ip route") + else: + tmp = node.vtysh_cmd("show ip route vrf {0}".format(vrf_name)) + output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp) + + lines = output.splitlines() + header_found = False + while lines and (not lines[0].strip() or not header_found): + if "o - offload failure" in lines[0]: + header_found = True + lines = lines[1:] + return "\n".join(lines) + + +def ip6_route_zebra(node, vrf_name=None): + """ + Retrieves the output of 'show ipv6 route [vrf vrf_name]', then + canonicalizes it by eliding link-locals. + """ + + if vrf_name == None: + tmp = node.vtysh_cmd("show ipv6 route") + else: + tmp = node.vtysh_cmd("show ipv6 route vrf {0}".format(vrf_name)) + + # Mask out timestamp + output = re.sub(r" [0-2][0-9]:[0-5][0-9]:[0-5][0-9]", " XX:XX:XX", tmp) + + # Mask out the link-local addresses + output = re.sub(r"fe80::[^ ]+,", "fe80::XXXX:XXXX:XXXX:XXXX,", output) + + lines = output.splitlines() + header_found = False + while lines and (not lines[0].strip() or not header_found): + if "o - offload failure" in lines[0]: + header_found = True + lines = lines[1:] + + return "\n".join(lines) + + +def proto_name_to_number(protocol): + return { + "bgp": "186", + "isis": "187", + "ospf": "188", + "rip": "189", + "ripng": "190", + "nhrp": "191", + "eigrp": "192", + "ldp": "193", + "sharp": "194", + "pbr": "195", + "static": "196", + "ospf6": "197", + }.get( + protocol, protocol + ) # default return same as input + + +def ip4_route(node): + """ + Gets a structured return of the command 'ip route'. It can be used in + conjunction with json_cmp() to provide accurate assert explanations. + + Return example: + { + '10.0.1.0/24': { + 'dev': 'eth0', + 'via': '172.16.0.1', + 'proto': '188', + }, + '10.0.2.0/24': { + 'dev': 'eth1', + 'proto': 'kernel', + } + } + """ + output = normalize_text(node.run("ip route")).splitlines() + result = {} + for line in output: + columns = line.split(" ") + route = result[columns[0]] = {} + prev = None + for column in columns: + if prev == "dev": + route["dev"] = column + if prev == "via": + route["via"] = column + if prev == "proto": + # translate protocol names back to numbers + route["proto"] = proto_name_to_number(column) + if prev == "metric": + route["metric"] = column + if prev == "scope": + route["scope"] = column + prev = column + + return result + + +def ip4_vrf_route(node): + """ + Gets a structured return of the command 'ip route show vrf {0}-cust1'. + It can be used in conjunction with json_cmp() to provide accurate assert explanations. + + Return example: + { + '10.0.1.0/24': { + 'dev': 'eth0', + 'via': '172.16.0.1', + 'proto': '188', + }, + '10.0.2.0/24': { + 'dev': 'eth1', + 'proto': 'kernel', + } + } + """ + output = normalize_text( + node.run("ip route show vrf {0}-cust1".format(node.name)) + ).splitlines() + + result = {} + for line in output: + columns = line.split(" ") + route = result[columns[0]] = {} + prev = None + for column in columns: + if prev == "dev": + route["dev"] = column + if prev == "via": + route["via"] = column + if prev == "proto": + # translate protocol names back to numbers + route["proto"] = proto_name_to_number(column) + if prev == "metric": + route["metric"] = column + if prev == "scope": + route["scope"] = column + prev = column + + return result + + +def ip6_route(node): + """ + Gets a structured return of the command 'ip -6 route'. It can be used in + conjunction with json_cmp() to provide accurate assert explanations. + + Return example: + { + '2001:db8:1::/64': { + 'dev': 'eth0', + 'proto': '188', + }, + '2001:db8:2::/64': { + 'dev': 'eth1', + 'proto': 'kernel', + } + } + """ + output = normalize_text(node.run("ip -6 route")).splitlines() + result = {} + for line in output: + columns = line.split(" ") + route = result[columns[0]] = {} + prev = None + for column in columns: + if prev == "dev": + route["dev"] = column + if prev == "via": + route["via"] = column + if prev == "proto": + # translate protocol names back to numbers + route["proto"] = proto_name_to_number(column) + if prev == "metric": + route["metric"] = column + if prev == "pref": + route["pref"] = column + prev = column + + return result + + +def ip6_vrf_route(node): + """ + Gets a structured return of the command 'ip -6 route show vrf {0}-cust1'. + It can be used in conjunction with json_cmp() to provide accurate assert explanations. + + Return example: + { + '2001:db8:1::/64': { + 'dev': 'eth0', + 'proto': '188', + }, + '2001:db8:2::/64': { + 'dev': 'eth1', + 'proto': 'kernel', + } + } + """ + output = normalize_text( + node.run("ip -6 route show vrf {0}-cust1".format(node.name)) + ).splitlines() + result = {} + for line in output: + columns = line.split(" ") + route = result[columns[0]] = {} + prev = None + for column in columns: + if prev == "dev": + route["dev"] = column + if prev == "via": + route["via"] = column + if prev == "proto": + # translate protocol names back to numbers + route["proto"] = proto_name_to_number(column) + if prev == "metric": + route["metric"] = column + if prev == "pref": + route["pref"] = column + prev = column + + return result + + +def ip_rules(node): + """ + Gets a structured return of the command 'ip rule'. It can be used in + conjunction with json_cmp() to provide accurate assert explanations. + + Return example: + [ + { + "pref": "0" + "from": "all" + }, + { + "pref": "32766" + "from": "all" + }, + { + "to": "3.4.5.0/24", + "iif": "r1-eth2", + "pref": "304", + "from": "1.2.0.0/16", + "proto": "zebra" + } + ] + """ + output = normalize_text(node.run("ip rule")).splitlines() + result = [] + for line in output: + columns = line.split(" ") + + route = {} + # remove last character, since it is ':' + pref = columns[0][:-1] + route["pref"] = pref + prev = None + for column in columns: + if prev == "from": + route["from"] = column + if prev == "to": + route["to"] = column + if prev == "proto": + route["proto"] = column + if prev == "iif": + route["iif"] = column + if prev == "fwmark": + route["fwmark"] = column + prev = column + + result.append(route) + return result + + +def sleep(amount, reason=None): + """ + Sleep wrapper that registers in the log the amount of sleep + """ + if reason is None: + logger.info("Sleeping for {} seconds".format(amount)) + else: + logger.info(reason + " ({} seconds)".format(amount)) + + time.sleep(amount) + + +def checkAddressSanitizerError(output, router, component, logdir=""): + "Checks for AddressSanitizer in output. If found, then logs it and returns true, false otherwise" + + def processAddressSanitizerError(asanErrorRe, output, router, component): + sys.stderr.write( + "%s: %s triggered an exception by AddressSanitizer\n" % (router, component) + ) + # Sanitizer Error found in log + pidMark = asanErrorRe.group(1) + addressSanitizerLog = re.search( + "%s(.*)%s" % (pidMark, pidMark), output, re.DOTALL + ) + if addressSanitizerLog: + # Find Calling Test. Could be multiple steps back + testframe = sys._current_frames().values()[0] + level = 0 + while level < 10: + test = os.path.splitext( + os.path.basename(testframe.f_globals["__file__"]) + )[0] + if (test != "topotest") and (test != "topogen"): + # Found the calling test + callingTest = os.path.basename(testframe.f_globals["__file__"]) + break + level = level + 1 + testframe = testframe.f_back + if level >= 10: + # somehow couldn't find the test script. + callingTest = "unknownTest" + # + # Now finding Calling Procedure + level = 0 + while level < 20: + callingProc = sys._getframe(level).f_code.co_name + if ( + (callingProc != "processAddressSanitizerError") + and (callingProc != "checkAddressSanitizerError") + and (callingProc != "checkRouterCores") + and (callingProc != "stopRouter") + and (callingProc != "stop") + and (callingProc != "stop_topology") + and (callingProc != "checkRouterRunning") + and (callingProc != "check_router_running") + and (callingProc != "routers_have_failure") + ): + # Found the calling test + break + level = level + 1 + if level >= 20: + # something wrong - couldn't found the calling test function + callingProc = "unknownProc" + with open("/tmp/AddressSanitzer.txt", "a") as addrSanFile: + sys.stderr.write( + "AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" + % (callingTest, callingProc, router) + ) + sys.stderr.write( + "\n".join(addressSanitizerLog.group(1).splitlines()) + "\n" + ) + addrSanFile.write("## Error: %s\n\n" % asanErrorRe.group(2)) + addrSanFile.write( + "### AddressSanitizer error in topotest `%s`, test `%s`, router `%s`\n\n" + % (callingTest, callingProc, router) + ) + addrSanFile.write( + " " + + "\n ".join(addressSanitizerLog.group(1).splitlines()) + + "\n" + ) + addrSanFile.write("\n---------------\n") + return + + addressSanitizerError = re.search( + r"(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ", output + ) + if addressSanitizerError: + processAddressSanitizerError(addressSanitizerError, output, router, component) + return True + + # No Address Sanitizer Error in Output. Now check for AddressSanitizer daemon file + if logdir: + filepattern = logdir + "/" + router + "/" + component + ".asan.*" + logger.debug( + "Log check for %s on %s, pattern %s\n" % (component, router, filepattern) + ) + for file in glob.glob(filepattern): + with open(file, "r") as asanErrorFile: + asanError = asanErrorFile.read() + addressSanitizerError = re.search( + r"(==[0-9]+==)ERROR: AddressSanitizer: ([^\s]*) ", asanError + ) + if addressSanitizerError: + processAddressSanitizerError( + addressSanitizerError, asanError, router, component + ) + return True + return False + + +def _sysctl_atleast(commander, variable, min_value): + if isinstance(min_value, tuple): + min_value = list(min_value) + is_list = isinstance(min_value, list) + + sval = commander.cmd_raises("sysctl -n " + variable).strip() + if is_list: + cur_val = [int(x) for x in sval.split()] + else: + cur_val = int(sval) + + set_value = False + if is_list: + for i, v in enumerate(cur_val): + if v < min_value[i]: + set_value = True + else: + min_value[i] = v + else: + if cur_val < min_value: + set_value = True + if set_value: + if is_list: + valstr = " ".join([str(x) for x in min_value]) + else: + valstr = str(min_value) + logger.info("Increasing sysctl %s from %s to %s", variable, cur_val, valstr) + commander.cmd_raises('sysctl -w {}="{}"\n'.format(variable, valstr)) + + +def _sysctl_assure(commander, variable, value): + if isinstance(value, tuple): + value = list(value) + is_list = isinstance(value, list) + + sval = commander.cmd_raises("sysctl -n " + variable).strip() + if is_list: + cur_val = [int(x) for x in sval.split()] + else: + cur_val = sval + + set_value = False + if is_list: + for i, v in enumerate(cur_val): + if v != value[i]: + set_value = True + else: + value[i] = v + else: + if cur_val != str(value): + set_value = True + + if set_value: + if is_list: + valstr = " ".join([str(x) for x in value]) + else: + valstr = str(value) + logger.info("Changing sysctl %s from %s to %s", variable, cur_val, valstr) + commander.cmd_raises('sysctl -w {}="{}"\n'.format(variable, valstr)) + + +def sysctl_atleast(commander, variable, min_value, raises=False): + try: + if commander is None: + commander = micronet.Commander("topotest") + return _sysctl_atleast(commander, variable, min_value) + except subprocess.CalledProcessError as error: + logger.warning( + "%s: Failed to assure sysctl min value %s = %s", + commander, + variable, + min_value, + ) + if raises: + raise + + +def sysctl_assure(commander, variable, value, raises=False): + try: + if commander is None: + commander = micronet.Commander("topotest") + return _sysctl_assure(commander, variable, value) + except subprocess.CalledProcessError as error: + logger.warning( + "%s: Failed to assure sysctl value %s = %s", + commander, + variable, + value, + exc_info=True, + ) + if raises: + raise + + +def rlimit_atleast(rname, min_value, raises=False): + try: + cval = resource.getrlimit(rname) + soft, hard = cval + if soft < min_value: + nval = (min_value, hard if min_value < hard else min_value) + logger.info("Increasing rlimit %s from %s to %s", rname, cval, nval) + resource.setrlimit(rname, nval) + except subprocess.CalledProcessError as error: + logger.warning( + "Failed to assure rlimit [%s] = %s", rname, min_value, exc_info=True + ) + if raises: + raise + + +def fix_netns_limits(ns): + + # Maximum read and write socket buffer sizes + sysctl_atleast(ns, "net.ipv4.tcp_rmem", [10 * 1024, 87380, 16 * 2 ** 20]) + sysctl_atleast(ns, "net.ipv4.tcp_wmem", [10 * 1024, 87380, 16 * 2 ** 20]) + + sysctl_assure(ns, "net.ipv4.conf.all.rp_filter", 0) + sysctl_assure(ns, "net.ipv4.conf.default.rp_filter", 0) + sysctl_assure(ns, "net.ipv4.conf.lo.rp_filter", 0) + + sysctl_assure(ns, "net.ipv4.conf.all.forwarding", 1) + sysctl_assure(ns, "net.ipv4.conf.default.forwarding", 1) + + # XXX if things fail look here as this wasn't done previously + sysctl_assure(ns, "net.ipv6.conf.all.forwarding", 1) + sysctl_assure(ns, "net.ipv6.conf.default.forwarding", 1) + + # ARP + sysctl_assure(ns, "net.ipv4.conf.default.arp_announce", 2) + sysctl_assure(ns, "net.ipv4.conf.default.arp_notify", 1) + # Setting this to 1 breaks topotests that rely on lo addresses being proxy arp'd for + sysctl_assure(ns, "net.ipv4.conf.default.arp_ignore", 0) + sysctl_assure(ns, "net.ipv4.conf.all.arp_announce", 2) + sysctl_assure(ns, "net.ipv4.conf.all.arp_notify", 1) + # Setting this to 1 breaks topotests that rely on lo addresses being proxy arp'd for + sysctl_assure(ns, "net.ipv4.conf.all.arp_ignore", 0) + + sysctl_assure(ns, "net.ipv4.icmp_errors_use_inbound_ifaddr", 1) + + # Keep ipv6 permanent addresses on an admin down + sysctl_assure(ns, "net.ipv6.conf.all.keep_addr_on_down", 1) + if version_cmp(platform.release(), "4.20") >= 0: + sysctl_assure(ns, "net.ipv6.route.skip_notify_on_dev_down", 1) + + sysctl_assure(ns, "net.ipv4.conf.all.ignore_routes_with_linkdown", 1) + sysctl_assure(ns, "net.ipv6.conf.all.ignore_routes_with_linkdown", 1) + + # igmp + sysctl_atleast(ns, "net.ipv4.igmp_max_memberships", 1000) + + # Use neigh information on selection of nexthop for multipath hops + sysctl_assure(ns, "net.ipv4.fib_multipath_use_neigh", 1) + + +def fix_host_limits(): + """Increase system limits.""" + + rlimit_atleast(resource.RLIMIT_NPROC, 8 * 1024) + rlimit_atleast(resource.RLIMIT_NOFILE, 16 * 1024) + sysctl_atleast(None, "fs.file-max", 16 * 1024) + sysctl_atleast(None, "kernel.pty.max", 16 * 1024) + + # Enable coredumps + # Original on ubuntu 17.x, but apport won't save as in namespace + # |/usr/share/apport/apport %p %s %c %d %P + sysctl_assure(None, "kernel.core_pattern", "%e_core-sig_%s-pid_%p.dmp") + sysctl_assure(None, "kernel.core_uses_pid", 1) + sysctl_assure(None, "fs.suid_dumpable", 1) + + # Maximum connection backlog + sysctl_atleast(None, "net.core.netdev_max_backlog", 4 * 1024) + + # Maximum read and write socket buffer sizes + sysctl_atleast(None, "net.core.rmem_max", 16 * 2 ** 20) + sysctl_atleast(None, "net.core.wmem_max", 16 * 2 ** 20) + + # Garbage Collection Settings for ARP and Neighbors + sysctl_atleast(None, "net.ipv4.neigh.default.gc_thresh2", 4 * 1024) + sysctl_atleast(None, "net.ipv4.neigh.default.gc_thresh3", 8 * 1024) + sysctl_atleast(None, "net.ipv6.neigh.default.gc_thresh2", 4 * 1024) + sysctl_atleast(None, "net.ipv6.neigh.default.gc_thresh3", 8 * 1024) + # Hold entries for 10 minutes + sysctl_assure(None, "net.ipv4.neigh.default.base_reachable_time_ms", 10 * 60 * 1000) + sysctl_assure(None, "net.ipv6.neigh.default.base_reachable_time_ms", 10 * 60 * 1000) + + # igmp + sysctl_assure(None, "net.ipv4.neigh.default.mcast_solicit", 10) + + # MLD + sysctl_atleast(None, "net.ipv6.mld_max_msf", 512) + + # Increase routing table size to 128K + sysctl_atleast(None, "net.ipv4.route.max_size", 128 * 1024) + sysctl_atleast(None, "net.ipv6.route.max_size", 128 * 1024) + + +def setup_node_tmpdir(logdir, name): + # Cleanup old log, valgrind, and core files. + subprocess.check_call( + "rm -rf {0}/{1}.valgrind.* {1}.*.asan {0}/{1}/".format(logdir, name), shell=True + ) + + # Setup the per node directory. + nodelogdir = "{}/{}".format(logdir, name) + subprocess.check_call( + "mkdir -p {0} && chmod 1777 {0}".format(nodelogdir), shell=True + ) + logfile = "{0}/{1}.log".format(logdir, name) + return logfile + + +class Router(Node): + "A Node with IPv4/IPv6 forwarding enabled" + + def __init__(self, name, **params): + + # Backward compatibility: + # Load configuration defaults like topogen. + self.config_defaults = configparser.ConfigParser( + defaults={ + "verbosity": "info", + "frrdir": "/usr/lib/frr", + "routertype": "frr", + "memleak_path": "", + } + ) + + self.config_defaults.read( + os.path.join(os.path.dirname(os.path.realpath(__file__)), "../pytest.ini") + ) + + # If this topology is using old API and doesn't have logdir + # specified, then attempt to generate an unique logdir. + self.logdir = params.get("logdir") + if self.logdir is None: + self.logdir = get_logs_path(g_extra_config["rundir"]) + + if not params.get("logger"): + # If logger is present topogen has already set this up + logfile = setup_node_tmpdir(self.logdir, name) + l = topolog.get_logger(name, log_level="debug", target=logfile) + params["logger"] = l + + super(Router, self).__init__(name, **params) + + self.daemondir = None + self.hasmpls = False + self.routertype = "frr" + self.unified_config = None + self.daemons = { + "zebra": 0, + "ripd": 0, + "ripngd": 0, + "ospfd": 0, + "ospf6d": 0, + "isisd": 0, + "bgpd": 0, + "pimd": 0, + "pim6d": 0, + "ldpd": 0, + "eigrpd": 0, + "nhrpd": 0, + "staticd": 0, + "bfdd": 0, + "sharpd": 0, + "babeld": 0, + "pbrd": 0, + "pathd": 0, + "snmpd": 0, + } + self.daemons_options = {"zebra": ""} + self.reportCores = True + self.version = None + + self.ns_cmd = "sudo nsenter -a -t {} ".format(self.pid) + try: + # Allow escaping from running inside docker + cgroup = open("/proc/1/cgroup").read() + m = re.search("[0-9]+:cpuset:/docker/([a-f0-9]+)", cgroup) + if m: + self.ns_cmd = "docker exec -it {} ".format(m.group(1)) + self.ns_cmd + except IOError: + pass + else: + logger.debug("CMD to enter {}: {}".format(self.name, self.ns_cmd)) + + def _config_frr(self, **params): + "Configure FRR binaries" + self.daemondir = params.get("frrdir") + if self.daemondir is None: + self.daemondir = self.config_defaults.get("topogen", "frrdir") + + zebra_path = os.path.join(self.daemondir, "zebra") + if not os.path.isfile(zebra_path): + raise Exception("FRR zebra binary doesn't exist at {}".format(zebra_path)) + + # pylint: disable=W0221 + # Some params are only meaningful for the parent class. + def config(self, **params): + super(Router, self).config(**params) + + # User did not specify the daemons directory, try to autodetect it. + self.daemondir = params.get("daemondir") + if self.daemondir is None: + self.routertype = params.get( + "routertype", self.config_defaults.get("topogen", "routertype") + ) + self._config_frr(**params) + else: + # Test the provided path + zpath = os.path.join(self.daemondir, "zebra") + if not os.path.isfile(zpath): + raise Exception("No zebra binary found in {}".format(zpath)) + # Allow user to specify routertype when the path was specified. + if params.get("routertype") is not None: + self.routertype = params.get("routertype") + + # Set ownership of config files + self.cmd("chown {0}:{0}vty /etc/{0}".format(self.routertype)) + + def terminate(self): + # Stop running FRR daemons + self.stopRouter() + super(Router, self).terminate() + os.system("chmod -R go+rw " + self.logdir) + + # Return count of running daemons + def listDaemons(self): + ret = [] + rc, stdout, _ = self.cmd_status( + "ls -1 /var/run/%s/*.pid" % self.routertype, warn=False + ) + if rc: + return ret + for d in stdout.strip().split("\n"): + pidfile = d.strip() + try: + pid = int(self.cmd_raises("cat %s" % pidfile, warn=False).strip()) + name = os.path.basename(pidfile[:-4]) + + # probably not compatible with bsd. + rc, _, _ = self.cmd_status("test -d /proc/{}".format(pid), warn=False) + if rc: + logger.warning( + "%s: %s exited leaving pidfile %s (%s)", + self.name, + name, + pidfile, + pid, + ) + self.cmd("rm -- " + pidfile) + else: + ret.append((name, pid)) + except (subprocess.CalledProcessError, ValueError): + pass + return ret + + def stopRouter(self, assertOnError=True, minErrorVersion="5.1"): + # Stop Running FRR Daemons + running = self.listDaemons() + if not running: + return "" + + logger.info("%s: stopping %s", self.name, ", ".join([x[0] for x in running])) + for name, pid in running: + logger.info("{}: sending SIGTERM to {}".format(self.name, name)) + try: + os.kill(pid, signal.SIGTERM) + except OSError as err: + logger.info( + "%s: could not kill %s (%s): %s", self.name, name, pid, str(err) + ) + + running = self.listDaemons() + if running: + for _ in range(0, 30): + sleep( + 0.5, + "{}: waiting for daemons stopping: {}".format( + self.name, ", ".join([x[0] for x in running]) + ), + ) + running = self.listDaemons() + if not running: + break + + if not running: + return "" + + logger.warning( + "%s: sending SIGBUS to: %s", self.name, ", ".join([x[0] for x in running]) + ) + for name, pid in running: + pidfile = "/var/run/{}/{}.pid".format(self.routertype, name) + logger.info("%s: killing %s", self.name, name) + self.cmd("kill -SIGBUS %d" % pid) + self.cmd("rm -- " + pidfile) + + sleep( + 0.5, "%s: waiting for daemons to exit/core after initial SIGBUS" % self.name + ) + + errors = self.checkRouterCores(reportOnce=True) + if self.checkRouterVersion("<", minErrorVersion): + # ignore errors in old versions + errors = "" + if assertOnError and (errors is not None) and len(errors) > 0: + assert "Errors found - details follow:" == 0, errors + return errors + + def removeIPs(self): + for interface in self.intfNames(): + try: + self.intf_ip_cmd(interface, "ip address flush " + interface) + except Exception as ex: + logger.error("%s can't remove IPs %s", self, str(ex)) + # pdb.set_trace() + # assert False, "can't remove IPs %s" % str(ex) + + def checkCapability(self, daemon, param): + if param is not None: + daemon_path = os.path.join(self.daemondir, daemon) + daemon_search_option = param.replace("-", "") + output = self.cmd( + "{0} -h | grep {1}".format(daemon_path, daemon_search_option) + ) + if daemon_search_option not in output: + return False + return True + + def loadConf(self, daemon, source=None, param=None): + """Enabled and set config for a daemon. + + Arranges for loading of daemon configuration from the specified source. Possible + `source` values are `None` for an empty config file, a path name which is used + directly, or a file name with no path components which is first looked for + directly and then looked for under a sub-directory named after router. + """ + + # Unfortunately this API allowsfor source to not exist for any and all routers. + if source: + head, tail = os.path.split(source) + if not head and not self.path_exists(tail): + script_dir = os.environ["PYTEST_TOPOTEST_SCRIPTDIR"] + router_relative = os.path.join(script_dir, self.name, tail) + if self.path_exists(router_relative): + source = router_relative + self.logger.info( + "using router relative configuration: {}".format(source) + ) + + # print "Daemons before:", self.daemons + if daemon in self.daemons.keys() or daemon == "frr": + if daemon == "frr": + self.unified_config = 1 + else: + self.daemons[daemon] = 1 + if param is not None: + self.daemons_options[daemon] = param + conf_file = "/etc/{}/{}.conf".format(self.routertype, daemon) + if source is None or not os.path.exists(source): + if daemon == "frr" or not self.unified_config: + self.cmd_raises("rm -f " + conf_file) + self.cmd_raises("touch " + conf_file) + else: + self.cmd_raises("cp {} {}".format(source, conf_file)) + + if not self.unified_config or daemon == "frr": + self.cmd_raises("chown {0}:{0} {1}".format(self.routertype, conf_file)) + self.cmd_raises("chmod 664 {}".format(conf_file)) + + if (daemon == "snmpd") and (self.routertype == "frr"): + # /etc/snmp is private mount now + self.cmd('echo "agentXSocket /etc/frr/agentx" >> /etc/snmp/frr.conf') + self.cmd('echo "mibs +ALL" > /etc/snmp/snmp.conf') + + if (daemon == "zebra") and (self.daemons["staticd"] == 0): + # Add staticd with zebra - if it exists + try: + staticd_path = os.path.join(self.daemondir, "staticd") + except: + pdb.set_trace() + + if os.path.isfile(staticd_path): + self.daemons["staticd"] = 1 + self.daemons_options["staticd"] = "" + # Auto-Started staticd has no config, so it will read from zebra config + else: + logger.info("No daemon {} known".format(daemon)) + # print "Daemons after:", self.daemons + + def runInWindow(self, cmd, title=None): + return self.run_in_window(cmd, title) + + def startRouter(self, tgen=None): + if self.unified_config: + self.cmd( + 'echo "service integrated-vtysh-config" >> /etc/%s/vtysh.conf' + % self.routertype + ) + else: + # Disable integrated-vtysh-config + self.cmd( + 'echo "no service integrated-vtysh-config" >> /etc/%s/vtysh.conf' + % self.routertype + ) + + self.cmd( + "chown %s:%svty /etc/%s/vtysh.conf" + % (self.routertype, self.routertype, self.routertype) + ) + # TODO remove the following lines after all tests are migrated to Topogen. + # Try to find relevant old logfiles in /tmp and delete them + map(os.remove, glob.glob("{}/{}/*.log".format(self.logdir, self.name))) + # Remove old core files + map(os.remove, glob.glob("{}/{}/*.dmp".format(self.logdir, self.name))) + # Remove IP addresses from OS first - we have them in zebra.conf + self.removeIPs() + # If ldp is used, check for LDP to be compiled and Linux Kernel to be 4.5 or higher + # No error - but return message and skip all the tests + if self.daemons["ldpd"] == 1: + ldpd_path = os.path.join(self.daemondir, "ldpd") + if not os.path.isfile(ldpd_path): + logger.info("LDP Test, but no ldpd compiled or installed") + return "LDP Test, but no ldpd compiled or installed" + + if version_cmp(platform.release(), "4.5") < 0: + logger.info("LDP Test need Linux Kernel 4.5 minimum") + return "LDP Test need Linux Kernel 4.5 minimum" + # Check if have mpls + if tgen != None: + self.hasmpls = tgen.hasmpls + if self.hasmpls != True: + logger.info( + "LDP/MPLS Tests will be skipped, platform missing module(s)" + ) + else: + # Test for MPLS Kernel modules available + self.hasmpls = False + if not module_present("mpls-router"): + logger.info( + "MPLS tests will not run (missing mpls-router kernel module)" + ) + elif not module_present("mpls-iptunnel"): + logger.info( + "MPLS tests will not run (missing mpls-iptunnel kernel module)" + ) + else: + self.hasmpls = True + if self.hasmpls != True: + return "LDP/MPLS Tests need mpls kernel modules" + + # Really want to use sysctl_atleast here, but only when MPLS is actually being + # used + self.cmd("echo 100000 > /proc/sys/net/mpls/platform_labels") + + shell_routers = g_extra_config["shell"] + if "all" in shell_routers or self.name in shell_routers: + self.run_in_window(os.getenv("SHELL", "bash"), title="sh-%s" % self.name) + + if self.daemons["eigrpd"] == 1: + eigrpd_path = os.path.join(self.daemondir, "eigrpd") + if not os.path.isfile(eigrpd_path): + logger.info("EIGRP Test, but no eigrpd compiled or installed") + return "EIGRP Test, but no eigrpd compiled or installed" + + if self.daemons["bfdd"] == 1: + bfdd_path = os.path.join(self.daemondir, "bfdd") + if not os.path.isfile(bfdd_path): + logger.info("BFD Test, but no bfdd compiled or installed") + return "BFD Test, but no bfdd compiled or installed" + + status = self.startRouterDaemons(tgen=tgen) + + vtysh_routers = g_extra_config["vtysh"] + if "all" in vtysh_routers or self.name in vtysh_routers: + self.run_in_window("vtysh", title="vt-%s" % self.name) + + if self.unified_config: + self.cmd("vtysh -f /etc/frr/frr.conf") + + return status + + def getStdErr(self, daemon): + return self.getLog("err", daemon) + + def getStdOut(self, daemon): + return self.getLog("out", daemon) + + def getLog(self, log, daemon): + return self.cmd("cat {}/{}/{}.{}".format(self.logdir, self.name, daemon, log)) + + def startRouterDaemons(self, daemons=None, tgen=None): + "Starts FRR daemons for this router." + + asan_abort = g_extra_config["asan_abort"] + gdb_breakpoints = g_extra_config["gdb_breakpoints"] + gdb_daemons = g_extra_config["gdb_daemons"] + gdb_routers = g_extra_config["gdb_routers"] + valgrind_extra = g_extra_config["valgrind_extra"] + valgrind_memleaks = g_extra_config["valgrind_memleaks"] + strace_daemons = g_extra_config["strace_daemons"] + + # Get global bundle data + if not self.path_exists("/etc/frr/support_bundle_commands.conf"): + # Copy global value if was covered by namespace mount + bundle_data = "" + if os.path.exists("/etc/frr/support_bundle_commands.conf"): + with open("/etc/frr/support_bundle_commands.conf", "r") as rf: + bundle_data = rf.read() + self.cmd_raises( + "cat > /etc/frr/support_bundle_commands.conf", + stdin=bundle_data, + ) + + # Starts actual daemons without init (ie restart) + # cd to per node directory + self.cmd("install -m 775 -o frr -g frr -d {}/{}".format(self.logdir, self.name)) + self.set_cwd("{}/{}".format(self.logdir, self.name)) + self.cmd("umask 000") + + # Re-enable to allow for report per run + self.reportCores = True + + # XXX: glue code forward ported from removed function. + if self.version == None: + self.version = self.cmd( + os.path.join(self.daemondir, "bgpd") + " -v" + ).split()[2] + logger.info("{}: running version: {}".format(self.name, self.version)) + # If `daemons` was specified then some upper API called us with + # specific daemons, otherwise just use our own configuration. + daemons_list = [] + if daemons is not None: + daemons_list = daemons + else: + # Append all daemons configured. + for daemon in self.daemons: + if self.daemons[daemon] == 1: + daemons_list.append(daemon) + + def start_daemon(daemon, extra_opts=None): + daemon_opts = self.daemons_options.get(daemon, "") + rediropt = " > {0}.out 2> {0}.err".format(daemon) + if daemon == "snmpd": + binary = "/usr/sbin/snmpd" + cmdenv = "" + cmdopt = "{} -C -c /etc/frr/snmpd.conf -p ".format( + daemon_opts + ) + "/var/run/{}/snmpd.pid -x /etc/frr/agentx".format(self.routertype) + else: + binary = os.path.join(self.daemondir, daemon) + + cmdenv = "ASAN_OPTIONS=" + if asan_abort: + cmdenv = "abort_on_error=1:" + cmdenv += "log_path={0}/{1}.{2}.asan ".format( + self.logdir, self.name, daemon + ) + + if valgrind_memleaks: + this_dir = os.path.dirname( + os.path.abspath(os.path.realpath(__file__)) + ) + supp_file = os.path.abspath( + os.path.join(this_dir, "../../../tools/valgrind.supp") + ) + cmdenv += " /usr/bin/valgrind --num-callers=50 --log-file={1}/{2}.valgrind.{0}.%p --leak-check=full --suppressions={3}".format( + daemon, self.logdir, self.name, supp_file + ) + if valgrind_extra: + cmdenv += ( + " --gen-suppressions=all --expensive-definedness-checks=yes" + ) + elif daemon in strace_daemons or "all" in strace_daemons: + cmdenv = "strace -f -D -o {1}/{2}.strace.{0} ".format( + daemon, self.logdir, self.name + ) + + cmdopt = "{} --command-log-always --log file:{}.log --log-level debug".format( + daemon_opts, daemon + ) + if extra_opts: + cmdopt += " " + extra_opts + + if ( + (gdb_routers or gdb_daemons) + and ( + not gdb_routers or self.name in gdb_routers or "all" in gdb_routers + ) + and (not gdb_daemons or daemon in gdb_daemons or "all" in gdb_daemons) + ): + if daemon == "snmpd": + cmdopt += " -f " + + cmdopt += rediropt + gdbcmd = "sudo -E gdb " + binary + if gdb_breakpoints: + gdbcmd += " -ex 'set breakpoint pending on'" + for bp in gdb_breakpoints: + gdbcmd += " -ex 'b {}'".format(bp) + gdbcmd += " -ex 'run {}'".format(cmdopt) + + self.run_in_window(gdbcmd, daemon) + + logger.info( + "%s: %s %s launched in gdb window", self, self.routertype, daemon + ) + else: + if daemon != "snmpd": + cmdopt += " -d " + cmdopt += rediropt + + try: + self.cmd_raises(" ".join([cmdenv, binary, cmdopt]), warn=False) + except subprocess.CalledProcessError as error: + self.logger.error( + '%s: Failed to launch "%s" daemon (%d) using: %s%s%s:', + self, + daemon, + error.returncode, + error.cmd, + '\n:stdout: "{}"'.format(error.stdout.strip()) + if error.stdout + else "", + '\n:stderr: "{}"'.format(error.stderr.strip()) + if error.stderr + else "", + ) + else: + logger.info("%s: %s %s started", self, self.routertype, daemon) + + # Start Zebra first + if "zebra" in daemons_list: + start_daemon("zebra", "-s 90000000") + while "zebra" in daemons_list: + daemons_list.remove("zebra") + + # Start staticd next if required + if "staticd" in daemons_list: + start_daemon("staticd") + while "staticd" in daemons_list: + daemons_list.remove("staticd") + + if "snmpd" in daemons_list: + # Give zerbra a chance to configure interface addresses that snmpd daemon + # may then use. + time.sleep(2) + + start_daemon("snmpd") + while "snmpd" in daemons_list: + daemons_list.remove("snmpd") + + if daemons is None: + # Fix Link-Local Addresses on initial startup + # Somehow (on Mininet only), Zebra removes the IPv6 Link-Local addresses on start. Fix this + _, output, _ = self.cmd_status( + "for i in `ls /sys/class/net/` ; do mac=`cat /sys/class/net/$i/address`; echo $i: $mac; [ -z \"$mac\" ] && continue; IFS=':'; set $mac; unset IFS; ip address add dev $i scope link fe80::$(printf %02x $((0x$1 ^ 2)))$2:${3}ff:fe$4:$5$6/64; done", + stderr=subprocess.STDOUT, + ) + logger.debug("Set MACs:\n%s", output) + + # Now start all the other daemons + for daemon in daemons_list: + if self.daemons[daemon] == 0: + continue + start_daemon(daemon) + + # Check if daemons are running. + rundaemons = self.cmd("ls -1 /var/run/%s/*.pid" % self.routertype) + if re.search(r"No such file or directory", rundaemons): + return "Daemons are not running" + + # Update the permissions on the log files + self.cmd("chown frr:frr -R {}/{}".format(self.logdir, self.name)) + self.cmd("chmod ug+rwX,o+r -R {}/{}".format(self.logdir, self.name)) + + return "" + + def killRouterDaemons( + self, daemons, wait=True, assertOnError=True, minErrorVersion="5.1" + ): + # Kill Running FRR + # Daemons(user specified daemon only) using SIGKILL + rundaemons = self.cmd("ls -1 /var/run/%s/*.pid" % self.routertype) + errors = "" + daemonsNotRunning = [] + if re.search(r"No such file or directory", rundaemons): + return errors + for daemon in daemons: + if rundaemons is not None and daemon in rundaemons: + numRunning = 0 + dmns = rundaemons.split("\n") + # Exclude empty string at end of list + for d in dmns[:-1]: + if re.search(r"%s" % daemon, d): + daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip() + if daemonpid.isdigit() and pid_exists(int(daemonpid)): + logger.info( + "{}: killing {}".format( + self.name, + os.path.basename(d.rstrip().rsplit(".", 1)[0]), + ) + ) + self.cmd("kill -9 %s" % daemonpid) + if pid_exists(int(daemonpid)): + numRunning += 1 + while wait and numRunning > 0: + sleep( + 2, + "{}: waiting for {} daemon to be stopped".format( + self.name, daemon + ), + ) + + # 2nd round of kill if daemons didn't exit + for d in dmns[:-1]: + if re.search(r"%s" % daemon, d): + daemonpid = self.cmd("cat %s" % d.rstrip()).rstrip() + if daemonpid.isdigit() and pid_exists( + int(daemonpid) + ): + logger.info( + "{}: killing {}".format( + self.name, + os.path.basename( + d.rstrip().rsplit(".", 1)[0] + ), + ) + ) + self.cmd("kill -9 %s" % daemonpid) + if daemonpid.isdigit() and not pid_exists( + int(daemonpid) + ): + numRunning -= 1 + self.cmd("rm -- {}".format(d.rstrip())) + if wait: + errors = self.checkRouterCores(reportOnce=True) + if self.checkRouterVersion("<", minErrorVersion): + # ignore errors in old versions + errors = "" + if assertOnError and len(errors) > 0: + assert "Errors found - details follow:" == 0, errors + else: + daemonsNotRunning.append(daemon) + if len(daemonsNotRunning) > 0: + errors = errors + "Daemons are not running", daemonsNotRunning + + return errors + + def checkRouterCores(self, reportLeaks=True, reportOnce=False): + if reportOnce and not self.reportCores: + return + reportMade = False + traces = "" + for daemon in self.daemons: + if self.daemons[daemon] == 1: + # Look for core file + corefiles = glob.glob( + "{}/{}/{}_core*.dmp".format(self.logdir, self.name, daemon) + ) + if len(corefiles) > 0: + backtrace = gdb_core(self, daemon, corefiles) + traces = ( + traces + + "\n%s: %s crashed. Core file found - Backtrace follows:\n%s" + % (self.name, daemon, backtrace) + ) + reportMade = True + elif reportLeaks: + log = self.getStdErr(daemon) + if "memstats" in log: + sys.stderr.write( + "%s: %s has memory leaks:\n" % (self.name, daemon) + ) + traces = traces + "\n%s: %s has memory leaks:\n" % ( + self.name, + daemon, + ) + log = re.sub("core_handler: ", "", log) + log = re.sub( + r"(showing active allocations in memory group [a-zA-Z0-9]+)", + r"\n ## \1", + log, + ) + log = re.sub("memstats: ", " ", log) + sys.stderr.write(log) + reportMade = True + # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found + if checkAddressSanitizerError( + self.getStdErr(daemon), self.name, daemon, self.logdir + ): + sys.stderr.write( + "%s: Daemon %s killed by AddressSanitizer" % (self.name, daemon) + ) + traces = traces + "\n%s: Daemon %s killed by AddressSanitizer" % ( + self.name, + daemon, + ) + reportMade = True + if reportMade: + self.reportCores = False + return traces + + def checkRouterRunning(self): + "Check if router daemons are running and collect crashinfo they don't run" + + global fatal_error + + daemonsRunning = self.cmd( + 'vtysh -c "show logging" | grep "Logging configuration for"' + ) + # Look for AddressSanitizer Errors in vtysh output and append to /tmp/AddressSanitzer.txt if found + if checkAddressSanitizerError(daemonsRunning, self.name, "vtysh"): + return "%s: vtysh killed by AddressSanitizer" % (self.name) + + for daemon in self.daemons: + if daemon == "snmpd": + continue + if (self.daemons[daemon] == 1) and not (daemon in daemonsRunning): + sys.stderr.write("%s: Daemon %s not running\n" % (self.name, daemon)) + if daemon == "staticd": + sys.stderr.write( + "You may have a copy of staticd installed but are attempting to test against\n" + ) + sys.stderr.write( + "a version of FRR that does not have staticd, please cleanup the install dir\n" + ) + + # Look for core file + corefiles = glob.glob( + "{}/{}/{}_core*.dmp".format(self.logdir, self.name, daemon) + ) + if len(corefiles) > 0: + gdb_core(self, daemon, corefiles) + else: + # No core found - If we find matching logfile in /tmp, then print last 20 lines from it. + if os.path.isfile( + "{}/{}/{}.log".format(self.logdir, self.name, daemon) + ): + log_tail = subprocess.check_output( + [ + "tail -n20 {}/{}/{}.log 2> /dev/null".format( + self.logdir, self.name, daemon + ) + ], + shell=True, + ) + sys.stderr.write( + "\nFrom %s %s %s log file:\n" + % (self.routertype, self.name, daemon) + ) + sys.stderr.write("%s\n" % log_tail) + + # Look for AddressSanitizer Errors and append to /tmp/AddressSanitzer.txt if found + if checkAddressSanitizerError( + self.getStdErr(daemon), self.name, daemon, self.logdir + ): + return "%s: Daemon %s not running - killed by AddressSanitizer" % ( + self.name, + daemon, + ) + + return "%s: Daemon %s not running" % (self.name, daemon) + return "" + + def checkRouterVersion(self, cmpop, version): + """ + Compares router version using operation `cmpop` with `version`. + Valid `cmpop` values: + * `>=`: has the same version or greater + * '>': has greater version + * '=': has the same version + * '<': has a lesser version + * '<=': has the same version or lesser + + Usage example: router.checkRouterVersion('>', '1.0') + """ + + # Make sure we have version information first + if self.version == None: + self.version = self.cmd( + os.path.join(self.daemondir, "bgpd") + " -v" + ).split()[2] + logger.info("{}: running version: {}".format(self.name, self.version)) + + rversion = self.version + if rversion == None: + return False + + result = version_cmp(rversion, version) + if cmpop == ">=": + return result >= 0 + if cmpop == ">": + return result > 0 + if cmpop == "=": + return result == 0 + if cmpop == "<": + return result < 0 + if cmpop == "<": + return result < 0 + if cmpop == "<=": + return result <= 0 + + def get_ipv6_linklocal(self): + "Get LinkLocal Addresses from interfaces" + + linklocal = [] + + ifaces = self.cmd("ip -6 address") + # Fix newlines (make them all the same) + ifaces = ("\n".join(ifaces.splitlines()) + "\n").splitlines() + interface = "" + ll_per_if_count = 0 + for line in ifaces: + m = re.search("[0-9]+: ([^:@]+)[-@a-z0-9:]+ <", line) + if m: + interface = m.group(1) + ll_per_if_count = 0 + m = re.search( + "inet6 (fe80::[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)[/0-9]* scope link", + line, + ) + if m: + local = m.group(1) + ll_per_if_count += 1 + if ll_per_if_count > 1: + linklocal += [["%s-%s" % (interface, ll_per_if_count), local]] + else: + linklocal += [[interface, local]] + return linklocal + + def daemon_available(self, daemon): + "Check if specified daemon is installed (and for ldp if kernel supports MPLS)" + + daemon_path = os.path.join(self.daemondir, daemon) + if not os.path.isfile(daemon_path): + return False + if daemon == "ldpd": + if version_cmp(platform.release(), "4.5") < 0: + return False + if not module_present("mpls-router", load=False): + return False + if not module_present("mpls-iptunnel", load=False): + return False + return True + + def get_routertype(self): + "Return the type of Router (frr)" + + return self.routertype + + def report_memory_leaks(self, filename_prefix, testscript): + "Report Memory Leaks to file prefixed with given string" + + leakfound = False + filename = filename_prefix + re.sub(r"\.py", "", testscript) + ".txt" + for daemon in self.daemons: + if self.daemons[daemon] == 1: + log = self.getStdErr(daemon) + if "memstats" in log: + # Found memory leak + logger.info( + "\nRouter {} {} StdErr Log:\n{}".format(self.name, daemon, log) + ) + if not leakfound: + leakfound = True + # Check if file already exists + fileexists = os.path.isfile(filename) + leakfile = open(filename, "a") + if not fileexists: + # New file - add header + leakfile.write( + "# Memory Leak Detection for topotest %s\n\n" + % testscript + ) + leakfile.write("## Router %s\n" % self.name) + leakfile.write("### Process %s\n" % daemon) + log = re.sub("core_handler: ", "", log) + log = re.sub( + r"(showing active allocations in memory group [a-zA-Z0-9]+)", + r"\n#### \1\n", + log, + ) + log = re.sub("memstats: ", " ", log) + leakfile.write(log) + leakfile.write("\n") + if leakfound: + leakfile.close() + + +def frr_unicode(s): + """Convert string to unicode, depending on python version""" + if sys.version_info[0] > 2: + return s + else: + return unicode(s) # pylint: disable=E0602 + + +def is_mapping(o): + return isinstance(o, Mapping) |