Esempio n. 1
0
    def _assert_mtu_match(self, **kwargs) -> pd.DataFrame:
        """Workhorse routine that validates MTU match for specified input"""
        columns = kwargs.pop('columns', [])

        columns = [
            "namespace", "hostname", "ifname", "state", "type", "mtu",
            "timestamp"
        ]

        type = kwargs.pop('type', 'ethernet')

        if_df = self.get(columns=columns, type=type, **kwargs)
        if if_df.empty:
            return pd.DataFrame(columns=columns + ["assert"])

        lldpobj = LldpObj(context=self.ctxt)
        lldp_df = lldpobj.get(**kwargs).query('peerIfname != "-"')

        if lldp_df.empty:
            if_df['assertReason'] = 'No LLDP peering info'
            if_df['assert'] = 'fail'
            return if_df

        # Now create a single DF where you get the MTU for the lldp
        # combo of (namespace, hostname, ifname) and the MTU for
        # the combo of (namespace, peerHostname, peerIfname) and then
        # pare down the result to the rows where the two MTUs don't match
        combined_df = (pd.merge(
            lldp_df,
            if_df[["namespace", "hostname", "ifname", "mtu"]],
            on=["namespace", "hostname", "ifname"],
            how="outer",
        ).dropna(how="any").merge(
            if_df[["namespace", "hostname", "ifname", "mtu"]],
            left_on=["namespace", "peerHostname", "peerIfname"],
            right_on=["namespace", "hostname", "ifname"],
            how="outer",
        ).dropna(how="any").drop(
            columns=["hostname_y", "ifname_y", "mgmtIP", "description"
                     ]).rename(
                         index=str,
                         columns={
                             "hostname_x": "hostname",
                             "ifname_x": "ifname",
                             "mtu_x": "mtu",
                             "mtu_y": "peerMtu",
                         },
                     ))

        if combined_df.empty:
            if_df['assertReason'] = 'No LLDP peering info'
            if_df['assert'] = 'fail'
            return if_df

        combined_df['assert'] = combined_df.apply(
            lambda x: 'pass' if x['mtu'] == x['peerMtu'] else 'fail', axis=1)

        return combined_df
Esempio n. 2
0
    def _assert_mtu_match(self, **kwargs) -> pd.DataFrame:
        """Workhorse routine that validates MTU match for specified input"""
        lldpobj = LldpObj(context=self.ctxt)
        lldp_df = lldpobj.get(**kwargs)

        if lldp_df.empty:
            raise NoLLdpError("No Valid LLDP info found")

        columns = [
            "namespace", "hostname", "ifname", "state", "mtu", "timestamp"
        ]
        if_df = self.get_valid_df(
            "interfaces",
            self.sort_fields,
            hostname=kwargs.get("hostname", []),
            namespace=kwargs.get("namespace", []),
            columns=columns,
            ifname=kwargs.get("ifname", []),
        )
        if if_df.empty:
            return pd.DataFrame(columns=columns + ["assert"])

        # Now create a single DF where you get the MTU for the lldp
        # combo of (namespace, hostname, ifname) and the MTU for
        # the combo of (namespace, peerHostname, peerIfname) and then
        # pare down the result to the rows where the two MTUs don't match
        combined_df = (pd.merge(
            lldp_df,
            if_df[["namespace", "hostname", "ifname", "mtu"]],
            on=["namespace", "hostname", "ifname"],
            how="outer",
        ).dropna(how="any").merge(
            if_df[["namespace", "hostname", "ifname", "mtu"]],
            left_on=["namespace", "peerHostname", "peerIfname"],
            right_on=["namespace", "hostname", "ifname"],
            how="outer",
        ).dropna(how="any").drop(
            columns=["hostname_y", "ifname_y", "mgmtIP", "description"
                     ]).rename(
                         index=str,
                         columns={
                             "hostname_x": "hostname",
                             "ifname_x": "ifname",
                             "mtu_x": "mtu",
                             "mtu_y": "peerMtu",
                         },
                     ))

        if combined_df.empty:
            return combined_df

        combined_df['assert'] = combined_df.apply(
            lambda x: 'pass' if x['mtu'] == x['peerMtu'] else 'fail', axis=1)

        return combined_df
