network/netbox_client.py
2026-05-08 17:47:42 -05:00

251 lines
No EOL
9.5 KiB
Python

#!/usr/bin/env python3
import requests
import json
from urllib.parse import urljoin
class NetBoxClient:
def __init__(self, url, token):
self.base_url = url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {token}',
'Content-Type': 'application/json',
'Accept': 'application/json'
}
self.session = requests.Session()
self.session.headers.update(self.headers)
def get(self, endpoint, params=None):
"""Make GET request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.get(url, params=params)
response.raise_for_status()
return response.json()
def get_all(self, endpoint, params=None):
"""Get all results handling pagination"""
if params is None:
params = {}
params['limit'] = 100
all_results = []
url = urljoin(self.api_url, endpoint.lstrip('/'))
while url:
response = self.session.get(url, params=params)
response.raise_for_status()
data = response.json()
all_results.extend(data['results'])
url = data['next']
params = None # Don't send params on subsequent requests
return all_results
# Site methods
def get_sites(self, **kwargs):
return self.get_all('dcim/sites/', params=kwargs)
def get_site_by_name(self, name):
sites = self.get('dcim/sites/', params={'name': name})
if sites['count'] > 0:
return sites['results'][0]
return None
# Prefix methods
def get_prefixes(self, **kwargs):
return self.get_all('ipam/prefixes/', params=kwargs)
def get_prefixes_by_site(self, site_name):
"""Get all prefixes associated with a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('ipam/prefixes/', params={'site_id': site['id']})
# IP Address methods
def get_ip_addresses(self, **kwargs):
return self.get_all('ipam/ip-addresses/', params=kwargs)
def get_ip_addresses_by_site(self, site_name):
"""Get all IP addresses associated with a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
# First try by site_id
ips = self.get_all('ipam/ip-addresses/', params={'site_id': site['id']})
# Also get IPs assigned to devices at this site
devices = self.get_all('dcim/devices/', params={'site_id': site['id']})
for device in devices:
device_ips = self.get_all('ipam/ip-addresses/', params={'device_id': device['id']})
ips.extend(device_ips)
# Remove duplicates
seen = set()
unique_ips = []
for ip in ips:
if ip['id'] not in seen:
seen.add(ip['id'])
unique_ips.append(ip)
return unique_ips
# Device methods
def get_devices_by_site(self, site_name):
"""Get all devices at a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('dcim/devices/', params={'site_id': site['id']})
# VLAN methods
def get_vlans_by_site(self, site_name):
"""Get all VLANs at a site"""
site = self.get_site_by_name(site_name)
if not site:
return []
return self.get_all('ipam/vlans/', params={'site_id': site['id']})
def compare_subnets():
"""Compare subnets from router with NetBox"""
# NetBox connection
nb = NetBoxClient('https://netbox.vntx.net/', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
# Subnets found on router (from previous scan)
router_subnets = [
{'address': '10.250.1.25/29', 'network': '10.250.1.24', 'interface': 'ether3-climax-11ghz'},
{'address': '10.254.254.101/32', 'network': '10.254.254.101', 'interface': 'loopback'},
{'address': '100.64.3.254/22', 'network': '100.64.0.0', 'interface': 'verona'},
{'address': '204.110.188.254/27', 'network': '204.110.188.224', 'interface': 'verona'},
{'address': '100.64.15.254/22', 'network': '100.64.12.0', 'interface': 'vlan_19_ether6'},
{'address': '10.10.95.254/20', 'network': '10.10.80.0', 'interface': 'vlan_10_ether6'},
{'address': '10.10.15.254/20', 'network': '10.10.0.0', 'interface': 'vlan_10_ether6'},
{'address': '10.0.101.254/24', 'network': '10.0.101.0', 'interface': 'sfp-sfpplus1-verona-tower-switch'},
{'address': '10.250.1.145/29', 'network': '10.250.1.144', 'interface': 'ether6-switch'},
{'address': '204.110.191.30/27', 'network': '204.110.191.0', 'interface': 'vlan9_sfpplus1'},
{'address': '10.25.1.254/24', 'network': '10.25.1.0', 'interface': 'ether1'},
{'address': '192.168.99.254/24', 'network': '192.168.99.0', 'interface': 'ether10-powerswitch'},
]
print("=== Checking Verona Site Subnets in NetBox ===\n")
# Get site info
site = nb.get_site_by_name('Verona')
if site:
print(f"Site: {site['name']} (ID: {site['id']})")
print(f"Status: {site['status']['label']}")
print(f"Address: {site['physical_address']}")
print()
# Get prefixes from NetBox
print("Fetching NetBox data...")
prefixes = nb.get_prefixes_by_site('Verona')
ip_addresses = nb.get_ip_addresses_by_site('Verona')
vlans = nb.get_vlans_by_site('Verona')
devices = nb.get_devices_by_site('Verona')
print(f"\nFound in NetBox:")
print(f"- {len(prefixes)} prefixes")
print(f"- {len(ip_addresses)} IP addresses")
print(f"- {len(vlans)} VLANs")
print(f"- {len(devices)} devices")
# Check if we need to search more broadly
if len(prefixes) == 0:
print("\nNo prefixes found with site association. Searching by description/comments...")
all_prefixes = nb.get_prefixes()
prefixes = [p for p in all_prefixes if 'verona' in (p.get('description', '') + p.get('comments', '')).lower()]
print(f"Found {len(prefixes)} prefixes with 'verona' in description/comments")
# Display NetBox prefixes
if prefixes:
print("\n=== Prefixes in NetBox ===")
for prefix in prefixes:
status = prefix['status']['label'] if prefix.get('status') else 'Unknown'
role = prefix['role']['name'] if prefix.get('role') else 'None'
description = prefix.get('description', '')
print(f"\n{prefix['prefix']}")
print(f" Status: {status}")
print(f" Role: {role}")
if description:
print(f" Description: {description}")
# Display NetBox IP addresses
if ip_addresses:
print("\n\n=== IP Addresses in NetBox ===")
for ip in ip_addresses:
status = ip['status']['label'] if ip.get('status') else 'Unknown'
role = ip['role']['name'] if ip.get('role') else 'None'
interface = ip.get('assigned_object', {}).get('name', 'Not assigned') if ip.get('assigned_object') else 'Not assigned'
print(f"\n{ip['address']}")
print(f" Status: {status}")
print(f" Role: {role}")
print(f" Interface: {interface}")
# Compare with router subnets
print("\n\n=== Comparison Analysis ===")
# Extract prefixes from NetBox data
netbox_prefixes = set(p['prefix'] for p in prefixes)
netbox_ips = set(ip['address'] for ip in ip_addresses)
# Check each router subnet
missing_subnets = []
missing_ips = []
for subnet in router_subnets:
# Check if the subnet prefix exists
subnet_cidr = f"{subnet['network']}/{subnet['address'].split('/')[-1]}"
found_prefix = False
found_ip = False
# Check against prefixes
for prefix in netbox_prefixes:
if subnet_cidr == prefix or subnet['address'] == prefix:
found_prefix = True
break
# Check against IP addresses
if subnet['address'] in netbox_ips:
found_ip = True
if not found_prefix and not found_ip:
if '/32' in subnet['address']:
missing_ips.append(subnet)
else:
missing_subnets.append(subnet)
# Report findings
print(f"\nMissing Subnets (not in NetBox):")
if missing_subnets:
for subnet in missing_subnets:
print(f" - {subnet['network']}/{subnet['address'].split('/')[-1]} ({subnet['interface']})")
else:
print(" None - all subnets are documented")
print(f"\nMissing IP Addresses (not in NetBox):")
if missing_ips:
for ip in missing_ips:
print(f" - {ip['address']} ({ip['interface']})")
else:
print(" None - all IPs are documented")
# Summary
total_router_subnets = len([s for s in router_subnets if '/32' not in s['address']])
total_router_ips = len([s for s in router_subnets if '/32' in s['address']])
print(f"\n=== Summary ===")
print(f"Router has {total_router_subnets} subnets and {total_router_ips} host IPs")
print(f"NetBox has {len(prefixes)} prefixes and {len(ip_addresses)} IP addresses for Verona")
print(f"Missing from NetBox: {len(missing_subnets)} subnets and {len(missing_ips)} IPs")
return missing_subnets, missing_ips
if __name__ == "__main__":
compare_subnets()