#!/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())