Esempio n. 3
0
    def aver(self, **kwargs) -> pd.DataFrame:
        """BGP Assert"""

        assert_cols = [
            "namespace", "hostname", "vrf", "peer", "asn", "state", "peerAsn",
            "v4Enabled", "v6Enabled", "evpnEnabled", "v4Advertised",
            "v6Advertised", "evpnAdvertised", "v4Received", "v6Received",
            "evpnReceived", "bfdStatus", "reason", "notifcnReason"
        ]

        kwargs.pop("columns", None)  # Loose whatever's passed

        df = self.get(columns=assert_cols, **kwargs)
        if df.empty:
            return pd.DataFrame()

        if_df = IfObj(context=self.ctxt).get(namespace=kwargs.get("namespace", ""),
                                             columns=['namespace', 'hostname',
                                                      'ifname', 'state']) \
            .rename(columns={'state': 'ifState'})

        lldp_df = LldpObj(context=self.ctxt).get(
            namespace=kwargs.get("namespace", ""),
            columns=[
                'namespace', 'hostname', 'ifname', 'peerHostname', 'peerIfname'
            ])

        # Get the dataframes we need for processing
        df['cif'] = df.apply(self._get_connect_if, axis=1)
        df['ifname'] = df['cif'].str.split('.').str[0]

        df = df.merge(if_df, left_on=['namespace', 'hostname', 'cif'],
                      right_on=['namespace', 'hostname', 'ifname'],
                      how='left') \
            .drop(columns=['timestamp_y', 'ifname_y']) \
            .rename(columns={'ifname_x': 'ifname'})

        # We split off at this point to avoid merging mess because of lack of
        # LLDP info
        df = df.merge(lldp_df, on=['namespace', 'hostname', 'ifname'], how='left') \
               .drop(columns=['timestamp']) \
               .rename(columns={'timestamp_x': 'timestamp'})
        # Some munging to handle subinterfaces
        df['xx'] = df['peerIfname'] + '.' + df['cif'].str.split('.').str[1]

        df['peerIfname'] = df['xx'].where(df['xx'].notnull(), df['peerIfname'])
        df.drop(columns=['xx'], inplace=True)

        df = df.merge(df, left_on=['namespace', 'hostname', 'cif'],
                      right_on=['namespace', 'peerHostname',
                                'peerIfname'], how='left') \
            .drop(columns=['peerIfname_y', 'timestamp_y', 'cif_y',
                           'ifState_y', 'reason_y', 'notifcnReason_y']) \
            .rename(columns={'timestamp_x': 'timestamp',
                             'cif_x': 'cif', 'ifState_x': 'ifState',
                             'reason_x': 'reason',
                             'notifcnReason_x': 'notifcnReason'})
        df['peer_y'] = df['peer_y'].astype(str) \
                                   .where(df['peer_y'].notnull(), '')
        df["assertReason"] = [[] for _ in range(len(df))]
        # Now all sessions with NaN in the oif column have no connected route
        df['assertReason'] += df.apply(
            lambda x: ["outgoing link down"]
            if x['cif'] and x['ifState'] != "up" else [],
            axis=1)

        df['assertReason'] += df.apply(lambda x: ["no route to peer"]
                                       if not len(x['cif']) else [],
                                       axis=1)

        df['assertReason'] += df.apply(self._check_afi_safi, axis=1)

        df['assertReason'] += df.apply(
            lambda x: ["asn mismatch"]
            if x['peer_y'] and ((x["asn_x"] != x["peerAsn_y"]) or
                                (x['asn_y'] != x['peerAsn_x'])) else [],
            axis=1)

        df['assertReason'] += df.apply(
            lambda x: [f"{x['reason']}:{x['notifcnReason']}"]
            if (x['reason'] and x['state_x'] != 'Established') else [],
            axis=1)

        df['assert'] = df.apply(lambda x: 'pass'
                                if not len(x.assertReason) else 'fail',
                                axis=1)
        return (df[[
            'namespace', 'hostname_x', 'vrf_x', 'peer_x', 'asn_x', 'peerAsn_x',
            'state_x', 'peerHostname_x', 'vrf_y', 'peer_y', 'asn_y',
            'peerAsn_y', 'assert', 'assertReason', 'timestamp'
        ]].rename(columns={
            'hostname_x': 'hostname',
            'vrf_x': 'vrf',
            'peer_x': 'peer',
            'asn_x': 'asn',
            'peerAsn_x': 'peerAsn',
            'state_x': 'state',
            'peerHostname_x': 'hostnamePeer',
            'vrf_y': 'vrfPeer',
            'peer_y': 'peerPeer',
            'asn_y': 'asnPeer',
            'peerAsn_y': 'peerAsnPeer'
        },
                  copy=False).explode(column="assertReason").astype(
                      {
                          'asn': 'Int64',
                          'peerAsn': 'Int64',
                          'asnPeer': 'Int64',
                          'peerAsnPeer': 'Int64'
                      },
                      copy=False).fillna({'assertReason': '-'}))
