summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMatthieu Pignolet <m@mpgn.dev>2025-04-05 13:10:19 +0400
committerMatthieu Pignolet <m@mpgn.dev>2025-04-05 13:10:19 +0400
commit0d512f1a23ba004fc75f4bd9fef164edcad52d68 (patch)
tree5c35390a7fe5ffa6ed9bdaa5047598910e99e3d1
parent304fb71d78ff17fa3014f93e0f5f61f705cf0da1 (diff)
major: rework the route watcher
-rw-r--r--.fpm1
-rw-r--r--frr-evpn-route-watcher.conf2
-rwxr-xr-xfrr-evpn-route-watcher/task.py215
-rw-r--r--frr-evpn-route-watcher_0.0.2_all.debbin3028 -> 0 bytes
4 files changed, 117 insertions, 101 deletions
diff --git a/.fpm b/.fpm
index b0cf29a..c620755 100644
--- a/.fpm
+++ b/.fpm
@@ -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
deleted file mode 100644
index f316b29..0000000
--- a/frr-evpn-route-watcher_0.0.2_all.deb
+++ /dev/null
Binary files differ