def _get_connect_if(self, row) -> str: """Given a BGP DF row, retrieve the connecting interface for the row""" if re.match(r'^[0-9a-f.:]*$', row['peer']): rslt = RoutesObj(context=self.ctxt).lpm(namespace=row['namespace'], hostname=row['hostname'], address=row['peer'], vrf=row['vrf']) if not rslt.empty: val = rslt['oifs'][0][0] else: val = '' else: val = row['peer'] return val
def _is_vtep_reachable(self, row): reason = [] defrt = ipaddress.IPv4Network("0.0.0.0/0") for vtep in row['remoteVtepList'].tolist(): rdf = RoutesObj(context=self.ctxt) \ .lpm(namespace=row['namespace'], vrf=['default'], hostname=row['hostname'], address=vtep) if rdf.empty: reason += [f"{vtep} not reachable"] continue if rdf.prefix[0] == defrt: reason += [f"{vtep} reachable via default"] return reason
def _is_vtep_reachable(self, row): reason = [] defrt = ipaddress.IPv4Network("0.0.0.0/0") for vtep in row['remoteVtepList'].tolist(): if vtep == '-': continue # Have to hardcode lpm query here sadly to avoid reading of the # data repeatedly. The time for asserting the state of 500 VNIs # came down from 194s to 4s with the below piece of code instead # of invoking route's lpm to accomplish the task. cached_df = self._routes_df \ .query(f'namespace=="{row.namespace}" and hostname=="{row.hostname}"') route = RoutesObj(context=self.ctxt).lpm(vrf='default', address=vtep, cached_df=cached_df) if route.empty: reason += [f"{vtep} not reachable"] continue if route.prefix.tolist() == [defrt]: reason += [f"{vtep} reachable via default"] return reason
def aver(self, **kwargs) -> pd.DataFrame: """Assert for EVPN Data""" assert_cols = [ "namespace", "hostname", "vni", "remoteVtepList", "vrf", "mcastGroup", "type", "priVtepIp", "state", "l2VniList", "ifname", "secVtepIp" ] kwargs.pop("columns", None) # Loose whatever's passed status = kwargs.pop('status', 'all') df = self.get(columns=assert_cols, **kwargs) if df.empty: df = pd.DataFrame(columns=assert_cols) if status != 'pass': df['assertReason'] = 'No data found' df['assert'] = 'fail' return df df["assertReason"] = [[] for _ in range(len(df))] # Gather the unique set of VTEPs per VNI vteps_df = df.explode(column='remoteVtepList') \ .dropna(how='any') \ .groupby(by=['vni', 'type'])['remoteVtepList'] \ .aggregate(lambda x: x.unique().tolist()) \ .reset_index() \ .dropna(how='any') \ .rename(columns={'remoteVtepList': 'allVteps'}) if not vteps_df.empty: her_df = df.merge(vteps_df) else: her_df = pd.DataFrame() if (not her_df.empty and (her_df.remoteVtepList.str.len() != 0).any()): # Check if every VTEP we know is reachable rdf = RoutesObj(context=self.ctxt).get( namespace=kwargs.get('namespace'), vrf='default') if not rdf.empty: rdf['prefixlen'] = rdf['prefix'].str.split('/') \ .str[1].astype('int') self._routes_df = rdf her_df["assertReason"] += her_df.apply(self._is_vtep_reachable, axis=1) mcast_df = df.query('mcastGroup != "0.0.0.0"') if not mcast_df.empty: # Ensure that all VNIs have at most one multicast group associated # per namespace mismatched_vni_df = mcast_df \ .groupby(by=['namespace', 'vni'])['mcastGroup'] \ .unique() \ .reset_index() \ .dropna() \ .query('mcastGroup.str.len() != 1') if not mismatched_vni_df.empty: df['assertReason'] += df.apply( lambda x, err_df: ['VNI has multiple mcast group'] if (x['namespace'] in err_df['namespace'] and x['vni'] in err_df['vni']) else [], axis=1, args=(mismatched_vni_df, )) elif not her_df.empty: # Every VTEP has info about every other VTEP for a given VNI her_df["assertReason"] += her_df.apply(self._all_vteps_present, axis=1) # State is up df["assertReason"] += df.apply( lambda x: ['interface is down'] if x['type'] == "L2" and x['state'] != "up" else [], axis=1) devices = df["hostname"].unique().tolist() ifdf = IfObj(context=self.ctxt) \ .get(namespace=kwargs.get("namespace", ""), hostname=devices, type='vxlan') df = df.merge( ifdf[['namespace', 'hostname', 'ifname', 'master', 'vlan']], on=['namespace', 'hostname', 'ifname'], how='left') # vxlan interfaces, if defined, for every VNI is part of bridge # We ensure this is true artificially for NXOS, ignored for JunOS. df["assertReason"] += df.apply( lambda x: ['vni not in bridge'] if (x['type'] == "L2" and x[ 'ifname'] != '-' and x['master'] != "bridge") else [], axis=1) # mac_df = MacsObj(context=self.ctxt) \ # .get(namespace=kwargs.get("namespace", ""), # macaddr=["00:00:00:00:00:00"]) # # Assert that we have HER for every remote VTEP # df['assertReason'] += df.apply(self._is_her_good, # args=(mac_df, ), axis=1) # Fill out the assert column df['assert'] = df.apply(lambda x: 'pass' if not len(x.assertReason) else 'fail', axis=1) if status == 'fail': df = df.query('assertReason.str.len() != 0') elif status == "pass": df = df.query('assertReason.str.len() == 0') return df[['namespace', 'hostname', 'vni', 'type', 'assertReason', 'assert', 'timestamp']] \ .explode(column='assertReason') \ .fillna({'assertReason': '-'})