Esempio n. 4
0
    def aver(self, **kwargs):
        """Assert that the OSPF state is OK"""

        columns = [
            "namespace",
            "hostname",
            "vrf",
            "ifname",
            "routerId",
            "helloTime",
            "deadTime",
            "passive",
            "ipAddress",
            "isUnnumbered",
            "areaStub",
            "networkType",
            "timestamp",
            "area",
            "nbrCount",
            "origIfname",
        ]
        sort_fields = ["namespace", "hostname", "ifname", "vrf"]

        ospf_df = self.get_valid_df("ospfIf",
                                    sort_fields,
                                    columns=columns,
                                    **kwargs)
        if ospf_df.empty:
            return pd.DataFrame(columns=columns)

        ospf_df['ifname'] = ospf_df['origIfname']
        ospf_df.drop(columns=['origIfname'], inplace=True)

        ospf_df["assertReason"] = [[] for _ in range(len(ospf_df))]
        df = (ospf_df[ospf_df["routerId"] != ""].groupby(
            ["routerId", "namespace"], as_index=False)[[
                "hostname", "namespace"
            ]].agg(lambda x: x.unique().tolist())).dropna(how='any')

        # df is a dataframe with each row containing the routerId and the
        # corresponding list of hostnames with that routerId. In a good
        # configuration, the list must have exactly one entry
        ospf_df['assertReason'] = (ospf_df.merge(
            df, on=["routerId"], how="outer").apply(
                lambda x: ["duplicate routerId {}".format(x["hostname_y"])]
                if len(x['hostname_y']) != 1 else [],
                axis=1))

        # Now  peering match
        lldpobj = LldpObj(context=self.ctxt)
        lldp_df = lldpobj.get(namespace=kwargs.get("namespace", ""),
                              hostname=kwargs.get("hostname", ""),
                              ifname=kwargs.get("ifname", ""),
                              columns=[
                                  "namespace", "hostname", "ifname",
                                  "peerHostname", "peerIfname", "peerMacaddr"
                              ])
        if lldp_df.empty:
            ospf_df['assertReason'] = 'No LLDP peering info'
            ospf_df['assert'] = 'fail'
            return ospf_df[[
                'namespace', 'hostname', 'vrf', 'ifname', 'assertReason',
                'assert'
            ]]

        # Create a single massive DF with fields populated appropriately
        use_cols = [
            "namespace",
            "routerId",
            "hostname",
            "vrf",
            "ifname",
            "helloTime",
            "deadTime",
            "passive",
            "ipAddress",
            "areaStub",
            "isUnnumbered",
            "networkType",
            "area",
            "timestamp",
        ]

        int_df = ospf_df[use_cols].merge(lldp_df,
                                         on=["namespace", "hostname",
                                             "ifname"]) \
            .dropna(how="any")

        if int_df.empty:
            ospf_df['assertReason'] = 'No LLDP peering info'
            ospf_df['assert'] = 'fail'
            return ospf_df[[
                'namespace', 'hostname', 'vrf', 'ifname', 'assertReason',
                'assert'
            ]]

        ospf_df = ospf_df.merge(int_df,
                                left_on=["namespace", "hostname", "ifname"],
                                right_on=["namespace", "peerHostname",
                                          "peerIfname"]) \
            .dropna(how="any")

        # Now start comparing the various parameters
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["subnet mismatch"]
            if ((x["isUnnumbered_x"] != x["isUnnumbered_y"]) and
                (IPv4Network(x["ipAddress_x"], strict=False) != IPv4Network(
                    x["ipAddress_y"], strict=False))) else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["area mismatch"] if (x["area_x"] != x["area_y"] and x[
                "areaStub_x"] != x["areaStub_y"]) else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["Hello timers mismatch"]
            if x["helloTime_x"] != x["helloTime_y"] else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["Dead timer mismatch"]
            if x["deadTime_x"] != x["deadTime_y"] else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["network type mismatch"]
            if x["networkType_x"] != x["networkType_y"] else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["passive config mismatch"]
            if x["passive_x"] != x["passive_y"] else [],
            axis=1,
        )
        ospf_df["assertReason"] += ospf_df.apply(
            lambda x: ["vrf mismatch"] if x["vrf_x"] != x["vrf_y"] else [],
            axis=1,
        )

        # Fill up a single assert column now indicating pass/fail
        ospf_df['assert'] = ospf_df.apply(
            lambda x: 'pass' if not len(x['assertReason']) else 'fail', axis=1)

        return (ospf_df.rename(
            index=str,
            columns={
                "hostname_x": "hostname",
                "ifname_x": "ifname",
                "vrf_x": "vrf",
            },
        )[[
            "namespace", "hostname", "ifname", "vrf", "assert", "assertReason",
            "timestamp"
        ]].explode(column='assertReason').fillna({'assertReason': '-'}))
