diff options
| author | Matthieu Pignolet <m@mpgn.dev> | 2025-04-05 13:10:19 +0400 |
|---|---|---|
| committer | Matthieu Pignolet <m@mpgn.dev> | 2025-04-05 13:10:19 +0400 |
| commit | 0d512f1a23ba004fc75f4bd9fef164edcad52d68 (patch) | |
| tree | 5c35390a7fe5ffa6ed9bdaa5047598910e99e3d1 | |
| parent | 304fb71d78ff17fa3014f93e0f5f61f705cf0da1 (diff) | |
major: rework the route watcher
| -rw-r--r-- | .fpm | 1 | ||||
| -rw-r--r-- | frr-evpn-route-watcher.conf | 2 | ||||
| -rwxr-xr-x | frr-evpn-route-watcher/task.py | 215 | ||||
| -rw-r--r-- | frr-evpn-route-watcher_0.0.2_all.deb | bin | 3028 -> 0 bytes |
4 files changed, 117 insertions, 101 deletions
@@ -10,3 +10,4 @@ frr-evpn-route-watcher/=/usr/lib/frr-evpn-route-watcher/ frr-evpn-route-watcher.service=/etc/systemd/system/frr-evpn-route-watcher.service frr-evpn-route-watcher.timer=/etc/systemd/system/frr-evpn-route-watcher.timer +frr-evpn-route-watcher.conf=/etc/iproute2/rt_protos.d/frr-evpn-route-watcher.conf diff --git a/frr-evpn-route-watcher.conf b/frr-evpn-route-watcher.conf new file mode 100644 index 0000000..e53963c --- /dev/null +++ b/frr-evpn-route-watcher.conf @@ -0,0 +1,2 @@ +# Used for registering the routing protocol defined by frr-evpn-route-watcher +199 frr-evpn-route-watcher diff --git a/frr-evpn-route-watcher/task.py b/frr-evpn-route-watcher/task.py index ce31d92..ab1a87e 100755 --- a/frr-evpn-route-watcher/task.py +++ b/frr-evpn-route-watcher/task.py @@ -2,114 +2,127 @@ import json import ipaddress import subprocess -import re -def get_frr_vrfs(): - """Gets the list of vrf VNIs configured in FRR""" - process = subprocess.run(["vtysh", "-c", "show vrf vni json"], capture_output=True) - return json.loads(process.stdout)["vrfs"] +def vtysh(cmd): + print(f"@: vtysh -c {cmd}") + return subprocess.run([ + "vtysh", + "-c", + cmd + ], capture_output=True).stdout -def get_frr_evpn_info(): - """Lists all the routes learned via evpn""" - process = subprocess.run( - ["vtysh", "-c", "show bgp l2vpn evpn json"], capture_output=True - ) - return json.loads(process.stdout) +def get_frr_vrfs(): + """ + gets the list of VNIs declared in the evpn config + """ + print("I: Listing evpn VNIs") + show_evpn_vni = vtysh("sh evpn vni json") + return json.loads(show_evpn_vni) + +def get_frr_arp_cache(vni): + """ + gets the list of atp-cache entries in the given VNI + """ + print(f"I: Listing evpn arp-cache for VNI {vni}") + show_evpn_arp_cache = vtysh(f"sh evpn arp-cache vni {vni} json") + return json.loads(show_evpn_arp_cache) + +def get_current_routes(): + """ + gets the list of routes currently applied using frr-evpn-route-watcher + """ + command = [ + "ip", + "route", + "show", + "proto", + "frr-evpn-route-watcher" + ] + process4 = subprocess.run(command, capture_output=True) + process6 = subprocess.run(command + ["-6"], capture_output=True) + + routes = process4.stdout.splitlines() + process6.stdout.splitlines() + out = {} + + for route in routes: + # <ip> dev <vrf> scope link + parts = route.split(" ") + if len(parts) == 5: + valid = parts[1] == "dev" \ + and parts[3] == "scope" \ + and parts[4] == "link" + if valid: + ipraw = parts[0] + vrf = parts[2] + ip = ipaddress.ip_address(ipraw) + out[ip] = vrf + print(f"D: Found existing route for {ip}") + return out def add_route_vrf(ip, vrf): - """Add a route using the ip route command""" - subprocess.run( - ["ip", "route", "add", str(ip), "dev", vrf, "metric", "0"], - ) + """ + Add a route using the ip route command that points to a vrf interface + """ + print(f"I: Adding route for {ip} -> {vrf}") + subprocess.run([ + "ip", + "route", + "add", + str(ip), + "dev", + vrf, + "proto", + "frr-evpn-route-watcher" + ]) def remove_route_vrf(ip): + """removes a route to a given ip""" + print(f"I: Removing route to {ip}") subprocess.run(["ip", "route", "del", str(ip)]) -def currently_routed(vrfs_names): - processfour = subprocess.run(["ip", "route"], capture_output=True, text=True).stdout.splitlines() - processsixe = subprocess.run(["ip", "-6", "route"], capture_output=True, text=True).stdout.splitlines() - processresults = processfour + processsixe - routed_ips = [] - for route in processresults: - parts = route.split(" ") - if len(parts) == 6: - addr, dev, vrf, scope, link = ( - parts[0], - parts[1], - parts[2], - parts[3], - parts[4], - ) - addr = parse_ip(addr) - if ( - addr != None - and dev == "dev" - and vrf in vrfs_names - and scope == "scope" - and link == "link" - ): - routed_ips.append(addr) - return routed_ips - -def parse_ip(str): - try: - return ipaddress.ip_network(str) - except ValueError: - return False - -def get_vrfs(): - return {str(vrf["vni"]): vrf["vrf"] for vrf in get_frr_vrfs()} - -def resolve_routes(): - json = get_frr_evpn_info() - vrfs = get_vrfs() - - # The local router-id to search for IPs in the evpn routes information - localRouterId = json["bgpLocalRouterId"] - localAS = json["localAS"] - - currentlyRouted = currently_routed([vrfs[vrf] for vrf in vrfs]) - - for jsonKey in json: - rd = jsonKey.split(":") - if len(rd) == 2: - peer, _ = rd - - if parse_ip(peer) and peer == localRouterId: - rdObject = json[jsonKey] - for route in rdObject: - matches = re.findall(r"(?:\[([^\]]*)\]:?)", route) - if len(matches) == 6: - route_type = matches[0] - if route_type == "2": - ip = parse_ip(matches[5]) - - if ip != None and not ip.is_link_local: - extendedCommunities = rdObject[route]["paths"][0][ - "extendedCommunity" - ]["string"] - rts = re.finditer( - f"RT:{localAS}:(\d+)", extendedCommunities - ) - - # For all the matches we have in the extentendCommunity value - for rt in rts: - vni = rt.group(1) - if vni in vrfs: - vrf = vrfs[vni] - if ip in currentlyRouted: - currentlyRouted.remove(ip) - break - - print( - f"wanting to add route {ip} to {vrf} ({vni}) vrf" - ) - add_route_vrf(ip, vrf) - - - break - for remove in currentlyRouted: - print(f"removing route for {remove}") +def main(): + print("frr-evpn-route-watcher by Matthieu P. <m@mpgn.dev>") + # Get all the L2 VRFs + vrfs = [ + vrf + for vrf in get_frr_vrfs() + if vrf['type'] == "L2" + ] + # get all the current routes + to_remove = get_current_routes() + + for vrf in vrfs: + vrfName = vrf['tenantVrf'] + vni = vrf['vni'] + arp_cache = get_frr_arp_cache(vni) + + for entry in arp_cache.keys(): + local = type({}) == type(arp_cache[entry]) and \ + arp_cache[entry]['type'] == 'local' + + if local: + ip = ipaddress.ip_address(entry) + present = ip in to_remove + + # if the route already exists + if present: + # if the route points to the correct VRF interface + valid = to_remove[ip] == vrfName + + # if it's not, we delete the old route and + # create a new correct one. + if not valid: + remove_route_vrf(ip) + add_route_vrf(ip, vrfName) + else: + # if the route doesn't exist, we simply create it + add_route_vrf(ip, vrfName) + + # since the route muse say we remove it from the to_remove list + to_remove.pop(ip) + + for remove in to_remove.keys(): remove_route_vrf(remove) -resolve_routes() +if __name__ == "__main__": + main() diff --git a/frr-evpn-route-watcher_0.0.2_all.deb b/frr-evpn-route-watcher_0.0.2_all.deb Binary files differdeleted file mode 100644 index f316b29..0000000 --- a/frr-evpn-route-watcher_0.0.2_all.deb +++ /dev/null |
