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

341 lines
No EOL
14 KiB
Python
Executable file

#!/usr/bin/env python3
import requests
import json
import argparse
import sys
from urllib.parse import urljoin
from mikrotik_connect import MikrotikAPI
class NetBoxSync:
def __init__(self, netbox_url, netbox_token):
self.base_url = netbox_url.rstrip('/')
self.api_url = urljoin(self.base_url + '/', 'api/')
self.headers = {
'Authorization': f'Token {netbox_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_prefix_role(self, role_name):
"""Get or create a prefix role"""
# Check if role exists
role_response = self.get('ipam/roles/', params={'name': role_name})
if role_response['count'] > 0:
return role_response['results'][0]['id']
# Create role
role_data = {
'name': role_name,
'slug': role_name.lower().replace(' ', '-')
}
created_role = self.post('ipam/roles/', role_data)
return created_role['id']
def determine_prefix_role(self, interface_name, ip_address):
"""Determine the role of a prefix based on interface and IP"""
interface_lower = interface_name.lower()
if 'loopback' in interface_lower:
return 'Loopback'
elif any(mgmt in interface_lower for mgmt in ['mgmt', 'management', 'ether1']):
return 'Management'
elif any(infra in interface_lower for infra in ['tower', 'backhaul', 'climax', 'switch']):
return 'Infrastructure'
elif ip_address.startswith('100.64.'):
return 'CGNAT'
elif ip_address.startswith('10.10.'):
return 'CPE-Management'
elif any(cust in interface_lower for cust in ['customer', 'cpe', 'vlan']):
return 'Customer'
else:
return 'Unknown'
def determine_interface_type(self, interface_name):
"""Determine the NetBox interface type based on MikroTik interface name"""
interface_lower = interface_name.lower()
# Check for specific interface types first (most specific to least specific)
if 'loopback' in interface_lower or interface_lower == 'lo':
return 'virtual'
elif 'vlan' in interface_lower:
return 'virtual'
elif 'bond' in interface_lower or 'bonding' in interface_lower:
return 'lag'
elif 'pppoe' in interface_lower:
return 'virtual'
elif 'sfp-sfpplus' in interface_lower or 'sfpplus' in interface_lower:
return '10gbase-x-sfpp'
elif 'sfp' in interface_lower:
return '1000base-x-sfp'
elif 'ether' in interface_lower:
return '1000base-t'
elif 'wlan' in interface_lower or 'wireless' in interface_lower:
return 'ieee802.11n'
elif 'bridge' in interface_lower:
return 'bridge'
# Only check for exact matches for bridge names
elif interface_lower in ['verona', 'lowry', 'culleoka', 'climax', 'yorkshire', 'new-hope']:
return 'bridge'
else:
return 'other'
def sync_router_to_netbox(self, router_ip, router_user, router_pass, site_name, device_name=None):
"""Sync router configuration to NetBox"""
# Connect to router and get data
print(f"Connecting to router {router_ip}...")
api = MikrotikAPI(router_ip, router_user, router_pass)
try:
if not api.connect():
print("Failed to connect to router")
return False
if not api.login():
print("Failed to login to router")
return False
# Get all IP addresses from router
addresses = api.command("/ip/address/print")
print(f"Retrieved {len(addresses)} IP addresses from router")
finally:
api.disconnect()
# Get site from NetBox
site_response = self.get('dcim/sites/', params={'name': site_name})
if site_response['count'] == 0:
print(f"Error: Site '{site_name}' not found in NetBox")
return False
site_id = site_response['results'][0]['id']
print(f"Found site '{site_name}' with ID: {site_id}")
# Get device if specified
device_id = None
if device_name:
device_response = self.get('dcim/devices/', params={'name': device_name})
if device_response['count'] > 0:
device_id = device_response['results'][0]['id']
print(f"Found device '{device_name}' with ID: {device_id}")
else:
# Try to find a device at this site
device_response = self.get('dcim/devices/', params={'site_id': site_id})
if device_response['count'] > 0:
device_id = device_response['results'][0]['id']
device_name = device_response['results'][0]['name']
print(f"Found device '{device_name}' at site")
# Process addresses (excluding dynamic PPPoE)
prefixes_to_create = {}
ips_to_update = []
for addr in addresses:
# Skip disabled and dynamic addresses
if addr.get('disabled', 'false') == 'true':
continue
if addr.get('dynamic', 'false') == 'true' and 'pppoe' in addr.get('interface', '').lower():
continue
interface = addr.get('interface', 'unknown')
address = addr.get('address', '')
network = addr.get('network', '')
if not address or not network:
continue
# Calculate prefix
prefix_bits = address.split('/')[-1]
prefix = f"{network}/{prefix_bits}"
# Store unique prefixes
if prefix not in prefixes_to_create and prefix != f"{network}/32":
prefixes_to_create[prefix] = {
'interface': interface,
'role': self.determine_prefix_role(interface, address)
}
# Store IPs to update
ips_to_update.append({
'address': address,
'interface': interface,
'prefix': prefix
})
# Create/Update prefixes
print(f"\n=== Processing {len(prefixes_to_create)} Prefixes ===")
prefix_stats = {'created': 0, 'updated': 0, 'failed': 0}
for prefix, info in prefixes_to_create.items():
# Check if prefix exists
prefix_response = self.get('ipam/prefixes/', params={'prefix': prefix})
prefix_data = {
'prefix': prefix,
'site': site_id,
'status': 'active',
'description': f"{site_name} - {info['interface']}",
'is_pool': False
}
# Add role
try:
role_id = self.get_or_create_prefix_role(info['role'])
prefix_data['role'] = role_id
except:
pass
try:
if prefix_response['count'] == 0:
# Create new prefix
self.post('ipam/prefixes/', prefix_data)
print(f"Created prefix: {prefix} ({info['role']})")
prefix_stats['created'] += 1
else:
# Update existing prefix
existing_id = prefix_response['results'][0]['id']
self.patch(f'ipam/prefixes/{existing_id}/', prefix_data)
print(f"Updated prefix: {prefix} ({info['role']})")
prefix_stats['updated'] += 1
except Exception as e:
print(f"Failed to process prefix {prefix}: {e}")
prefix_stats['failed'] += 1
print(f"\nPrefix Summary: {prefix_stats['created']} created, {prefix_stats['updated']} updated, {prefix_stats['failed']} failed")
# Update IP addresses
print(f"\n=== Processing {len(ips_to_update)} IP Addresses ===")
ip_stats = {'created': 0, 'updated': 0, 'failed': 0}
for ip_info in ips_to_update:
# Check if IP exists
ip_response = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})
ip_data = {
'address': ip_info['address'],
'status': 'active',
'description': f"{ip_info['interface']}",
'tenant': None
}
try:
if ip_response['count'] == 0:
# Create new IP
ip_data['site'] = site_id
self.post('ipam/ip-addresses/', ip_data)
print(f"Created IP: {ip_info['address']} ({ip_info['interface']})")
ip_stats['created'] += 1
else:
# Update existing IP
existing_ip = ip_response['results'][0]
existing_id = existing_ip['id']
# Only update if needed
if existing_ip.get('site', {}).get('id') != site_id or existing_ip.get('description') != ip_data['description']:
ip_data['site'] = site_id
self.patch(f'ipam/ip-addresses/{existing_id}/', ip_data)
print(f"Updated IP: {ip_info['address']} ({ip_info['interface']})")
ip_stats['updated'] += 1
# Create interface if device exists
if device_id:
interface_name = ip_info['interface']
# Check if interface exists
interface_response = self.get('dcim/interfaces/', params={
'device_id': device_id,
'name': interface_name
})
if interface_response['count'] == 0:
# Create interface
interface_data = {
'device': device_id,
'name': interface_name,
'type': self.determine_interface_type(interface_name),
'enabled': True
}
try:
created_interface = self.post('dcim/interfaces/', interface_data)
interface_id = created_interface['id']
# Assign IP to interface
ip_id = self.get('ipam/ip-addresses/', params={'address': ip_info['address']})['results'][0]['id']
self.patch(f'ipam/ip-addresses/{ip_id}/', {
'assigned_object_type': 'dcim.interface',
'assigned_object_id': interface_id
})
print(f" - Created interface: {interface_name} (Type: {interface_data['type']})")
except Exception as e:
print(f" - Failed to create interface {interface_name}: {e}")
except Exception as e:
print(f"Failed to process IP {ip_info['address']}: {e}")
ip_stats['failed'] += 1
print(f"\nIP Summary: {ip_stats['created']} created, {ip_stats['updated']} updated, {ip_stats['failed']} failed")
print("\n=== Sync Complete ===")
return True
def main():
parser = argparse.ArgumentParser(description='Sync MikroTik router configuration to NetBox')
parser.add_argument('router_ip', help='Router IP address')
parser.add_argument('site_name', help='NetBox site name')
parser.add_argument('--router-user', default='grahamro', help='Router username (default: grahamro)')
parser.add_argument('--router-pass', default='cFKhz8q5gPLoucMbcT1Iy58r3IXgc3', help='Router password')
parser.add_argument('--device-name', help='NetBox device name (optional)')
parser.add_argument('--netbox-url', default='https://netbox.vntx.net/', help='NetBox URL')
parser.add_argument('--netbox-token', default='e50298f7fd20f7fd6f1931f635511b34f6e8cfde', help='NetBox API token')
args = parser.parse_args()
# Create sync instance
sync = NetBoxSync(args.netbox_url, args.netbox_token)
# Run sync
success = sync.sync_router_to_netbox(
args.router_ip,
args.router_user,
args.router_pass,
args.site_name,
args.device_name
)
return 0 if success else 1
if __name__ == "__main__":
sys.exit(main())