Esempio n. 5
0
    def _assert_interfaces(self, **kwargs) -> pd.DataFrame:
        """Workhorse routine that validates MTU match for specified input"""
        columns = kwargs.pop('columns', [])
        status = kwargs.pop('status', 'all')
        stime = kwargs.pop('start_time', '')
        etime = kwargs.pop('end_time', '')

        columns = [
            "namespace", "hostname", "ifname", "state", "type", "mtu", "vlan",
            "adminState", "ipAddressList", "ip6AddressList", "speed", "master",
            "timestamp"
        ]

        if_df = self.get(columns=columns, **kwargs)
        if if_df.empty:
            if status != 'pass':
                if_df['assert'] = 'fail'
                if_df['assertReason'] = 'No data'

            return if_df

        lldpobj = LldpObj(context=self.ctxt, start_time=stime, end_time=etime)
        vlanobj = VlanObj(context=self.ctxt, start_time=stime, end_time=etime)

        # can't pass all kwargs, because lldp acceptable arguements are
        # different than interface
        namespace = kwargs.get('namespace', None)
        hostname = kwargs.get('hostname', None)
        lldp_df = lldpobj.get(namespace=namespace, hostname=hostname) \
                         .query('peerIfname != "-"')

        # Get the VLAN info to ensure trunking ports are identical on both ends
        vlan_df = vlanobj.get(namespace=namespace, hostname=hostname)
        if not vlan_df.empty:
            vlan_df = vlan_df.explode('interfaces') \
                             .drop(columns=['vlanName'], errors='ignore') \
                             .rename(columns={'interfaces': 'ifname',
                                              'vlan': 'vlanList'})

            vlan_df = vlan_df.groupby(
                by=['namespace', 'hostname', 'ifname'
                    ])['vlanList'].unique().reset_index().query('ifname != ""')

            # OK merge this list into the interface info
            if_df = if_df.merge(vlan_df,
                                on=['namespace', 'hostname', 'ifname'],
                                how='outer')
            if_df['vlanList'] = if_df['vlanList'] \
                .fillna({i: [] for i in if_df.index})

        if lldp_df.empty:
            if status != 'pass':
                if_df['assertReason'] = 'No LLDP peering info'
                if_df['assert'] = 'fail'

            return if_df

        # Now create a single DF where you get the MTU for the lldp
        # combo of (namespace, hostname, ifname) and the MTU for
        # the combo of (namespace, peerHostname, peerIfname) and then
        # pare down the result to the rows where the two MTUs don't match
        combined_df = (pd.merge(
            lldp_df,
            if_df,
            on=["namespace", "hostname", "ifname"],
            how="outer",
        ).dropna(how="any").merge(
            if_df,
            left_on=["namespace", "peerHostname", "peerIfname"],
            right_on=["namespace", "hostname", "ifname"],
            how="outer",
            suffixes=["", "Peer"],
        ).dropna(how="any").drop(columns=[
            "hostnamePeer", "ifnamePeer", "timestamp_x", "mgmtIP",
            "description"
        ]))

        if combined_df.empty:
            if status != 'pass':
                if_df['assertReason'] = 'No LLDP peering info'
                if_df['assert'] = 'fail'

            return if_df

        combined_df['assertReason'] = combined_df.apply(
            lambda x: []
            if (x['adminState'] == 'down' or (x['adminState'] == "up" and x[
                'state'] == "up")) else ['Interface down'],
            axis=1)

        combined_df['assertReason'] += combined_df.apply(
            lambda x: [] if x['mtu'] == x['mtuPeer'] else ['MTU mismatch'],
            axis=1)

        combined_df['assertReason'] += combined_df.apply(
            lambda x: [] if x['vlan'] == x['vlanPeer'] else ['PVID mismatch'],
            axis=1)

        combined_df['assertReason'] += combined_df.apply(
            lambda x: []
            if x['speed'] == x['speedPeer'] else ['Speed mismatch'],
            axis=1)

        combined_df['assertReason'] += combined_df.apply(lambda x: [] if (
            (x['type'] == x['typePeer']) or
            (x['type'] == 'vlan' and x['typePeer'] == 'subinterface') or
            (x['type'].startswith('ether') and x['typePeer'].startswith(
                'ether'))) else ['type mismatch'],
                                                         axis=1)

        combined_df['assertReason'] += combined_df.apply(lambda x: [] if (
            (len(x['ipAddressList']) == len(x['ipAddressListPeer'])) and (
                (len(x['ipAddressList']) == 0) or
                (x['ipAddressList'][0].split('/')[1] == '32') or
                (ip_network(x['ipAddressList'][0], strict=False) == ip_network(
                    x['ipAddressListPeer'][0], strict=False)
                 ))) else ['IP address mismatch'],
                                                         axis=1)

        combined_df['assertReason'] += combined_df.apply(lambda x: [] if set(x[
            'vlanList']) == set(x['vlanListPeer']) else ['VLAN set mismatch'],
                                                         axis=1)

        combined_df['assert'] = combined_df.apply(
            lambda x: 'fail' if len(x.assertReason) else 'pass', axis=1)

        if status == "fail":
            combined_df = combined_df.query('assertReason.str.len() != 0')
        elif status == "pass":
            combined_df = combined_df.query('assertReason.str.len() == 0')

        return combined_df[[
            'namespace', 'hostname', 'ifname', 'state', 'peerHostname',
            'peerIfname', 'assert', 'assertReason'
        ]]
