diff --git a/dump/match_infra.py b/dump/match_infra.py index 0b4ca0490a39..9e7ffa334ffb 100644 --- a/dump/match_infra.py +++ b/dump/match_infra.py @@ -1,5 +1,6 @@ import json import fnmatch +import copy from abc import ABC, abstractmethod from dump.helper import verbose_print from swsscommon.swsscommon import SonicV2Connector, SonicDBConfig @@ -343,4 +344,86 @@ def fetch(self, req): verbose_print("Filtered Keys:" + str(filtered_keys)) if not filtered_keys: return self.__display_error(EXCEP_DICT["NO_ENTRIES"]) - return self.__fill_template(src, req, filtered_keys, template) \ No newline at end of file + return self.__fill_template(src, req, filtered_keys, template) + + +class MatchRequestOptimizer(): + """ + A Stateful Wrapper which reduces the number of calls to redis by caching the keys + The Cache saves all the fv-pairs for a key + Caching would only happen when the "key_pattern" is an absolute key and is not a glob-style pattern + """ + + def __init__(self, m_engine): + self.__key_cache = {} + self.m_engine = m_engine + + def __mutate_request(self, req): + """ + Mutate the Request to fetch all the fv pairs, regardless of the orignal request + Save the return_fields and just_keys args of original request + """ + fv_requested = [] + ret_just_keys = req.just_keys + fv_requested = copy.deepcopy(req.return_fields) + if ret_just_keys: + req.just_keys = False + req.return_fields = [] + return req, fv_requested, ret_just_keys + + def __mutate_response(self, ret, fv_requested, ret_just_keys): + """ + Mutate the Response based on what was originally asked. + """ + if not ret_just_keys: + return ret + new_ret = {"error": "", "keys": [], "return_values": {}} + for key_fv in ret["keys"]: + if isinstance(key_fv, dict): + keys = key_fv.keys() + new_ret["keys"].extend(keys) + for key in keys: + new_ret["return_values"][key] = {} + for field in fv_requested: + new_ret["return_values"][key][field] = key_fv[key][field] + return new_ret + + def __fill_cache(self, ret): + """ + Fill the cache with all the fv-pairs + """ + for key_fv in ret["keys"]: + keys = key_fv.keys() + for key in keys: + self.__key_cache[key] = key_fv[key] + + def __fetch_from_cache(self, key, req): + """ + Cache will have all the fv-pairs of the requested key + Response will be tailored based on what was asked + """ + new_ret = {"error": "", "keys": [], "return_values": {}} + if not req.just_keys: + new_ret["keys"].append(self.__key_cache[key]) + else: + new_ret["keys"].append(key) + if req.return_fields: + new_ret["return_values"][key] = {} + for field in req.return_fields: + new_ret["return_values"][key][field] = self.__key_cache[key][field] + return new_ret + + def fetch(self, req_orig): + req = copy.deepcopy(req_orig) + key = req.table + ":" + req.key_pattern + if key in self.__key_cache: + verbose_print("Cache Hit for Key: {}".format(key)) + return self.__fetch_from_cache(key, req) + else: + verbose_print("Cache Miss for Key: {}".format(key)) + req, fv_requested, ret_just_keys = self.__mutate_request(req) + ret = self.m_engine.fetch(req) + if ret["error"]: + return ret + self.__fill_cache(ret) + return self.__mutate_response(ret, fv_requested, ret_just_keys) diff --git a/dump/plugins/route.py b/dump/plugins/route.py new file mode 100644 index 000000000000..b53a83375c2d --- /dev/null +++ b/dump/plugins/route.py @@ -0,0 +1,271 @@ +import json +import re +from dump.match_infra import MatchRequest, MatchRequestOptimizer +from dump.helper import create_template_dict +from .executor import Executor + +NH = "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP" +NH_GRP = "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP" +RIF = "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE" +CPU_PORT = "ASIC_STATE:SAI_OBJECT_TYPE_PORT" + +OID_HEADERS = { + NH: "0x40", + NH_GRP: "0x50", + RIF: "0x60", + CPU_PORT: "0x10" +} + + +def get_route_pattern(dest): + return "*\"dest\":\"" + dest + "\"*" + + +def get_vr_oid(asic_route_entry): + """ + Route Entry Format: ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY: + {'dest':'::0','switch_id':'oid:0x21000000000000','vr':'oid:0x3000000000002'} + """ + matches = re.findall(r"\{.*\}", asic_route_entry) + key_dict = {} + if matches: + try: + key_dict = json.loads(matches[0]) + except Exception as e: + pass + return key_dict.get("vr", "") + + +class Route(Executor): + """ + Debug Dump Plugin for Route Module + """ + ARG_NAME = "destination_network" + + def __init__(self, match_engine=None): + super().__init__(match_engine) + self.nhgrp_match_engine = MatchRequestOptimizer(self.match_engine) + """ + MatchRequestOptimizer will be used for the keys related to these tables + 1) SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER + 2) SAI_OBJECT_TYPE_NEXT_HOP + 3) SAI_OBJECT_TYPE_ROUTER_INTERFACE + 4) CLASS_BASED_NEXT_HOP_GROUP_TABLE + 5) NEXTHOP_GROUP_TABLE + """ + self.ret_temp = {} + self.ns = '' + self.dest_net = '' + self.nh_id = '' + self.nh_type = '' + + def get_all_args(self, ns=""): + req = MatchRequest(db="APPL_DB", table="ROUTE_TABLE", key_pattern="*", ns=self.ns) + ret = self.match_engine.fetch(req) + all_routes = ret.get("keys", []) + return [key[len("ROUTE_TABLE:"):] for key in all_routes] + + def execute(self, params): + self.ret_temp = create_template_dict(dbs=["CONFIG_DB", "APPL_DB", "ASIC_DB"]) + self.dest_net = params[Route.ARG_NAME] + self.ns = params["namespace"] + # CONFIG DB + if not self.init_route_config_info(): + del self.ret_temp["CONFIG_DB"] + # APPL DB + nhgrp_field = self.init_route_appl_info() + self.init_nhgrp_cbf_appl_info(nhgrp_field) + # ASIC DB - ROUTE ENTRY + self.nh_id, vr = self.init_asic_route_entry_info() + # ASIC DB - VIRTUAL ROUTER + self.init_asic_vr_info(vr) + # ASIC DB - KEYS dependent on NEXT HOP ID + self.init_asic_nh() + return self.ret_temp + + def add_to_ret_template(self, table, db, keys, err, add_to_tables_not_found=True): + if not err and keys: + self.ret_temp[db]["keys"].extend(keys) + return keys + elif add_to_tables_not_found: + self.ret_temp[db]["tables_not_found"].extend([table]) + return [] + + def init_route_config_info(self): + req = MatchRequest(db="CONFIG_DB", table="STATIC_ROUTE", key_pattern=self.dest_net, ns=self.ns) + ret = self.match_engine.fetch(req) + return self.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"]) + + def init_route_appl_info(self): + req = MatchRequest(db="APPL_DB", table="ROUTE_TABLE", key_pattern=self.dest_net, + ns=self.ns, return_fields=["nexthop_group"]) + ret = self.match_engine.fetch(req) + self.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"]) + if ret["keys"]: + return ret["return_values"].get(ret["keys"][0], {}).get("nexthop_group", "") + return "" + + def init_nhgrp_cbf_appl_info(self, nhgrp_field): + if not nhgrp_field: + return + + # Verify if the nhgrp field in the route table refers to class based next_hop_group + req = MatchRequest(db="APPL_DB", table="CLASS_BASED_NEXT_HOP_GROUP_TABLE", key_pattern=nhgrp_field, + ns=self.ns, return_fields=["members"]) + ret = self.nhgrp_match_engine.fetch(req) + self.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"], False) + + nggrp_table_key = "" + if not ret["keys"]: + nggrp_table_key = nhgrp_field + else: + nggrp_table_key = ret["return_values"].get(ret["keys"][0], {}).get("members", "") + + if nggrp_table_key: + # Retrieve the next_hop_group key + req = MatchRequest(db="APPL_DB", table="NEXTHOP_GROUP_TABLE", key_pattern=nggrp_table_key, ns=self.ns) + ret = self.nhgrp_match_engine.fetch(req) + self.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"], False) + + def init_asic_route_entry_info(self): + nh_id_field = "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID" + req = MatchRequest(db="ASIC_DB", table="ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY", key_pattern=get_route_pattern(self.dest_net), + ns=self.ns, return_fields=[nh_id_field]) + ret = self.match_engine.fetch(req) + keys = self.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"]) + asic_route_entry = keys[0] if keys else "" + vr = get_vr_oid(asic_route_entry) + nh_id = ret["return_values"].get(asic_route_entry, {}).get(nh_id_field, "") + return nh_id, vr + + def init_asic_vr_info(self, vr): + ret = {} + if vr: + req = MatchRequest(db="ASIC_DB", table="ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER", key_pattern=vr, ns=self.ns) + ret = self.nhgrp_match_engine.fetch(req) + self.add_to_ret_template("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER", "ASIC_DB", ret.get("keys", []), ret.get("error", "")) + + def init_asic_nh(self): + self.nh_type = self.get_nh_type() + nh_ex = NHExtractor.initialize(self) + nh_ex.collect() + + def get_nh_type(self): + """ + Figure out the nh_type using OID Header + """ + if not self.nh_id: + return "DROP" + oid = self.nh_id.split(":")[-1] + for nh_type in [NH_GRP, NH, RIF, CPU_PORT]: + if oid.startswith(OID_HEADERS.get(nh_type, "")): + return nh_type + return "DROP" + + +class NHExtractor(object): + """ + Base Class for NH_ID Type + """ + @staticmethod + def initialize(route_obj): + if route_obj.nh_type == NH: + return SingleNextHop(route_obj) + elif route_obj.nh_type == NH_GRP: + return MultipleNextHop(route_obj) + elif route_obj.nh_type == RIF: + return DirecAttachedRt(route_obj) + elif route_obj.nh_type == CPU_PORT: + return CPUPort(route_obj) + return NHExtractor(route_obj) + + def __init__(self, route_obj): + self.rt = route_obj + + def collect(self): + pass + + def init_asic_rif_info(self, oid, add_to_tables_not_found=True): + ret = {} + if oid: + req = MatchRequest(db="ASIC_DB", table=RIF, key_pattern=oid, ns=self.rt.ns) + ret = self.rt.nhgrp_match_engine.fetch(req) + return self.rt.add_to_ret_template(RIF, "ASIC_DB", ret.get("keys", []), ret.get("error", ""), add_to_tables_not_found) + + def init_asic_next_hop_info(self, oid, add_to_tables_not_found=True): + ret = {} + if oid: + req = MatchRequest(db="ASIC_DB", table=NH, key_pattern=oid, ns=self.rt.ns, + return_fields=["SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID"]) + ret = self.rt.nhgrp_match_engine.fetch(req) + keys = self.rt.add_to_ret_template(NH, "ASIC_DB", ret.get("keys", []), ret.get("error", ""), add_to_tables_not_found) + nh_key = keys[0] if keys else "" + return ret.get("return_values", {}).get(nh_key, {}).get("SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID", "") + + +class CPUPort(NHExtractor): + def collect(self): + req = MatchRequest(db="ASIC_DB", table=CPU_PORT, key_pattern=self.rt.nh_id, ns=self.rt.ns) + ret = self.rt.nhgrp_match_engine.fetch(req) + self.rt.add_to_ret_template(req.table, req.db, ret["keys"], ret["error"]) + + +class DirecAttachedRt(NHExtractor): + def collect(self): + self.init_asic_rif_info(self.rt.nh_id) + + +class SingleNextHop(NHExtractor): + def collect(self): + rif_oid = self.init_asic_next_hop_info(self.rt.nh_id) + self.init_asic_rif_info(rif_oid) + + +class MultipleNextHop(NHExtractor): + def collect(self): + # Save nh_grp related keys + self.init_asic_nh_group_info(self.rt.nh_id) + # Save nh_grp_members info and fetch nh_oids + nh_oids = self.init_asic_nh_group_members_info(self.rt.nh_id) + # Save the actual next_hop using the nh_oids retrieved, fetch rif oid's if any + rif_oids = self.init_asic_next_hops_info(nh_oids) + # Save the rif_oid related ASIC keys + self.init_asic_rifs_info(rif_oids) + + def init_asic_nh_group_info(self, oid): + ret = {} + if oid: + req = MatchRequest(db="ASIC_DB", table=NH_GRP, key_pattern=oid, ns=self.rt.ns) + ret = self.rt.nhgrp_match_engine.fetch(req) + self.rt.add_to_ret_template(NH_GRP, "ASIC_DB", ret.get("keys", []), ret.get("error", "")) + + def init_asic_nh_group_members_info(self, oid): + ret = {} + if oid: + req = MatchRequest(db="ASIC_DB", table="ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", + field="SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID", value=oid, ns=self.rt.ns, + return_fields=["SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID"]) + ret = self.rt.nhgrp_match_engine.fetch(req) + keys = self.rt.add_to_ret_template("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER", "ASIC_DB", + ret.get("keys", []), ret.get("error", ""), False) + if not keys: + self.rt.ret_temp["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER") + return [ret.get("return_values", {}).get(key, {}).get("SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID", "") for key in keys] + + def init_asic_next_hops_info(self, nh_oids): + rif_oids = [] + for oid in nh_oids: + rif_oid = self.init_asic_next_hop_info(oid, False) + if rif_oid: + rif_oids.append(rif_oid) + if not rif_oids: + self.rt.ret_temp["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP") + return rif_oids + + def init_asic_rifs_info(self, rif_oids): + nothing_found = True + for oid in rif_oids: + if self.init_asic_rif_info(oid, False): + nothing_found = False + if nothing_found: + self.rt.ret_temp["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE") diff --git a/tests/dump_input/route/appl_db.json b/tests/dump_input/route/appl_db.json new file mode 100644 index 000000000000..f4cf9e5d8b21 --- /dev/null +++ b/tests/dump_input/route/appl_db.json @@ -0,0 +1,48 @@ +{ + "ROUTE_TABLE:1.1.1.0/24":{ + "ifname":"Ethernet0", + "nexthop":"0.0.0.0" + }, + "ROUTE_TABLE:10.1.0.32":{ + "ifname":"Loopback0", + "nexthop":"0.0.0.0" + }, + "ROUTE_TABLE:10.212.0.0/16":{ + "ifname":"Ethernet0", + "nexthop":"1.1.1.2" + }, + "ROUTE_TABLE:20.0.0.0/24":{ + "ifname":"Ethernet0", + "nexthop":"1.1.1.2" + }, + "ROUTE_TABLE:fe80::/64":{ + "ifname":"Loopback0", + "nexthop":"::" + }, + "ROUTE_TABLE:20c0:e6e0:0:80::/64":{ + "ifname":"PortChannel0001,PortChannel0002,PortChannel0003,PortChannel0004", + "nexthop":"fc00::72,fc00::76,fc00::7a,fc00::7e" + }, + "ROUTE_TABLE:192.168.0.4/24":{ + "ifname":"PortChannel0001,PortChannel0002,PortChannel0003,PortChannel0004", + "nexthop":"fc00::72,fc00::76,fc00::7a,fc00::7e" + }, + "ROUTE_TABLE:192.168.0.10/22":{ + "ifname":"PortChannel0001,PortChannel0002,PortChannel0003,PortChannel0004", + "nexthop":"fc00::72,fc00::76,fc00::7a,fc00::7e" + }, + "ROUTE_TABLE:10.0.0.16/16":{ + "nexthop_group":"testnhg" + }, + "ROUTE_TABLE:10.1.1.16/16":{ + "nexthop_group":"testcbfnhg" + }, + "NEXTHOP_GROUP_TABLE:testnhg":{ + "nexthop":"10.0.0.1", + "ifname":"Ethernet0" + }, + "CLASS_BASED_NEXT_HOP_GROUP_TABLE:testcbfnhg":{ + "members":"testnhg", + "selection_map":"AZURE" + } +} diff --git a/tests/dump_input/route/asic_db.json b/tests/dump_input/route/asic_db.json new file mode 100644 index 000000000000..8c25630b43d3 --- /dev/null +++ b/tests/dump_input/route/asic_db.json @@ -0,0 +1,119 @@ +{ + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"0.0.0.0/0\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION":"SAI_PACKET_ACTION_DROP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"::/0\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION":"SAI_PACKET_ACTION_DROP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"1.1.1.1/32\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x1000000000001", + "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION":"SAI_PACKET_ACTION_FORWARD" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"fe80::/64\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x1000000000001", + "SAI_ROUTE_ENTRY_ATTR_PACKET_ACTION":"SAI_PACKET_ACTION_FORWARD" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000001":{ + "NULL":"NULL" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"10.212.0.0/16\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x40000000002e7" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"20.0.0.0/24\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x40000000002e7" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x40000000002e7":{ + "SAI_NEXT_HOP_ATTR_IP":"1.1.1.2", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID":"oid:0x60000000002cd", + "SAI_NEXT_HOP_ATTR_TYPE":"SAI_NEXT_HOP_TYPE_IP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"1.1.1.0/24\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x60000000002cd" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000002cd":{ + "SAI_ROUTER_INTERFACE_ATTR_MTU":"9100", + "SAI_ROUTER_INTERFACE_ATTR_PORT_ID":"oid:0x1000000000170", + "SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS":"EC:0D:9A:60:39:00", + "SAI_ROUTER_INTERFACE_ATTR_TYPE":"SAI_ROUTER_INTERFACE_TYPE_PORT", + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID":"oid:0x3000000000002" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"20c0:e6e0:0:80::/64\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x5000000000689" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"192.168.0.4/24\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x5000000000689" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"192.168.0.10/22\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}":{ + "SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID":"oid:0x5000000000689" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP:oid:0x5000000000689":{ + "SAI_NEXT_HOP_GROUP_ATTR_TYPE":"SAI_NEXT_HOP_GROUP_TYPE_DYNAMIC_UNORDERED_ECMP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068a":{ + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID":"oid:0x5000000000689", + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID":"oid:0x400000000067f" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068b":{ + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID":"oid:0x5000000000689", + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID":"oid:0x400000000066f" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068c":{ + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID":"oid:0x5000000000689", + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID":"oid:0x4000000000665" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068d":{ + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_GROUP_ID":"oid:0x5000000000689", + "SAI_NEXT_HOP_GROUP_MEMBER_ATTR_NEXT_HOP_ID":"oid:0x4000000000667" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x400000000066f":{ + "SAI_NEXT_HOP_ATTR_IP":"fc00::76", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID":"oid:0x60000000005c7", + "SAI_NEXT_HOP_ATTR_TYPE":"SAI_NEXT_HOP_TYPE_IP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x400000000067f":{ + "SAI_NEXT_HOP_ATTR_IP":"fc00::72", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID":"oid:0x60000000005c6", + "SAI_NEXT_HOP_ATTR_TYPE":"SAI_NEXT_HOP_TYPE_IP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x4000000000665":{ + "SAI_NEXT_HOP_ATTR_IP":"fc00::7a", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID":"oid:0x60000000005c8", + "SAI_NEXT_HOP_ATTR_TYPE":"SAI_NEXT_HOP_TYPE_IP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x4000000000667":{ + "SAI_NEXT_HOP_ATTR_IP":"fc00::7e", + "SAI_NEXT_HOP_ATTR_ROUTER_INTERFACE_ID":"oid:0x60000000005c9", + "SAI_NEXT_HOP_ATTR_TYPE":"SAI_NEXT_HOP_TYPE_IP" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c6":{ + "SAI_ROUTER_INTERFACE_ATTR_MTU":"9100", + "SAI_ROUTER_INTERFACE_ATTR_PORT_ID":"oid:0x2000000000599", + "SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS":"1C:34:DA:1D:40:00", + "SAI_ROUTER_INTERFACE_ATTR_TYPE":"SAI_ROUTER_INTERFACE_TYPE_PORT", + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID":"oid:0x3000000000002" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c7":{ + "SAI_ROUTER_INTERFACE_ATTR_MTU":"9100", + "SAI_ROUTER_INTERFACE_ATTR_PORT_ID":"oid:0x200000000059a", + "SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS":"1C:34:DA:1D:40:00", + "SAI_ROUTER_INTERFACE_ATTR_TYPE":"SAI_ROUTER_INTERFACE_TYPE_PORT", + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID":"oid:0x3000000000002" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c8":{ + "SAI_ROUTER_INTERFACE_ATTR_MTU":"9100", + "SAI_ROUTER_INTERFACE_ATTR_PORT_ID":"oid:0x200000000059b", + "SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS":"1C:34:DA:1D:40:00", + "SAI_ROUTER_INTERFACE_ATTR_TYPE":"SAI_ROUTER_INTERFACE_TYPE_PORT", + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID":"oid:0x3000000000002" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002":{ + "NULL":"NULL" + }, + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c9":{ + "SAI_ROUTER_INTERFACE_ATTR_MTU":"9100", + "SAI_ROUTER_INTERFACE_ATTR_PORT_ID":"oid:0x200000000059c", + "SAI_ROUTER_INTERFACE_ATTR_SRC_MAC_ADDRESS":"1C:34:DA:1D:40:00", + "SAI_ROUTER_INTERFACE_ATTR_TYPE":"SAI_ROUTER_INTERFACE_TYPE_PORT", + "SAI_ROUTER_INTERFACE_ATTR_VIRTUAL_ROUTER_ID":"oid:0x3000000000002" + } +} diff --git a/tests/dump_input/route/config_db.json b/tests/dump_input/route/config_db.json new file mode 100644 index 000000000000..0d25accd5e59 --- /dev/null +++ b/tests/dump_input/route/config_db.json @@ -0,0 +1,9 @@ +{ + "STATIC_ROUTE|20.0.0.0/24":{ + "blackhole":"false", + "distance":"0", + "ifname":",", + "nexthop":"1.1.1.2", + "nexthop-vrf":"," + } +} diff --git a/tests/dump_tests/module_tests/route_test.py b/tests/dump_tests/module_tests/route_test.py new file mode 100644 index 000000000000..568bf3330072 --- /dev/null +++ b/tests/dump_tests/module_tests/route_test.py @@ -0,0 +1,326 @@ +import os +import unittest +import pytest +import json +from deepdiff import DeepDiff +from mock import patch +from dump.helper import create_template_dict +from dump.plugins.route import Route +from dump.match_infra import MatchEngine, ConnectionPool +from swsscommon.swsscommon import SonicV2Connector + + +# Location for dedicated db's used for UT +module_tests_path = os.path.dirname(__file__) +dump_tests_path = os.path.join(module_tests_path, "../") +tests_path = os.path.join(dump_tests_path, "../") +dump_test_input = os.path.join(tests_path, "dump_input") +Route_files_path = os.path.join(dump_test_input, "route") + +# Define the mock files to read from +dedicated_dbs = {} +dedicated_dbs['CONFIG_DB'] = os.path.join(Route_files_path, "config_db.json") +dedicated_dbs['APPL_DB'] = os.path.join(Route_files_path, "appl_db.json") +dedicated_dbs['ASIC_DB'] = os.path.join(Route_files_path, "asic_db.json") + + +def populate_mock(db, db_names): + for db_name in db_names: + db.connect(db_name) + # Delete any default data + db.delete_all_by_pattern(db_name, "*") + with open(dedicated_dbs[db_name]) as f: + mock_json = json.load(f) + for key in mock_json: + for field, value in mock_json[key].items(): + db.set(db_name, key, field, value) + + +@pytest.fixture(scope="class", autouse=True) +def match_engine(): + + print("SETUP") + os.environ["VERBOSE"] = "1" + + # Monkey Patch the SonicV2Connector Object + from ...mock_tables import dbconnector + db = SonicV2Connector() + + # popualate the db with mock data + db_names = list(dedicated_dbs.keys()) + try: + populate_mock(db, db_names) + except Exception as e: + assert False, "Mock initialization failed: " + str(e) + + # Initialize connection pool + conn_pool = ConnectionPool() + DEF_NS = '' # Default Namespace + conn_pool.cache = {DEF_NS: {'conn': db, + 'connected_to': set(db_names)}} + + # Initialize match_engine + match_engine = MatchEngine(conn_pool) + yield match_engine + print("TEARDOWN") + os.environ["VERBOSE"] = "0" + + +def get_asic_route_key(dest): + return "ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY:{\"dest\":\"" + dest + \ + "\",\"switch_id\":\"oid:0x21000000000000\",\"vr\":\"oid:0x3000000000002\"}" + + +@pytest.mark.usefixtures("match_engine") +class TestRouteModule: + def test_static_route(self, match_engine): + """ + Scenario: Fetch the Keys related to a Static Route from CONF, APPL & ASIC DB's + 1) CONF & APPL are straightforward + 2) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = SAI_OBJECT_TYPE_NEXT_HOP here + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "20.0.0.0/24", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["CONFIG_DB", "APPL_DB", "ASIC_DB"]) + expect["CONFIG_DB"]["keys"].append("STATIC_ROUTE|20.0.0.0/24") + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:20.0.0.0/24") + expect["ASIC_DB"]["keys"].append(get_asic_route_key("20.0.0.0/24")) + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x40000000002e7") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000002cd") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_ip2me_route(self, match_engine): + """ + Scenario: Fetch the keys related to a ip2me route from APPL & ASIC DB. + 1) CONF DB doesn't have a ip2me route entry unlike a static route. + 2) APPL is straightforward + 3) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = SAI_OBJECT_TYPE_PORT (CPU Port) + 4) Thus, no SAI_OBJECT_TYPE_ROUTER_INTERFACE entry for this route + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "fe80::/64", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:fe80::/64") + expect["ASIC_DB"]["keys"].append(get_asic_route_key("fe80::/64")) + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_PORT:oid:0x1000000000001") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_directly_connected_route(self, match_engine): + """ + Scenario: Fetch the keys related to a directly connected route from APPL & ASIC DB. + 1) CONF DB doesn't have this route entry + 2) APPL is straightforward + 3) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = SAI_OBJECT_TYPE_ROUTER_INTERFACE + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "1.1.1.0/24", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:1.1.1.0/24") + expect["ASIC_DB"]["keys"].append(get_asic_route_key("1.1.1.0/24")) + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000002cd") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_route_with_next_hop(self, match_engine): + """ + Scenario: Fetch the keys related to a route with next hop. + 1) CONF DB doesn't have this route entry + 2) APPL is straightforward + 3) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = SAI_OBJECT_TYPE_NEXT_HOP + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "10.212.0.0/16", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:10.212.0.0/16") + expect["ASIC_DB"]["keys"].append(get_asic_route_key("10.212.0.0/16")) + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x40000000002e7") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000002cd") + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_no_next_hop_id(self, match_engine): + """ + Scenario: Fetch the keys related to a route with no next hop id + 1) CONF DB doesn't have this route entry + 2) APPL is straightforward + 3) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = EMPTY + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "0.0.0.0/0", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["tables_not_found"].append("ROUTE_TABLE") + expect["ASIC_DB"]["keys"].append(get_asic_route_key("0.0.0.0/0")) + expect["ASIC_DB"]["keys"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def get_asic_nh_group_expected(self, asic_route_key): + expect = [] + expect.append(asic_route_key) + exp_nh_group = "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP:oid:0x5000000000689" + exp_nh_group_mem = ["ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068a", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068b", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068c", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP_GROUP_MEMBER:oid:0x2d00000000068d"] + exp_nh = ["ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x400000000066f", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x400000000067f", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x4000000000665", + "ASIC_STATE:SAI_OBJECT_TYPE_NEXT_HOP:oid:0x4000000000667"] + exp_rif = ["ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c6", + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c7", + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c8", + "ASIC_STATE:SAI_OBJECT_TYPE_ROUTER_INTERFACE:oid:0x60000000005c9"] + expect.append(exp_nh_group) + expect.extend(exp_nh_group_mem) + expect.extend(exp_nh) + expect.extend(exp_rif) + expect.append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER:oid:0x3000000000002") + return expect + + def test_route_with_next_hop_group(self, match_engine): + """ + Scenario: Fetch the keys related to a route with multiple next hops. + 1) CONF DB doesn't have this route entry + 2) APPL is straightforward + 3) SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID = SAI_OBJECT_TYPE_NEXT_HOP_GROUP + For More details about SAI_ROUTE_ENTRY_ATTR_NEXT_HOP_ID, check the SAI header in sairoute.h + """ + params = {Route.ARG_NAME: "20c0:e6e0:0:80::/64", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:20c0:e6e0:0:80::/64") + expect["ASIC_DB"]["keys"].extend(self.get_asic_nh_group_expected(get_asic_route_key("20c0:e6e0:0:80::/64"))) + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_caching_redis_keys(self, match_engine): + """ + Scenario: Test the caching mechanism which reduces number of redis calls + """ + global num_hits, num_miss, msgs + num_hits, num_miss, msgs = 0, 0, [] + + def verbose_print_mock(msg): + global num_hits, num_miss, msgs + if "Cache Hit for Key:" in msg: + num_hits = num_hits + 1 + elif "Cache Miss for Key:" in msg: + num_miss = num_miss + 1 + else: + return + msgs.append(msg) + + with patch("dump.match_infra.verbose_print", verbose_print_mock): + m_route = Route(match_engine) + params = {Route.ARG_NAME: "20c0:e6e0:0:80::/64", "namespace": ""} + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:20c0:e6e0:0:80::/64") + expect["ASIC_DB"]["keys"].extend(self.get_asic_nh_group_expected(get_asic_route_key("20c0:e6e0:0:80::/64"))) + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + print(msgs) + assert num_hits == 0 + assert num_miss == 11 + num_hits, num_miss, msgs = 0, 0, [] + + params = {Route.ARG_NAME: "192.168.0.4/24", "namespace": ""} + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:192.168.0.4/24") + expect["ASIC_DB"]["keys"].extend(self.get_asic_nh_group_expected(get_asic_route_key("192.168.0.4/24"))) + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + print(msgs) + assert num_hits == 10 + assert num_miss == 1 + num_hits, num_miss, msgs = 0, 0, [] + + params = {Route.ARG_NAME: "192.168.0.10/22", "namespace": ""} + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:192.168.0.10/22") + expect["ASIC_DB"]["keys"].extend(self.get_asic_nh_group_expected(get_asic_route_key("192.168.0.10/22"))) + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + print(msgs) + assert num_hits == 10 + assert num_miss == 1 + + def test_no_route_entry(self, match_engine): + """ + Scenario: Fetch the keys related to a non-exitent route + """ + params = {Route.ARG_NAME: "192.168.19.45/28", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["tables_not_found"].append("ROUTE_TABLE") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER") + ddiff = DeepDiff(returned, expect, ignore_order=True) + assert not ddiff, ddiff + + def test_route_with_nhgrp_appl_table(self, match_engine): + """ + Scenario: Fetch the NEXTHOP_GROUP_TABLE keys, if the nexthop_group field has a NEXTHOP_GROUP_TABLE.key + """ + params = {Route.ARG_NAME: "10.0.0.16/16", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:10.0.0.16/16") + expect["APPL_DB"]["keys"].append("NEXTHOP_GROUP_TABLE:testnhg") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER") + ddiff = DeepDiff(returned, expect, ignore_order=True) + print("Expected: {}".format(expect)) + print("Returned: {}".format(returned)) + assert not ddiff, ddiff + + def test_route_with_nhgrp_appl_table(self, match_engine): + """ + Scenario: Fetch the NEXTHOP_GROUP_TABLE/CLASS_BASED_NEXT_HOP_GROUP_TABLE keys, + if the nexthop_group field has a CLASS_BASED_NEXT_HOP_GROUP_TABLE.key + """ + params = {Route.ARG_NAME: "10.1.1.16/16", "namespace": ""} + m_route = Route(match_engine) + returned = m_route.execute(params) + expect = create_template_dict(dbs=["APPL_DB", "ASIC_DB"]) + expect["APPL_DB"]["keys"].append("ROUTE_TABLE:10.1.1.16/16") + expect["APPL_DB"]["keys"].append("NEXTHOP_GROUP_TABLE:testnhg") + expect["APPL_DB"]["keys"].append("CLASS_BASED_NEXT_HOP_GROUP_TABLE:testcbfnhg") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_ROUTE_ENTRY") + expect["ASIC_DB"]["tables_not_found"].append("ASIC_STATE:SAI_OBJECT_TYPE_VIRTUAL_ROUTER") + ddiff = DeepDiff(returned, expect, ignore_order=True) + print("Expected: {}".format(expect)) + print("Returned: {}".format(returned)) + assert not ddiff, ddiff + + def test_all_args(self, match_engine): + """ + Scenario: Verify Whether the get_all_args method is working as expected + """ + m_route = Route(match_engine) + returned = m_route.get_all_args("") + expect = ["1.1.1.0/24", "10.1.0.32", "10.212.0.0/16", "20.0.0.0/24", "192.168.0.10/22", + "fe80::/64", "20c0:e6e0:0:80::/64", "192.168.0.4/24", "10.1.1.16/16", "10.0.0.16/16"] + ddiff = DeepDiff(expect, returned, ignore_order=True) + assert not ddiff, ddiff