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

369 lines
No EOL
13 KiB
Python

#!/usr/bin/env python3
import os
import sys
import json
import argparse
import requests
from urllib.parse import urljoin
class NetBoxUpdater:
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 post(self, endpoint, data):
"""Make POST request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.post(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error creating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
def patch(self, endpoint, data):
"""Make PATCH request to NetBox API"""
url = urljoin(self.api_url, endpoint.lstrip('/'))
response = self.session.patch(url, json=data)
if response.status_code not in [200, 201]:
print(f"Error updating {endpoint}: {response.status_code}")
print(f"Response: {response.text}")
response.raise_for_status()
return response.json()
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_or_create_role(self, name, slug=None):
"""Get or create an IPAM role"""
if not slug:
slug = name.lower().replace(' ', '-')
response = self.get('ipam/roles/', params={'slug': slug})
if response['count'] > 0:
return response['results'][0]['id']
# Create role
role_data = {
'name': name,
'slug': slug
}
created = self.post('ipam/roles/', role_data)
return created['id']
def determine_prefix_role(interface, description=''):
"""Determine the role of a prefix based on interface name and description"""
interface_lower = str(interface).lower()
# Infrastructure links
if any(term in interface_lower for term in ['airfiber', '11ghz', '24ghz', 'backhaul', 'ether3', 'ether4', 'ether5', 'ether6']):
return 'infrastructure'
# Loopback
if 'loopback' in interface_lower or interface == 'lo':
return 'loopback'
# Management
if any(term in interface_lower for term in ['mgmt', 'management', 'ether1']):
return 'management'
# Customer bridges
if 'bridge' in interface_lower or 'pppoe' in interface_lower:
return 'customer'
# VLANs are often customer-facing
if 'vlan' in interface_lower:
return 'customer'
return 'infrastructure' # Default
def process_router_data(json_file, site_name, dry_run=False):
"""Process router data JSON and update NetBox"""
# Load router data
with open(json_file, 'r') as f:
router_data = json.load(f)
# Get API token
api_token = os.environ.get('NETBOX_KEY', 'e50298f7fd20f7fd6f1931f635511b34f6e8cfde')
nb = NetBoxUpdater('https://netbox.vntx.net/', api_token)
print(f"=== Processing Router Data for {site_name} ===\n")
# Get site ID
site_response = nb.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
print(f"Error: Site '{site_name}' not found in NetBox")
print(f"Please create the site first using: python3 create_site_and_router.py {site_name} {router_data['host']}")
return False
site_id = site_response['results'][0]['id']
print(f"Found site: {site_name} (ID: {site_id})")
# Get device
device_response = nb.get('dcim/devices/', params={'site_id': site_id})
if device_response['count'] == 0:
print(f"Error: No device found for site '{site_name}'")
return False
device = device_response['results'][0]
device_id = device['id']
device_name = device['name']
print(f"Found device: {device_name} (ID: {device_id})\n")
# Get or create roles
role_mapping = {
'infrastructure': 'Infrastructure',
'loopback': 'Loopback',
'customer': 'Customer',
'management': 'Management'
}
if not dry_run:
print("Setting up IPAM roles...")
role_ids = {}
for key, name in role_mapping.items():
role_ids[key] = nb.get_or_create_role(name, key)
print(f"{name}")
print()
# Process subnets (non-dynamic only)
static_subnets = [s for s in router_data['subnets'] if not s.get('dynamic', False)]
print(f"=== Processing {len(static_subnets)} Static Subnets ===")
prefixes_to_create = []
ips_to_create = []
for subnet in static_subnets:
address = subnet['address']
network = subnet['network']
interface = subnet['interface']
comment = subnet.get('comment', '')
# Skip PPPoE dynamic IPs
if str(interface).startswith('<pppoe-'):
continue
# Determine if it's a host IP or a subnet
if address.endswith('/32'):
# It's a host IP
role = determine_prefix_role(interface, comment)
ips_to_create.append({
'address': address,
'interface': interface,
'description': comment or f"{device_name} - {interface}",
'role': role
})
else:
# It's a subnet
prefix_bits = address.split('/')[-1]
prefix = f"{network}/{prefix_bits}"
role = determine_prefix_role(interface, comment)
prefixes_to_create.append({
'prefix': prefix,
'interface': interface,
'description': comment or f"{site_name} - {interface}",
'role': role,
'gateway_ip': address
})
if dry_run:
print("\n=== DRY RUN - Would create the following: ===\n")
print(f"Prefixes ({len(prefixes_to_create)}):")
for p in prefixes_to_create:
print(f" - {p['prefix']} ({p['role']}) - {p['description']}")
print(f"\nIP Addresses ({len(ips_to_create)}):")
for ip in ips_to_create:
print(f" - {ip['address']} ({ip['role']}) - {ip['description']}")
return True
# Create prefixes
print(f"\n=== Creating/Updating {len(prefixes_to_create)} Prefixes ===")
created_prefixes = 0
failed_prefixes = 0
for item in prefixes_to_create:
# Check if prefix already exists
prefix_response = nb.get('ipam/prefixes/', params={'prefix': item['prefix']})
prefix_data = {
'prefix': item['prefix'],
'site': site_id,
'status': 'active',
'description': item['description'],
'is_pool': False
}
if role_ids.get(item['role']):
prefix_data['role'] = role_ids[item['role']]
try:
if prefix_response['count'] == 0:
# Create new prefix
created_prefix = nb.post('ipam/prefixes/', prefix_data)
print(f"✓ Created prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
else:
# Update existing prefix
existing_id = prefix_response['results'][0]['id']
updated_prefix = nb.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
print(f"✓ Updated prefix: {item['prefix']} - {item['description']}")
created_prefixes += 1
# Create gateway IP if needed
if item.get('gateway_ip'):
gw_response = nb.get('ipam/ip-addresses/', params={'address': item['gateway_ip']})
if gw_response['count'] == 0:
gw_data = {
'address': item['gateway_ip'],
'status': 'active',
'description': f"{item['interface']} gateway - {item['description']}",
'role': 'anycast' if item['role'] == 'infrastructure' else None
}
try:
nb.post('ipam/ip-addresses/', gw_data)
print(f" ✓ Created gateway IP: {item['gateway_ip']}")
except:
pass
except Exception as e:
print(f"✗ Failed to create/update prefix {item['prefix']}: {e}")
failed_prefixes += 1
print(f"\nPrefix Summary: {created_prefixes} successful, {failed_prefixes} failed")
# Create IP addresses
print(f"\n=== Creating/Updating {len(ips_to_create)} IP Addresses ===")
created_ips = 0
failed_ips = 0
for item in ips_to_create:
# Check if IP already exists
ip_response = nb.get('ipam/ip-addresses/', params={'address': item['address']})
ip_data = {
'address': item['address'],
'status': 'active',
'description': item['description']
}
if item['role'] == 'loopback':
ip_data['role'] = 'loopback'
try:
if ip_response['count'] == 0:
# Create new IP
created_ip = nb.post('ipam/ip-addresses/', ip_data)
print(f"✓ Created IP: {item['address']} - {item['description']}")
created_ips += 1
else:
# Update existing IP
existing_id = ip_response['results'][0]['id']
updated_ip = nb.patch(f'ipam/ip-addresses/{existing_id}/', ip_data)
print(f"✓ Updated IP: {item['address']} - {item['description']}")
created_ips += 1
except Exception as e:
print(f"✗ Failed to create/update IP {item['address']}: {e}")
failed_ips += 1
print(f"\nIP Summary: {created_ips} successful, {failed_ips} failed")
# Create interfaces on the device
print(f"\n=== Creating Device Interfaces ===")
created_interfaces = 0
# Get unique interfaces from both subnets and active interfaces
interface_set = set()
for subnet in static_subnets:
if not str(subnet['interface']).startswith('<'):
interface_set.add(str(subnet['interface']))
for iface in router_data.get('interfaces', []):
if not str(iface['name']).startswith('<'):
interface_set.add(str(iface['name']))
for interface_name in sorted(interface_set):
# Check if interface exists
interface_response = nb.get('dcim/interfaces/', params={
'device_id': device_id,
'name': interface_name
})
if interface_response['count'] == 0:
# Determine interface type
if 'vlan' in interface_name.lower():
iface_type = 'virtual'
elif 'bridge' in interface_name.lower():
iface_type = 'bridge'
elif 'loopback' in interface_name.lower() or interface_name == 'lo':
iface_type = 'virtual'
else:
iface_type = '1000base-t' # Default to gigabit ethernet
interface_data = {
'device': device_id,
'name': interface_name,
'type': iface_type,
'enabled': True
}
try:
created_interface = nb.post('dcim/interfaces/', interface_data)
print(f"✓ Created interface: {interface_name} ({iface_type})")
created_interfaces += 1
except Exception as e:
print(f"✗ Failed to create interface {interface_name}: {e}")
print(f"\nCreated {created_interfaces} new interfaces")
print(f"\n=== Update Complete ===")
print(f"Site: {site_name}")
print(f"Device: {device_name}")
print(f"Prefixes: {created_prefixes}/{len(prefixes_to_create)}")
print(f"IPs: {created_ips}/{len(ips_to_create)}")
print(f"Interfaces: {created_interfaces}")
return True
def main():
parser = argparse.ArgumentParser(description='Update NetBox with router data from JSON file')
parser.add_argument('json_file', help='Path to router data JSON file')
parser.add_argument('site_name', help='Name of the site in NetBox')
parser.add_argument('--dry-run', action='store_true', help='Show what would be created without making changes')
args = parser.parse_args()
# Check if file exists
if not os.path.exists(args.json_file):
print(f"Error: File '{args.json_file}' not found")
return 1
# Process the data
success = process_router_data(args.json_file, args.site_name, args.dry_run)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())