Esempio n. 6
0
    def aver(self, **kwargs):
        """Assert that the OSPF state is OK"""

        kwargs.pop('columns', [])
        columns = [
            "namespace",
            "hostname",
            "vrf",
            "ifname",
            "state",
            "routerId",
            "helloTime",
            "deadTime",
            "passive",
            "ipAddress",
            "isUnnumbered",
            "areaStub",
            "networkType",
            "timestamp",
            "area",
            "nbrCount",
        ]

        status = kwargs.pop('status', 'all')
        # we have to not filter hostname at this point because we need to
        #   understand neighbor relationships

        ospf_df = self.get_valid_df("ospfIf", columns=columns,
                                    state="!adminDown", **kwargs)
        if ospf_df.empty:
            return pd.DataFrame(columns=columns)

        ospf_df["assertReason"] = [[] for _ in range(len(ospf_df))]
        df = (
            ospf_df[ospf_df["routerId"] != ""]
            .groupby(["routerId", "namespace"], as_index=False)[["hostname",
                                                                 "namespace"]]
            .agg(lambda x: x.unique().tolist())
        ).dropna(how='any')

        # df is a dataframe with each row containing the routerId and the
        # corresponding list of hostnames with that routerId. In a good
        # configuration, the list must have exactly one entry.
        if not df.empty:
            # This check is because we don't get routerID with Arista boxes
            ospf_df['assertReason'] = (
                ospf_df.merge(df, on=["routerId"], how="outer")
                .apply(lambda x: ["duplicate routerId {}".format(
                    x["hostname_y"])]
                    if len(x['hostname_y']) != 1 else [], axis=1))

        # Now  peering match
        lldpobj = LldpObj(context=self.ctxt)
        lldp_df = lldpobj.get(
            namespace=kwargs.get("namespace", ""),
            hostname=kwargs.get("hostname", ""),
            ifname=kwargs.get("ifname", ""),
            columns=["namespace", "hostname", "ifname", "peerHostname",
                     "peerIfname", "peerMacaddr"]
        )
        if lldp_df.empty:
            ospf_df = ospf_df[~(ospf_df.ifname.str.contains('loopback') |
                                ospf_df.ifname.str.contains('Vlan'))]
            ospf_df['assertReason'] = 'No LLDP peering info'
            ospf_df['assert'] = 'fail'
            return ospf_df[['namespace', 'hostname', 'vrf', 'ifname',
                            'assertReason', 'assert']]

        # Create a single massive DF with fields populated appropriately
        use_cols = [
            "namespace",
            "routerId",
            "hostname",
            "vrf",
            "ifname",
            "helloTime",
            "deadTime",
            "passive",
            "ipAddress",
            "areaStub",
            "isUnnumbered",
            "networkType",
            "area",
            "timestamp",
        ]

        int_df = ospf_df[use_cols].merge(lldp_df,
                                         on=["namespace", "hostname",
                                             "ifname"]) \
            .dropna(how="any")

        if int_df.empty:
            # Weed out the loopback and SVI interfaces as they have no LLDP peers
            if status == "pass":
                ospf_df = ospf_df[(ospf_df.ifname.str.contains('loopback') |
                                   ospf_df.ifname.str.contains('Vlan'))]
                ospf_df['assertReason'] = []
                ospf_df['assert'] = 'pass'
            else:
                ospf_df = ospf_df[~(ospf_df.ifname.str.contains('loopback') |
                                    ospf_df.ifname.str.contains('Vlan'))]
                ospf_df['assertReason'] = 'No LLDP peering info'
                ospf_df['assert'] = 'fail'

            return ospf_df[['namespace', 'hostname', 'vrf', 'ifname',
                            'assertReason', 'assert']]

        peer_df = ospf_df.merge(int_df,
                                left_on=["namespace", "hostname", "ifname"],
                                right_on=["namespace", "peerHostname",
                                          "peerIfname"]) \
            .dropna(how="any")

        if peer_df.empty:
            ospf_df = ospf_df[~(ospf_df.ifname.str.contains('loopback') |
                                ospf_df.ifname.str.contains('Vlan'))]
            ospf_df['assertReason'] = 'No LLDP peering info'
            ospf_df['assert'] = 'fail'
        else:
            ospf_df = peer_df
            # Now start comparing the various parameters
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["subnet mismatch"]
                if (
                    (x["isUnnumbered_x"] != x["isUnnumbered_y"])
                    and (
                        IPv4Network(x["ipAddress_x"], strict=False)
                        != IPv4Network(x["ipAddress_y"], strict=False)
                    )
                )
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["area mismatch"]
                if (x["area_x"] != x["area_y"] and
                    x["areaStub_x"] != x["areaStub_y"])
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["Hello timers mismatch"]
                if x["helloTime_x"] != x["helloTime_y"]
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["Dead timer mismatch"]
                if x["deadTime_x"] != x["deadTime_y"]
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["network type mismatch"]
                if x["networkType_x"] != x["networkType_y"]
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["passive config mismatch"]
                if x["passive_x"] != x["passive_y"]
                else [],
                axis=1,
            )
            ospf_df["assertReason"] += ospf_df.apply(
                lambda x: ["vrf mismatch"] if x["vrf_x"] != x["vrf_y"] else [],
                axis=1,
            )

        # Fill up a single assert column now indicating pass/fail
        ospf_df['assert'] = ospf_df.apply(lambda x: 'pass'
                                          if not len(x['assertReason'])
                                          else 'fail', axis=1)

        result = (
            ospf_df.rename(
                index=str,
                columns={
                    "hostname_x": "hostname",
                    "ifname_x": "ifname",
                    "vrf_x": "vrf",
                },
            )[["namespace", "hostname", "ifname", "vrf", "assert",
               "assertReason", "timestamp"]].explode(column='assertReason')
            .fillna({'assertReason': '-'})
        )

        if status == "pass":
            return result.query('assertReason == "-"')
        elif status == "fail":
            return result.query('assertReason != "-"')

        return result