341 lines
No EOL
14 KiB
Python
Executable file
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()) |