Skip to content

Commit 5c576f6

Browse files
committed
Add support for ARP/ND
1 parent 6100bee commit 5c576f6

File tree

4 files changed

+330
-198
lines changed

4 files changed

+330
-198
lines changed

nornir_srl/connections/srlinux.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import json
44
import re
55
import copy
6+
import datetime
67

78
from natsort import natsorted
89
import jmespath
@@ -432,6 +433,13 @@ def get_mac_table(self, network_instance: Optional[str] = "*") -> Dict[str, Any]
432433
Dest:destination, Type:type}}',
433434
"datatype": "state",
434435
}
436+
if (
437+
not "bridged"
438+
in self.get(paths=["/system/features"], datatype="state")[0][
439+
"system/features"
440+
]
441+
):
442+
return {"mac_table": []}
435443
resp = self.get(
436444
paths=[path_spec.get("path", "")], datatype=path_spec["datatype"]
437445
)
@@ -653,6 +661,64 @@ def set_es_peers(resp):
653661
res = jmespath.search(path_spec["jmespath"], resp[0])
654662
return {"es": res}
655663

664+
def get_arp(self) -> Dict[str, Any]:
665+
path_spec = {
666+
"path": f"/interface[name=*]/subinterface[index=*]/ipv4/arp/neighbor",
667+
"jmespath": '"interface"[*].subinterface[].{interface:"_subitf", entries:ipv4.arp.neighbor[].{IPv4:"ipv4-address",MAC:"link-layer-address",Type:origin,expiry:"_rel_expiry" }}',
668+
"datatype": "state",
669+
}
670+
resp = self.get(
671+
paths=[path_spec.get("path", "")], datatype=path_spec["datatype"]
672+
)
673+
for itf in resp[0].get("interface", []):
674+
for subitf in itf.get("subinterface", []):
675+
subitf["_subitf"] = f"{itf['name']}.{subitf['index']}"
676+
for arp_entry in (
677+
subitf.get("ipv4", {}).get("arp", {}).get("neighbor", [])
678+
):
679+
try:
680+
ts = datetime.datetime.strptime(
681+
arp_entry["expiration-time"], "%Y-%m-%dT%H:%M:%S.%fZ"
682+
)
683+
arp_entry["_rel_expiry"] = (
684+
str(ts - datetime.datetime.now()).split(".")[0] + "s"
685+
)
686+
except:
687+
arp_entry["_rel_expiry"] = "-"
688+
689+
res = jmespath.search(path_spec["jmespath"], resp[0])
690+
return {"arp": res}
691+
692+
def get_nd(self) -> Dict[str, Any]:
693+
path_spec = {
694+
"path": f"/interface[name=*]/subinterface[index=*]/ipv6/neighbor-discovery/neighbor",
695+
"jmespath": '"interface"[*].subinterface[].{interface:"_subitf", entries:ipv6."neighbor-discovery".neighbor[].{IPv6:"ipv6-address",MAC:"link-layer-address",Type:origin,next_state:"_rel_expiry" }}',
696+
"datatype": "state",
697+
}
698+
resp = self.get(
699+
paths=[path_spec.get("path", "")], datatype=path_spec["datatype"]
700+
)
701+
for itf in resp[0].get("interface", []):
702+
for subitf in itf.get("subinterface", []):
703+
subitf["_subitf"] = f"{itf['name']}.{subitf['index']}"
704+
for nd_entry in (
705+
subitf.get("ipv6", {})
706+
.get("neighbor-discovery", {})
707+
.get("neighbor", [])
708+
):
709+
try:
710+
ts = datetime.datetime.strptime(
711+
nd_entry["next-state-time"], "%Y-%m-%dT%H:%M:%S.%fZ"
712+
)
713+
nd_entry["_rel_expiry"] = (
714+
str(ts - datetime.datetime.now()).split(".")[0] + "s"
715+
)
716+
except:
717+
nd_entry["_rel_expiry"] = "-"
718+
719+
res = jmespath.search(path_spec["jmespath"], resp[0])
720+
return {"nd": res}
721+
656722
def get(
657723
self,
658724
paths: List[str],

nornir_srl/fsc.py

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def pass_filter(row, filter):
152152
if len(col_names) == 0:
153153
col_names = get_fields(l)
154154
for col in col_names:
155-
table.add_column(col, no_wrap=True)
155+
table.add_column(col, no_wrap=False)
156156
common = {
157157
x: y
158158
for x, y in l.items()
@@ -727,5 +727,65 @@ def _es(task: Task) -> Result:
727727
)
728728

729729

730+
@cli.command()
731+
@click.pass_context
732+
@click.option(
733+
"--field-filter",
734+
"-f",
735+
multiple=True,
736+
help='filter fields with <field-name>=<glob-pattern>, e.g. -f name=ge-0/0/0 -f admin_state="ena*". Fieldnames correspond to column names of a report',
737+
)
738+
def arp(ctx: Context, field_filter: Optional[List] = None):
739+
"""Displays ARP table"""
740+
741+
def _arp(task: Task) -> Result:
742+
device = task.host.get_connection(CONNECTION_NAME, task.nornir.config)
743+
return Result(host=task.host, result=device.get_arp())
744+
745+
f_filter = (
746+
{k: v for k, v in [f.split("=") for f in field_filter]} if field_filter else {}
747+
)
748+
749+
result = ctx.obj["target"].run(task=_arp, name="arp", raise_on_error=False)
750+
print_report(
751+
result=result,
752+
name="ARP table",
753+
failed_hosts=result.failed_hosts,
754+
box_type=ctx.obj["box_type"],
755+
f_filter=f_filter,
756+
i_filter=ctx.obj["i_filter"],
757+
)
758+
759+
760+
@cli.command()
761+
@click.pass_context
762+
@click.option(
763+
"--field-filter",
764+
"-f",
765+
multiple=True,
766+
help='filter fields with <field-name>=<glob-pattern>, e.g. -f name=ge-0/0/0 -f admin_state="ena*". Fieldnames correspond to column names of a report',
767+
)
768+
def nd(ctx: Context, field_filter: Optional[List] = None):
769+
"""Displays IPv6 Neighbors"""
770+
771+
def _nd(task: Task) -> Result:
772+
device = task.host.get_connection(CONNECTION_NAME, task.nornir.config)
773+
return Result(host=task.host, result=device.get_nd())
774+
775+
f_filter = (
776+
{k: v for k, v in [f.split("=") for f in field_filter]} if field_filter else {}
777+
)
778+
779+
result = ctx.obj["target"].run(task=_nd, name="nd", raise_on_error=False)
780+
print_report(
781+
result=result,
782+
name="IPv6 Neighbors",
783+
failed_hosts=result.failed_hosts,
784+
box_type=ctx.obj["box_type"],
785+
f_filter=f_filter,
786+
i_filter=ctx.obj["i_filter"],
787+
)
788+
789+
730790
if __name__ == "__main__":
731791
cli(obj={})

0 commit comments

Comments
 (0)