summaryrefslogtreecommitdiff
path: root/frr-evpn-route-watcher/task.py
blob: d030d64e7b11b88dd8a6aa39e36bc18d43e3a871 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
#!/usr/bin/env python3
import json
import ipaddress
import subprocess

def vtysh(cmd):
    print(f"@: vtysh -c {cmd}")
    return subprocess.run([
        "vtysh",
        "-c",
        cmd
    ], capture_output=True, text=True).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 = [
        "route",
        "show",
        "proto",
        "frr-evpn-route-watcher"
    ]
    process4 = subprocess.run(["ip"] + command, capture_output=True, text=True)
    process6 = subprocess.run(["ip", "-6"] + command, capture_output=True, text=True)

    routes = process4.stdout.splitlines() + process6.stdout.splitlines()
    
    print(f"@: Routes found {routes}")
    out = {}

    for route in routes:
        # <ip> dev <vrf> scope link
        parts = route.strip().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}")
        # <ip> dev <vrf> metric <metric> pref <medium>
        elif len(parts) == 7:
            valid = parts[1] == "dev" \
                and parts[3] == "metric" \
                and parts[5] == "pref"
            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 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 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().values()
        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():
            if not type({}) == type(arp_cache[entry]):
                continue
            
            ip = ipaddress.ip_address(entry)
            should_route = arp_cache[entry]['type'] == 'local' and \
                not ip.is_link_local and \
                not ip.is_loopback and \
                not ip.is_multicast
            
            if should_route:
                present = ip in to_remove.keys()

                # 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:
                        print(f"@: Route to {ip} is already compliant")
                else:
                    # if the route doesn't exist, we simply create it
                    add_route_vrf(ip, vrfName)
                
                if present:
                    # since the route muse say we remove it from the to_remove list
                    to_remove.pop(ip)
    
    print(f"@: {len(to_remove)} routes need to be removed")
    for remove in to_remove.keys():
        remove_route_vrf(remove)

if __name__ == "__main__":
    main()