├── LICENSE ├── README.md └── get_l3_facts.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019, Austin de Coup-Crank 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # get_l3_facts.py 2 | 3 | Retrieves IPv4 layer 3 info from network devices, saving output to CSV 4 | 5 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/python/black) 6 | 7 | Requires: 8 | * Python 3.6+ 9 | * [`napalm`](https://github.com/napalm-automation/napalm) 10 | * [`tqdm`](https://github.com/tqdm/tqdm) 11 | 12 | ## Overview 13 | 14 | For each network device provided, `get_l3_facts.py` pulls info for all L3 interfaces, formats the results, and combines them into a single CSV. 15 | 16 | Column schema: 17 | * `hostname` - Hostname, FQDN, or IP provided 18 | * `interface` - Interface name 19 | * `address` - IPv4 address 20 | * `prefix_length` - Network prefix bits 21 | * `cidr` - IP address in CIDR notation: `address`/`prefix_length` 22 | * `netmask` - Netmask formatted in dotted-decimal 23 | * `network` - CIDR-notated network ID that the `address` resides within 24 | * `is_enabled` - Whether or not the interface is administratively enabled (true/false) 25 | * `is_up` - Whether or not the interface is up/up (true/false) 26 | * `description` - Interface description (if any) 27 | 28 | ## Usage 29 | 30 | The only required argument is `-H`, a comma-delimited list of hostnames, FQDNs, and/or IP addresses of devices to query. 31 | All other arguments are either optional, inferred, or prompted at runtime. 32 | 33 | ### Example 34 | 35 | ``` 36 | $ python3 get_l3_facts.py -H router1,router2,10.216.46.1 37 | Username [adecoup]: 38 | Password: 39 | Enable secret: 40 | Progress: 100%|#########################################################################################################################| 3/3 [00:00<00:00, 6102.29Device/s] 41 | ``` 42 | 43 | Resulting CSV: 44 | 45 | ``` 46 | device,interface,address,prefix_length,cidr,netmask,network,is_enabled,is_up,description 47 | router1,Vlan10,10.0.32.1,24,10.0.32.1/24,255.255.255.0,10.0.32.0/24,TRUE,TRUE,Mgmt 48 | router1,Vlan15,10.0.34.1,23,10.0.34.1/23,255.255.254.0,10.0.34.0/23,TRUE,TRUE,Staff 49 | router1,Vlan15,210.155.195.33,29,210.155.195.33/29,255.255.255.248,210.155.195.32/29,TRUE,TRUE,Staff 50 | router1,Vlan15,210.155.195.17,29,210.155.195.17/29,255.255.255.248,210.155.195.16/29,TRUE,TRUE,Staff 51 | router1,Vlan20,10.0.33.1,24,10.0.33.1/24,255.255.255.0,10.0.33.0/24,TRUE,TRUE,Cameras 52 | router1,Vlan25,10.0.36.1,24,10.0.36.1/24,255.255.255.0,10.0.36.0/24,TRUE,TRUE,Lab1 53 | router1,Vlan35,10.0.37.1,24,10.0.37.1/24,255.255.255.0,10.0.37.0/24,TRUE,TRUE,Lab2 54 | router1,Vlan45,10.0.38.1,23,10.0.38.1/23,255.255.254.0,10.0.38.0/23,TRUE,TRUE,Students 55 | router2,GigabitEthernet1/0/1,10.250.80.10,30,10.250.80.10/30,255.255.255.252,10.250.80.8/30,TRUE,TRUE, 56 | router2,GigabitEthernet1/0/2,10.250.80.21,30,10.250.80.21/30,255.255.255.252,10.250.80.20/30,TRUE,TRUE, 57 | router2,GigabitEthernet1/0/3,10.250.80.25,30,10.250.80.25/30,255.255.255.252,10.250.80.24/30,TRUE,TRUE, 58 | 10.216.46.1,Vlan10,10.216.46.1,23,10.216.46.1/23,255.255.254.0,10.216.46.0/23,LAN,TRUE,TRUE, 59 | 10.216.46.1,GigabitEthernet0/3,10.250.80.26,30,10.250.80.26/30,255.255.255.252,10.250.80.24/30,TRUE,TRUE,UPLINK 60 | ``` 61 | 62 | ## Arguments 63 | 64 | ``` 65 | $ python3 get_l3_facts.py --help 66 | usage: get_l3_facts.py [-h] (--hosts HOSTS | --input INPUT) 67 | [--username USERNAME] [--password PASSWORD] 68 | [--secret SECRET] [--output CSV_PATH] [--driver DRIVER] 69 | [--ssh-config SSH_CONFIG] [--max-threads THREADS] 70 | [--timeout TIMEOUT] 71 | 72 | optional arguments: 73 | -h, --help show this help message and exit 74 | --hosts HOSTS, -H HOSTS 75 | Comma-delimited list of IPs and/or FQDNs to query 76 | --input INPUT, -i INPUT 77 | Text file with list of IPs and/or FQDNs to query (one 78 | per line) 79 | --username USERNAME, -u USERNAME 80 | Username to use when logging into HOSTS 81 | --password PASSWORD, -p PASSWORD 82 | Password to use when logging into HOSTS 83 | --secret SECRET, -s SECRET 84 | Enable secret to pass to NAPALM 85 | --output CSV_PATH, -o CSV_PATH 86 | Full path to save CSV output to (default: 87 | ./l3_facts.csv) 88 | --driver DRIVER Network driver for NAPALM to use (default: ios) 89 | --ssh-config SSH_CONFIG, -c SSH_CONFIG 90 | SSH config file for NAPALM to use (default: 91 | ~/.ssh/config) 92 | --max-threads THREADS, -t THREADS 93 | Maximum number of concurrent connections. (default: 94 | Python version default) 95 | ``` 96 | -------------------------------------------------------------------------------- /get_l3_facts.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import argparse 4 | import csv 5 | from concurrent.futures import ThreadPoolExecutor 6 | import getpass 7 | import ipaddress 8 | import napalm 9 | import os 10 | import tqdm 11 | 12 | 13 | def get_args(): 14 | """ 15 | Function to gather all of the needed arguments 16 | Returns supplied args 17 | """ 18 | parser = argparse.ArgumentParser() 19 | group = parser.add_mutually_exclusive_group(required=True) 20 | group.add_argument( 21 | "--hosts", 22 | "-H", 23 | help="Comma-delimited list of IPs and/or FQDNs to query", 24 | type=arg_list, 25 | ) 26 | group.add_argument( 27 | "--input", 28 | "-i", 29 | help="Text file with list of IPs and/or FQDNs to query (one per line)", 30 | type=str, 31 | ) 32 | parser.add_argument( 33 | "--username", "-u", help="Username to use when logging into HOSTS" 34 | ) 35 | parser.add_argument( 36 | "--password", "-p", help="Password to use when logging into HOSTS" 37 | ) 38 | parser.add_argument("--secret", "-s", help="Enable secret to pass to NAPALM") 39 | parser.add_argument( 40 | "--output", 41 | "-o", 42 | help="Full path to save CSV output to (default: ./l3_facts.csv)", 43 | dest="csv_path", 44 | default="./l3_facts.csv", 45 | ) 46 | parser.add_argument( 47 | "--driver", 48 | help="Network driver for NAPALM to use (default: ios)", 49 | default="ios", 50 | ) 51 | parser.add_argument( 52 | "--ssh-config", 53 | "-c", 54 | help="SSH config file for NAPALM to use (default: ~/.ssh/config)", 55 | default="~/.ssh/config", 56 | dest="ssh_config", 57 | ) 58 | parser.add_argument( 59 | "--max-threads", 60 | "-t", 61 | help="Maximum number of concurrent connections. (default: Python version default)", 62 | default=None, 63 | dest="threads", 64 | ) 65 | parser.add_argument( 66 | "--timeout", 67 | "-T", 68 | help="Connection timeout (sec) to pass to NAPALM. (default: 120s)", 69 | default=120, 70 | dest="timeout", 71 | ) 72 | args = parser.parse_args() 73 | if args.input: 74 | if not os.path.exists(args.input): 75 | raise FileNotFoundError(f"File {args.input} does not exist") 76 | if not args.username: 77 | username = getpass.getuser() 78 | args.username = input(f"Username [{username}]: ") or username 79 | if not args.password: 80 | args.password = getpass.getpass() 81 | if not args.secret: 82 | args.secret = getpass.getpass("Enable secret: ") 83 | if not (args.hosts or args.input): 84 | parser.error( 85 | "No host input, cannot continue. Must provide either --hosts or --input." 86 | ) 87 | exit(1) 88 | return args 89 | 90 | 91 | def arg_list(string): 92 | return string.split(",") 93 | 94 | 95 | def open_device(host, driver, username, password, secret, timeout, ssh_config): 96 | """ Opens connection to host and returns NAPALM object """ 97 | napalm_driver = napalm.get_network_driver(driver) 98 | device = napalm_driver( 99 | hostname=host, 100 | username=username, 101 | password=password, 102 | timeout=timeout, 103 | optional_args={"secret": secret, "ssh_config_file": ssh_config}, 104 | ) 105 | device.open() 106 | return device 107 | 108 | 109 | def get_iface_facts(host, args): 110 | """ Returns formatted interface facts for a single device """ 111 | device = open_device( 112 | host, 113 | args.driver, 114 | args.username, 115 | args.password, 116 | args.secret, 117 | args.timeout, 118 | args.ssh_config, 119 | ) 120 | ifaces = device.get_interfaces() 121 | ifaces_ip = device.get_interfaces_ip() 122 | device.close() 123 | results = [] 124 | for iface, attrs in ifaces.items(): 125 | if ifaces_ip.get(iface): 126 | attrs.update(ifaces_ip.get(iface)) 127 | ipv4 = attrs.get("ipv4") 128 | for address, prefix in ipv4.items(): 129 | prefix_length = prefix.get("prefix_length") 130 | cidr = "{}/{}".format(address, prefix_length) 131 | addr = ipaddress.ip_interface(cidr) 132 | network = str(addr.network) 133 | netmask = str(addr.netmask) 134 | results.append( 135 | { 136 | "hostname": device.hostname, 137 | "interface": iface, 138 | "description": attrs.get("description"), 139 | "address": address, 140 | "prefix_length": prefix_length, 141 | "cidr": cidr, 142 | "network": network, 143 | "netmask": netmask, 144 | "is_enabled": attrs.get("is_enabled"), 145 | "is_up": attrs.get("is_up"), 146 | } 147 | ) 148 | return results 149 | 150 | 151 | def save_csv(csv_path, output): 152 | """ Saves output to CSV """ 153 | fieldnames = [ 154 | "hostname", 155 | "interface", 156 | "address", 157 | "prefix_length", 158 | "cidr", 159 | "netmask", 160 | "network", 161 | "is_enabled", 162 | "is_up", 163 | "description", 164 | ] 165 | with open(csv_path, "w", newline="") as f: 166 | writer = csv.DictWriter(f, fieldnames=fieldnames) 167 | writer.writeheader() 168 | writer.writerows(output) 169 | 170 | 171 | def main(): 172 | """ Save L3 interface info from a collection of network devices to CSV. """ 173 | args = get_args() 174 | results = [] 175 | hosts = [] 176 | if args.input: 177 | with open(args.input, "r") as f: 178 | hosts = f.read().splitlines() 179 | else: 180 | hosts = args.hosts 181 | 182 | with ThreadPoolExecutor(max_workers=args.threads) as pool: 183 | threads = [] 184 | for host in hosts: 185 | threads.append(pool.submit(get_iface_facts, host, args)) 186 | 187 | for thread in tqdm.tqdm(threads, desc="Progress", unit="Device", ascii=True): 188 | results = results + thread.result() 189 | 190 | save_csv(args.csv_path, results) 191 | 192 | 193 | if __name__ == "__main__": 194 | main() 195 | --------------------------------------------------------------------------------