From e13f9c4f31f443e0fd84955a99f8aa8b3f034df2 Mon Sep 17 00:00:00 2001 From: Kuldeep Kashyap Date: Sun, 8 May 2022 02:31:01 -0700 Subject: [PATCH 1/2] tests: [PIMv6] APIs for multicast PIMv6 config Enhanced few exsiting PIM APIs to support both IPv4 and IPv6 configuration. Added few new APIs for PIMv6. Tested all existing tests with new API changes. Signed-off-by: Kuldeep Kashyap --- tests/topotests/lib/common_config.py | 90 ++++++ tests/topotests/lib/pim.py | 445 +++++++++++++++++++++------ tests/topotests/lib/topogen.py | 6 +- tests/topotests/lib/topojson.py | 11 +- tests/topotests/lib/topotest.py | 1 + 5 files changed, 454 insertions(+), 99 deletions(-) diff --git a/tests/topotests/lib/common_config.py b/tests/topotests/lib/common_config.py index 4afa86f740..fa33b02ed1 100644 --- a/tests/topotests/lib/common_config.py +++ b/tests/topotests/lib/common_config.py @@ -449,6 +449,8 @@ def check_router_status(tgen): 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: @@ -1035,6 +1037,12 @@ def start_topology(tgen, daemon=None): 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() @@ -1131,6 +1139,8 @@ def topo_daemons(tgen, topo=None): 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: @@ -3234,6 +3244,86 @@ def configure_interface_mac(tgen, input_dict): 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 ############################################# diff --git a/tests/topotests/lib/pim.py b/tests/topotests/lib/pim.py index cd070e08b9..03ab02460f 100644 --- a/tests/topotests/lib/pim.py +++ b/tests/topotests/lib/pim.py @@ -36,6 +36,7 @@ from lib.common_config import ( InvalidCLIError, retry, run_frr_cmd, + validate_ip_address, ) from lib.micronet import get_exec_path from lib.topolog import logger @@ -47,7 +48,7 @@ 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 on router + API to configure pim/pimv6 on router Parameters ---------- @@ -70,6 +71,16 @@ def create_pim_config(tgen, topo, input_dict=None, build=False, load_config=True "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 + }] } } } @@ -97,12 +108,8 @@ def create_pim_config(tgen, topo, input_dict=None, build=False, load_config=True # Now add RP config to all routers for router in input_dict.keys(): - if "pim" not in input_dict[router]: - continue - if "rp" not in input_dict[router]["pim"]: - continue - _add_pim_rp_config(tgen, topo, input_dict, router, build, config_data_dict) - + 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 @@ -133,81 +140,123 @@ def _add_pim_rp_config(tgen, topo, input_dict, router, build, config_data_dict): """ logger.debug("Entering lib API: {}".format(sys._getframe().f_code.co_name)) + rp_data = [] - pim_data = input_dict[router]["pim"] - rp_data = pim_data["rp"] + # 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 not pim_if_enabled: + 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 = [] - 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, - ) + 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) + 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) - # 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, + ) - # 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) - return False - group_addr_range = rp_dict.setdefault("group_addr_range", None) + # Group prefix-list filter + prefix_list = rp_dict.setdefault("prefix_list", None) - # Group prefix-list filter - prefix_list = rp_dict.setdefault("prefix_list", None) + # Delete rp config + del_action = rp_dict.setdefault("delete", False) - # Delete rp config - del_action = rp_dict.setdefault("delete", False) - - if keep_alive_timer: - cmd = "ip 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: - cmd = "ip pim rp {} {}".format(rp_addr, grp_addr) + 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 prefix_list: - cmd = "ip pim rp {} prefix-list {}".format(rp_addr, prefix_list) - 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] - if config_data: - if dut not in config_data_dict: - config_data_dict[dut] = config_data - else: - config_data_dict[dut].extend(config_data) + 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): @@ -319,6 +368,121 @@ def create_igmp_config(tgen, topo, input_dict=None, build=False): 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 @@ -338,7 +502,7 @@ def _enable_disable_pim_config(tgen, topo, input_dict, router, build=False): config_data = [] - # Enable pim on interfaces + # Enable pim/pim6 on interfaces for destRouterLink, data in sorted(topo[router]["links"].items()): if "pim" in data and data["pim"] == "enable": # Loopback interfaces @@ -351,6 +515,17 @@ def _enable_disable_pim_config(tgen, topo, input_dict, router, build=False): 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"] @@ -366,6 +541,21 @@ def _enable_disable_pim_config(tgen, topo, input_dict, router, build=False): 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 @@ -732,9 +922,6 @@ def verify_upstream_iif( "[DUT: %s]: Verifying upstream Inbound Interface" " for IGMP groups received:", 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] @@ -742,6 +929,17 @@ def verify_upstream_iif( 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: @@ -883,13 +1081,19 @@ def verify_join_state_and_timer( "[DUT: %s]: Verifying Join state and Join Timer" " for IGMP groups received:", 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 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: @@ -1010,20 +1214,6 @@ def verify_mroutes( rnode = tgen.routers()[dut] - 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 ip mroute json", 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 - if not isinstance(group_addresses, list): group_addresses = [group_addresses] @@ -1033,6 +1223,30 @@ def verify_mroutes( 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]!! " % ( @@ -1214,15 +1428,20 @@ def verify_pim_rp_info( 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 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) @@ -1232,9 +1451,14 @@ def verify_pim_rp_info( else: iamRP = False else: - show_ip_route_json = run_frr_cmd( - rnode, "show ip route connected json", isjson=True - ) + 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 @@ -1242,16 +1466,27 @@ def verify_pim_rp_info( 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, + 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: @@ -1380,14 +1615,26 @@ def verify_pim_state( rnode = tgen.routers()[dut] logger.info("[DUT: %s]: Verifying pim state", dut) - show_pim_state_json = run_frr_cmd(rnode, "show ip pim state json", isjson=True) - - if installed_fl is None: - installed_fl = 1 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 = "*" @@ -3635,7 +3882,7 @@ def verify_local_igmp_groups(tgen, dut, interface, group_addresses): return True -def verify_pim_interface_traffic(tgen, input_dict, return_stats=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 @@ -3645,6 +3892,8 @@ def verify_pim_interface_traffic(tgen, input_dict, return_stats=True): * `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 = { @@ -3675,9 +3924,13 @@ def verify_pim_interface_traffic(tgen, input_dict, return_stats=True): rnode = tgen.routers()[dut] logger.info("[DUT: %s]: Verifying pim interface traffic", dut) - show_pim_intf_traffic_json = run_frr_cmd( - rnode, "show ip pim interface traffic json", isjson=True - ) + + 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(): diff --git a/tests/topotests/lib/topogen.py b/tests/topotests/lib/topogen.py index c04506f47e..c51a187f28 100644 --- a/tests/topotests/lib/topogen.py +++ b/tests/topotests/lib/topogen.py @@ -725,6 +725,7 @@ class TopoRouter(TopoGear): RD_PBRD = 16 RD_PATH = 17 RD_SNMP = 18 + RD_PIM6 = 19 RD = { RD_FRR: "frr", RD_ZEBRA: "zebra", @@ -735,6 +736,7 @@ class TopoRouter(TopoGear): RD_ISIS: "isisd", RD_BGP: "bgpd", RD_PIM: "pimd", + RD_PIM6: "pim6d", RD_LDP: "ldpd", RD_EIGRP: "eigrpd", RD_NHRP: "nhrpd", @@ -820,7 +822,8 @@ class TopoRouter(TopoGear): 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_PBR, TopoRouter.RD_SNMP. + 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 @@ -1276,6 +1279,7 @@ def diagnose_env_linux(rundir): "ripngd", "isisd", "pimd", + "pim6d", "ldpd", "pbrd", ]: diff --git a/tests/topotests/lib/topojson.py b/tests/topotests/lib/topojson.py index 3ca3353ed3..b49b09e636 100644 --- a/tests/topotests/lib/topojson.py +++ b/tests/topotests/lib/topojson.py @@ -41,7 +41,11 @@ from lib.common_config import ( number_to_column, ) from lib.ospf import create_router_ospf -from lib.pim import create_igmp_config, create_pim_config +from lib.pim import ( + create_igmp_config, + create_pim_config, + create_mld_config, +) from lib.topolog import logger @@ -332,6 +336,7 @@ def build_config_from_json(tgen, topo=None, save_bkup=True): ("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), ] @@ -352,7 +357,9 @@ def build_config_from_json(tgen, topo=None, save_bkup=True): 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") + 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]: diff --git a/tests/topotests/lib/topotest.py b/tests/topotests/lib/topotest.py index 27b566a8f5..5a3f586f82 100644 --- a/tests/topotests/lib/topotest.py +++ b/tests/topotests/lib/topotest.py @@ -1330,6 +1330,7 @@ class Router(Node): "isisd": 0, "bgpd": 0, "pimd": 0, + "pim6d": 0, "ldpd": 0, "eigrpd": 0, "nhrpd": 0, From dab5ff0030775d1e546c414825e5a831b8dd1fd8 Mon Sep 17 00:00:00 2001 From: Kuldeep Kashyap Date: Fri, 15 Jul 2022 01:01:23 -0700 Subject: [PATCH 2/2] tests: [PIMv6] Add test_multicast_pimv6_static_rp suite Adding supporting multicast PIMv6 static rp test suite. Signed-off-by: Kuldeep Kashyap --- .../multicast_pimv6_static_rp.json | 197 +++++++++ .../test_multicast_pimv6_static_rp.py | 412 ++++++++++++++++++ 2 files changed, 609 insertions(+) create mode 100644 tests/topotests/multicast_pim_static_rp_topo1/multicast_pimv6_static_rp.json create mode 100755 tests/topotests/multicast_pim_static_rp_topo1/test_multicast_pimv6_static_rp.py diff --git a/tests/topotests/multicast_pim_static_rp_topo1/multicast_pimv6_static_rp.json b/tests/topotests/multicast_pim_static_rp_topo1/multicast_pimv6_static_rp.json new file mode 100644 index 0000000000..9edfae4a24 --- /dev/null +++ b/tests/topotests/multicast_pim_static_rp_topo1/multicast_pimv6_static_rp.json @@ -0,0 +1,197 @@ +{ + "address_types": ["ipv6"], + "ipv6base": "fd00::", + "ipv6mask": 64, + "link_ip_start": { + "ipv6": "fd00::", + "v6mask": 64 + }, + "lo_prefix": { + "ipv6": "2001:db8:f::", + "v6mask": 128 + }, + "routers": { + "r0": { + "links": { + "r1": {"ipv6": "auto"} + } + }, + "r1": { + "links": { + "lo": {"ipv6": "auto", "type": "loopback", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r0": {"ipv6": "auto", "pim6": "enable"}, + "r2": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r3": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r4": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }} + }, + "ospf6": { + "router_id": "100.1.1.0", + "neighbors": { + "r2": {}, + "r3": {}, + "r4": {} + }, + "redistribute": [ + { + "redist_type": "static" + }, + { + "redist_type": "connected" + } + ] + }, + "mld": { + "interfaces": { + "r1-r0-eth0" :{ + "mld":{ + } + } + } + } + }, + "r2": { + "links": { + "lo": {"ipv6": "auto", "type": "loopback", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r1": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r3": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }} + }, + "ospf6": { + "router_id": "100.1.1.1", + "neighbors": { + "r1": {}, + "r3": {} + }, + "redistribute": [ + { + "redist_type": "static" + }, + { + "redist_type": "connected" + } + ] + } + }, + "r3": { + "links": { + "lo": {"ipv6": "auto", "type": "loopback", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r1": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r2": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r4": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r5": {"ipv6": "auto", "pim6": "enable"} + }, + "ospf6": { + "router_id": "100.1.1.2", + "neighbors": { + "r1": {}, + "r2": {}, + "r4": {} + }, + "redistribute": [ + { + "redist_type": "static" + }, + { + "redist_type": "connected" + } + ] + } + }, + "r4": { + "links": { + "lo": {"ipv6": "auto", "type": "loopback", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r1": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }}, + "r3": {"ipv6": "auto", "pim6": "enable", + "ospf6": { + "area": "0.0.0.0", + "hello_interval": 1, + "dead_interval": 4 + }} + }, + "ospf6": { + "router_id": "100.1.1.3", + "neighbors": { + "r1": {}, + "r3": {} + }, + "redistribute": [ + { + "redist_type": "static" + }, + { + "redist_type": "connected" + } + ] + } + }, + "r5": { + "links": { + "r3": {"ipv6": "auto"} + } + } + } +} diff --git a/tests/topotests/multicast_pim_static_rp_topo1/test_multicast_pimv6_static_rp.py b/tests/topotests/multicast_pim_static_rp_topo1/test_multicast_pimv6_static_rp.py new file mode 100755 index 0000000000..f046623b74 --- /dev/null +++ b/tests/topotests/multicast_pim_static_rp_topo1/test_multicast_pimv6_static_rp.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python + +# +# Copyright (c) 2022 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. +# + +""" +Following tests are covered to test Multicast basic functionality: + +Topology: + + _______r2_____ + | | + iperf | | iperf + r0-----r1-------------r3-----r5 + | | + |_____________| + r4 + +Test steps +- Create topology (setup module) +- Bring up topology + +TC_1 : Verify upstream interfaces(IIF) and join state are updated properly + after adding and deleting the static RP +TC_2 : Verify IIF and OIL in "show ip pim state" updated properly after + adding and deleting the static RP +TC_3: (*, G) Mroute entry are cleared when static RP gets deleted +TC_4: Verify (*,G) prune is send towards the RP after deleting the static RP +TC_24 : Verify (*,G) and (S,G) populated correctly when SPT and RPT share the + same path +""" + +import os +import sys +import json +import time +import pytest +from time import sleep +import datetime + +# Save the Current Working Directory to find configuration files. +CWD = os.path.dirname(os.path.realpath(__file__)) +sys.path.append(os.path.join(CWD, "../")) +sys.path.append(os.path.join(CWD, "../lib/")) + +# Required to instantiate the topology builder class. + +# pylint: disable=C0413 +# Import topogen and topotest helpers +from lib.topogen import Topogen, get_topogen + +from lib.common_config import ( + start_topology, + write_test_header, + write_test_footer, + reset_config_on_routers, + step, + shutdown_bringup_interface, + kill_router_daemons, + start_router_daemons, + create_static_routes, + check_router_status, + socat_send_igmp_join_traffic, + topo_daemons +) +from lib.pim import ( + create_pim_config, + verify_igmp_groups, + verify_upstream_iif, + verify_join_state_and_timer, + verify_mroutes, + verify_pim_neighbors, + verify_pim_interface_traffic, + verify_pim_rp_info, + verify_pim_state, + clear_pim_interface_traffic, + clear_igmp_interfaces, + clear_pim_interfaces, + clear_mroute, + clear_mroute_verify, +) +from lib.topolog import logger +from lib.topojson import build_topo_from_json, build_config_from_json + +# Global variables +GROUP_RANGE_V6 = "ff08::/64" +IGMP_JOIN_V6 = "ff08::1" +STAR = "*" +SOURCE = "Static" + +pytestmark = [pytest.mark.pimd] + + +def build_topo(tgen): + """Build function""" + + # Building topology from json file + build_topo_from_json(tgen, TOPO) + + +def setup_module(mod): + """ + Sets up the pytest environment + + * `mod`: module name + """ + + testsuite_run_time = time.asctime(time.localtime(time.time())) + logger.info("Testsuite start time: %s", testsuite_run_time) + logger.info("=" * 40) + + topology = """ + + _______r2_____ + | | + iperf | | iperf + r0-----r1-------------r3-----r5 + | | + |_____________| + r4 + + """ + logger.info("Master Topology: \n %s", topology) + + logger.info("Running setup_module to create topology") + + # This function initiates the topology build with Topogen... + json_file = "{}/multicast_pimv6_static_rp.json".format(CWD) + tgen = Topogen(json_file, mod.__name__) + global TOPO + TOPO = tgen.json_topo + + # ... and here it calls Mininet initialization functions. + + # get list of daemons needs to be started for this suite. + daemons = topo_daemons(tgen, TOPO) + + # Starting topology, create tmp files which are loaded to routers + # to start daemons and then start routers + start_topology(tgen, daemons) + + # Don"t run this test if we have any failure. + if tgen.routers_have_failure(): + pytest.skip(tgen.errors) + + # Creating configuration from JSON + build_config_from_json(tgen, TOPO) + + # Verify PIM neighbors + result = verify_pim_neighbors(tgen, TOPO) + assert result is True, "setup_module :Failed \n Error:" " {}".format(result) + + logger.info("Running setup_module() done") + + +def teardown_module(): + """Teardown the pytest environment""" + + logger.info("Running teardown_module to delete topology") + tgen = get_topogen() + + # Stop toplogy and Remove tmp files + tgen.stop_topology() + + logger.info("Testsuite end time: %s", time.asctime(time.localtime(time.time()))) + logger.info("=" * 40) + + +##################################################### +# +# Testcases +# +##################################################### + + +def verify_state_incremented(state_before, state_after): + """ + API to compare interface traffic state incrementing + + Parameters + ---------- + * `state_before` : State dictionary for any particular instance + * `state_after` : State dictionary for any particular instance + """ + + for router, state_data in state_before.items(): + for state, value in state_data.items(): + if state_before[router][state] >= state_after[router][state]: + errormsg = ( + "[DUT: %s]: state %s value has not" + " incremented, Initial value: %s, " + "Current value: %s [FAILED!!]" + % ( + router, + state, + state_before[router][state], + state_after[router][state], + ) + ) + return errormsg + + logger.info( + "[DUT: %s]: State %s value is " + "incremented, Initial value: %s, Current value: %s" + " [PASSED!!]", + router, + state, + state_before[router][state], + state_after[router][state], + ) + + return True + + +##################################################### + +def test_pimv6_add_delete_static_RP_p0(request): + """ + TC_1: Verify upstream interfaces(IIF) and join state are updated + properly after adding and deleting the static RP + TC_2: Verify IIF and OIL in "show ip pim state" updated properly + after adding and deleting the static RP + TC_3: (*, G) Mroute entry are cleared when static RP gets deleted + TC_4: Verify (*,G) prune is send towards the RP after deleting the + static RP + + TOPOlogy used: + r0------r1-----r2 + iperf DUT RP + """ + + tgen = get_topogen() + tc_name = request.node.name + write_test_header(tc_name) + + # Don"t run this test if we have any failure. + if tgen.routers_have_failure(): + check_router_status(tgen) + + step("Shut link b/w R1 and R3") + intf = TOPO["routers"]["r1"]["links"]["r3"]["interface"] + shutdown_bringup_interface(tgen, "r1", intf, ifaceaction=False) + + step("Enable PIM between r1 and r2") + step("Enable MLD on r1 interface and send IGMP " "join (FF08::1) to r1") + step("Configure r2 loopback interface as RP") + input_dict = { + "r2": { + "pim6": { + "rp": [ + { + "rp_addr": TOPO["routers"]["r2"]["links"]["lo"]["ipv6"].split( + "/" + )[0], + "group_addr_range": GROUP_RANGE_V6, + } + ] + } + } + } + + result = create_pim_config(tgen, TOPO, input_dict) + assert result is True, "Testcase {} : Failed Error: {}".format(tc_name, result) + + step("Verify show ip pim interface traffic without any mld join") + state_dict = { + "r1": {TOPO["routers"]["r1"]["links"]["r2"]["interface"]: ["pruneTx"]} + } + + state_before = verify_pim_interface_traffic(tgen, state_dict, addr_type="ipv6") + assert isinstance( + state_before, dict + ), "Testcase{} : Failed \n state_before is not dictionary \n " "Error: {}".format( + tc_name, result + ) + + step("send mld join (FF08::1) to R1") + intf = TOPO["routers"]["r0"]["links"]["r1"]["interface"] + intf_ip = TOPO["routers"]["r0"]["links"]["r1"]["ipv6"].split("/")[0] + result = socat_send_igmp_join_traffic( + tgen, "r0", "UDP6-RECV", IGMP_JOIN_V6, intf, intf_ip, join=True + ) + assert result is True, "Testcase {}: Failed Error: {}".format(tc_name, result) + + step("r1: Verify RP info") + dut = "r1" + oif = TOPO["routers"]["r1"]["links"]["r2"]["interface"] + iif = TOPO["routers"]["r1"]["links"]["r0"]["interface"] + rp_address = TOPO["routers"]["r2"]["links"]["lo"]["ipv6"].split("/")[0] + result = verify_pim_rp_info( + tgen, TOPO, dut, GROUP_RANGE_V6, oif, rp_address, SOURCE + ) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("r1: Verify upstream IIF interface") + result = verify_upstream_iif(tgen, dut, oif, STAR, IGMP_JOIN_V6) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("r1: Verify upstream join state and join timer") + result = verify_join_state_and_timer(tgen, dut, oif, STAR, IGMP_JOIN_V6) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("r1: Verify PIM state") + result = verify_pim_state(tgen, dut, oif, iif, IGMP_JOIN_V6) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("r1: Verify ip mroutes") + result = verify_mroutes(tgen, dut, STAR, IGMP_JOIN_V6, oif, iif) + assert result is True, "Testcase {} :Failed \n Error: {}".format(tc_name, result) + + step("r1: Delete RP configuration") + input_dict = { + "r2": { + "pim6": { + "rp": [ + { + "rp_addr": TOPO["routers"]["r2"]["links"]["lo"]["ipv6"].split( + "/" + )[0], + "group_addr_range": GROUP_RANGE_V6, + "delete": True, + } + ] + } + } + } + + result = create_pim_config(tgen, TOPO, input_dict) + assert result is True, "Testcase {} : Failed Error: {}".format(tc_name, result) + + step("r1: Verify RP info") + result = verify_pim_rp_info( + tgen, TOPO, dut, GROUP_RANGE_V6, oif, rp_address, SOURCE, expected=False + ) + assert ( + result is not True + ), "Testcase {} :Failed \n " "RP: {} info is still present \n Error: {}".format( + tc_name, rp_address, result + ) + + step("r1: Verify upstream IIF interface") + result = verify_upstream_iif(tgen, dut, oif, STAR, IGMP_JOIN_V6, expected=False) + assert result is not True, ( + "Testcase {} :Failed \n " + "Upstream ({}, {}) is still in join state \n Error: {}".format( + tc_name, STAR, IGMP_JOIN_V6, result + ) + ) + + step("r1: Verify upstream join state and join timer") + result = verify_join_state_and_timer( + tgen, dut, oif, STAR, IGMP_JOIN_V6, expected=False + ) + assert result is not True, ( + "Testcase {} :Failed \n " + "Upstream ({}, {}) timer is still running \n Error: {}".format( + tc_name, STAR, IGMP_JOIN_V6, result + ) + ) + + step("r1: Verify PIM state") + result = verify_pim_state(tgen, dut, oif, iif, IGMP_JOIN_V6, expected=False) + assert result is not True, ( + "Testcase {} :Failed \n " + "PIM state for group: {} is still Active \n Error: {}".format( + tc_name, IGMP_JOIN_V6, result + ) + ) + + step("r1: Verify ip mroutes") + result = verify_mroutes(tgen, dut, STAR, IGMP_JOIN_V6, oif, iif, expected=False) + assert result is not True, ( + "Testcase {} :Failed \n " + "mroute ({}, {}) is still present \n Error: {}".format( + tc_name, STAR, IGMP_JOIN_V6, result + ) + ) + + step("r1: Verify show ip pim interface traffic without any IGMP join") + state_after = verify_pim_interface_traffic(tgen, state_dict, addr_type="ipv6") + assert isinstance( + state_after, dict + ), "Testcase{} : Failed \n state_before is not dictionary \n " "Error: {}".format( + tc_name, result + ) + + result = verify_state_incremented(state_before, state_after) + assert result is True, "Testcase{} : Failed Error: {}".format(tc_name, result) + + write_test_footer(tc_name) + + +if __name__ == "__main__": + args = ["-s"] + sys.argv[1:] + sys.exit(pytest.main(args))