### !/bin/env python # ------------------------------------------------------------- # RouteTrack.py # version 1.4 2019-03-13 # Craig Weinhold, craig.weinhold@cdw.com # # Add or remove a static route based on pingability of a host # # Usage: # RouteTrack.py [vrf] [ping_host [ping_vrf]] [invert] [(no)stdout] [(no)syslog] [debug] # # Sample usage and syslog output: # command "python bootflash:/RouteTrack.py 10.88.0.0/16 10.89.169.1 management 10.89.169.23" # # 2017 Mar 8 12:24:25 cnat2-nexus9396-1 %USER-3-SYSTEM_MSG: Adding route vrf management 10.88.0.0/16 to 10.89.169.1 (ping 10.89.169.23 success) - RouteTrack.py # 2017 Mar 8 12:26:18 cnat2-nexus9396-1 %USER-3-SYSTEM_MSG: Removing route vrf management 10.88.0.0/16 to 10.89.169.1 (ping 10.89.169.23 failed) - RouteTrack.py # # Full description of options: # route - the static route this script takes responsibility for. (required) # # interface & nexthop - where the route should go. (required) # allowed combinations: # nexthop only -- the most common usage (e.g., "10.89.169.1") # nexthop & interface -- uncommon, but useful to avoid accidental recursive / dynamic nexthop lookup (e.g., "10.89.169.1 ethernet0/1") # interface only -- common for null and tunnel, but uncommon for ethernet due to dependence on neighbor's proxy ARP (e.g., "null0" or "ethernet0/1") # # vrf - the vrf of the route. (default is "global") # # ping_host - a target host to ping. If the ping succeeds, the route is installed. If the ping fails, the route is removed. # (if not specified, the nexthop is used) # # ping_vrf - the vrf of the ping_host. (if not specified, the route vrf is used, or "global") # # "invert" - invert the tracking logic. i.e., add the route on ping failure, and remove the route on ping success. (optional, uncommon) # # "stdout" or "nostdout" - control logging to console. (default disabled) # # "syslog" or "nosyslog" - control logging to syslog. (default enabled) # # "debug" - log extra details, including all CLI commands and output. Useful for learning and troubleshooting. # # Latest version 1.4 tested on: # Nexus 9300 9.2(3) good # Nexus 9300 7.0(3)I7(2) good # Nexus 7700 8.2(3) works, but syslog appears in the admin VDC # # Previous versions tested on: # Nexus 6001 7.1(0)N1(1a) good # Nexus 5548UP 5.2(1)N1(5) good # Nexus 5548P 6.0(2)N1(2a) failed miserably # Nexus 5548P 6.0(2)N2(2) good # Nexus 5548P 7.3(0)N1(1) good # Nexus 7700 7.2(0) works, but syslog appears in the admin VDC # Nexus 7000 6.2(8b) works, but syslog appears in the admin VDC # Nexus 9300 7.0(3)I4(2) good # Nexus 9300 7.0(3)I7(2) good # # Nexus 7K running NXOS 6.x or 7.x, and Nexus 5K/6K running NXOS 7.x can only run RouteTrack from the special "/scripts/" directory # put the script into bootflash:/scripts/ # use "source RouteTrack.py [args]" to invoke # # Nexus 3K/9K can run RouteTrack from any location. Same goes for Nexus 5K/6K/7K running versions other than those noted above. # put the script into bootflash: (or any subdirectory you want) # use "python bootflash:RouteTrack.py [args]" to invoke # # Test the script from the CLI with "debug" to verify the logic. E.g., # "python bootflash:RouteTrack.py 9.9.9.9/32 10.89.24.1 8.8.8.8 debug" # # For scheduling @ 1-minute interval or higher, use the NX/OS scheduler: # feature scheduler # scheduler job name track-next-hop # python bootflash:/RouteTrack.py ... (or "source RouteTrack.py ...") # exit # scheduler schedule name track-next-hop # time start now repeat 00:00:01 # # For configuring @ 10-second interval, use EEM: # event manager applet track-next-hop # event snmp oid 1.3.6.1.2.1.1.3.0 get-type exact entry-op ge entry-val 0 poll-interval 10 # action 1.0 cli command python bootflash:/RouteTrack.py ... (or "source RouteTrack.py ...") # # (add "logging level vshd 4" to squelch the "VSHD-5-VSHD_SYSLOG_CONFIG" log messages. CSCuj26129 and CSCut84503) # # On Nexus 7K, each VDC needs its own copy of the script. Despite that, syslog messages from all VDCs show up only in the admin VDC. # ------------------------------------------------------------- platform = None import re import sys import syslog import os if '__file__' in globals(): self = os.path.basename(__file__) elif (len(sys.argv) > 0): self = sys.argv[0] else: self = "python" route = None route_nexthop = None route_interface = None route_vrf = None ping_host = None ping_vrf = None invert = False debug = False printstdout = False printsyslog = True err = None def printit ( str ) : for s in str.split("\n"): if printstdout : print s if printsyslog : syslog.syslog(syslog.LOG_ERR, s) try: import cisco except ImportError: printit("unable to import module 'cisco'") exit(1) try: from cli import * platform = 3 # nexus 9k and maybe 3k except ImportError: pass if not platform: if (hasattr(cisco,'CLI')): platform = 1 # nexus 5K/6K 5.x or 6.x elif (hasattr(cisco,'cli')): platform = 2 # nexus 5K/6K 7.x, nexus 7k 6.2 or 7.2 if not platform: printit("unable to determine nexus platform") exit(1) # ------------------------------------------------------------- # START OF FUNCTIONS def docmd( cmd ) : return safecli(cmd) def doconf( cmds=[] ) : if platform == 3: cmd = 'conf t ; ' + ' ; '.join(cmds) results = docmd(cmd) else : docmd("conf t") # used to validate this step, but doesn't work on N7K for cmd in cmds : if debug: printit("** config '" + cmd + "'") results = docmd(cmd) if results : die("unexpected response to '" + cmd + "': " + results) def die( msg ) : printit( msg ) quit() def safecli( cmd ) : results = None if platform == 1: # N5K/N6K 5.x or 6.x results = "\n".join(cisco.CLI(cmd, 0).get_output()) elif platform == 2: # N5K/N6K 7.x, or N7K 6.2 or 7.2 (possibly others) results = cisco.cli(cmd) elif platform == 3: # N9K and N3K results = cli(cmd) if debug : printcmd(cmd, results) return results def printcmd ( cmd, str ) : printit("START OF OUTPUT from " + cmd) for s in str.split("\n"): printit(" - " + s) printit("END OF OUTPUT from " + cmd) # ------------------------------------------------------------- # PARSE ARGUMENTS argv = sys.argv[1:] vrfcli = safecli("show vrf") for arg in argv: pmatch = re.search(r'(no|)stdout', arg) smatch = re.search(r'(no|)syslog', arg) if re.search(r'^\d+\.\d+\.\d+\.\d+\/\d+$', arg) : route = arg elif re.search(r'^\d+\.\d+\.\d+\.\d+$', arg) : if not (route_nexthop or route_interface) : route_nexthop = arg elif not ping_host : ping_host = arg else : if route_interface and not route_nexthop : # goofy cli arg shuffle route_nexthop = ping_host ping_host = arg else : err = "extra argument: " + arg break elif re.search(r'^(null|ethernet|vlan|port-channel|tunnel|mgmt)[\d\/\.]+$', arg, re.IGNORECASE) : if not route_interface : route_interface = arg else : err = "extra argument: " + arg elif pmatch : printstdout = False if pmatch.group(1) == 'no' else True elif smatch : printsyslog = False if smatch.group(1) == 'no' else True elif re.search(r'invert', arg) : invert = True elif re.search(r'debug', arg) : debug = True print else : if re.search(arg, vrfcli) : if not route_vrf and not ping_host: route_vrf = arg elif not ping_vrf : ping_vrf = arg else : err = "extra arg: " + arg break else : err = "extra arg or unknown vrf: " + arg break if not (route and (route_nexthop or route_interface) and ((route_nexthop or ping_host)) ) : err = "Usage: " + self + " [interface] [nexthop] [vrf] [ping_host [ping_vrf]] [(no)syslog] [(no)stdout] [debug]" if err : printsyslog = True printstdout = True printit(err) exit() if not ping_host : ping_host = route_nexthop if not ping_vrf : ping_vrf = route_vrf if debug : printit("=== " + " ".join(sys.argv)) s = "route='" + route + "'" if route_interface : s = s + ", route_interface='" + route_interface + "'" if route_nexthop : s = s + ", route_nexthop='" + route_nexthop + "'" if route_vrf : s = s + ", route_vrf='" + route_vrf + "'" if ping_host : s = s + ", ping_host='" + ping_host + "'" if ping_vrf : s = s + ", ping_vrf='" + ping_vrf + "'" printit(s) # ------------------------------------------------------------- # START OF MAIN SCRIPT if route_vrf : rv1 = " vrf " + route_vrf else : rv1 = '' if ping_vrf : pv1 = "vrf " + ping_vrf else : pv1 = '' routecli = docmd("show ip route " + route + " static" + rv1) routevalid = True if re.search(r"IP Route Table", routecli) else False routeexists = True if re.search(route, routecli) else False if (debug or not routevalid): printit("** routevalid=" + str(routevalid) + " routeexists=" + str(routeexists)) # printcmd("route", routecli) pingcli = docmd("ping " + ping_host + " " + pv1 + " count 3 timeout 1") pingvalid = True if re.search(r"packet loss", pingcli) else False pingfailed = True if re.search(r"100.00% packet loss", pingcli) else False if (debug or not pingvalid): printit("** pingvalid=" + str(pingvalid) + " pingfailed=" + str(pingfailed)) # printcmd("ping", pingcli) mycmds = [] if route_vrf : mycmds.append("vrf context " + route_vrf) if (not pingvalid or not routevalid) : printit("Failed to parse results. Exiting") exit(1) if route_interface and route_nexthop : target = route_interface + " " + route_nexthop elif route_interface : target = route_interface else : target = route_nexthop add_msg = "failed w/invert" if (invert) else "success" remove_msg = "success w/invert" if (invert) else "failed" if not (pingfailed ^ invert): if not routeexists : if debug : printit("** adding route") printit("Adding route " + rv1 + route + " to " + target + " (ping " + ping_host + " " + add_msg + ")") mycmds.append("ip route " + route + " " + target + " name " + self) doconf(mycmds) elif debug : printit("** route already exists") else: if routeexists : if debug : printit("** removing route") printit("Removing route " + rv1 + route + " to " + target + " (ping " + ping_host + " " + remove_msg + ")") mycmds.append("no ip route " + route + " " + target) doconf(mycmds) elif debug : printit("** route already removed")