""" NetBox/Nautobot Source of Truth Integration Supports both NetBox and Nautobot as SoT backends """ import os import logging from typing import Dict, List, Optional, Any from dataclasses import dataclass logger = logging.getLogger(__name__) @dataclass class NetBoxConfig: """NetBox/Nautobot connection configuration""" url: str token: str api_version: str = "2.0" ssl_verify: bool = True class NetBoxClient: """ Unified client for NetBox/Nautobot Provides CRUD operations for network SoT """ def __init__(self, config: Optional[NetBoxConfig] = None): """ Initialize NetBox client Falls back to environment variables if config not provided """ if config: self.config = config else: # Try to load from environment url = os.getenv("NETBOX_URL") or os.getenv("NAUTOBOT_URL") token = os.getenv("NETBOX_TOKEN") or os.getenv("NAUTOBOT_TOKEN") if not url or not token: logger.warning("No NetBox/Nautobot credentials found - using mock mode") self.mock_mode = True return self.config = NetBoxConfig(url=url, token=token) self.mock_mode = False self._init_client() def _init_client(self): """Initialize pynetbox client""" try: import pynetbox self.nb = pynetbox.api( self.config.url, token=self.config.token ) self.nb.http_session.verify = self.config.ssl_verify logger.info(f"Connected to NetBox at {self.config.url}") except ImportError: logger.error("pynetbox not installed - install with: pip install pynetbox") self.mock_mode = True except Exception as e: logger.error(f"Failed to connect to NetBox: {e}") self.mock_mode = True # ==================== Sites & Locations ==================== def create_site(self, name: str, description: str = "", **kwargs) -> Dict: """Create a site in NetBox""" if self.mock_mode: return {"id": 1, "name": name, "description": description} try: site = self.nb.dcim.sites.create( name=name, slug=name.lower().replace(" ", "-"), description=description, **kwargs ) return dict(site) except Exception as e: logger.error(f"Failed to create site: {e}") raise def get_site(self, name: str) -> Optional[Dict]: """Get site by name""" if self.mock_mode: return {"id": 1, "name": name} try: sites = self.nb.dcim.sites.filter(name=name) return dict(sites[0]) if sites else None except Exception as e: logger.error(f"Failed to get site: {e}") return None # ==================== Devices ==================== def create_device( self, name: str, device_type: str, site: str, role: str, **kwargs ) -> Dict: """Create a device in NetBox""" if self.mock_mode: return { "id": 1, "name": name, "device_type": {"model": device_type}, "site": {"name": site}, "device_role": {"name": role} } try: # Get or create device type dev_type = self._get_or_create_device_type(device_type) # Get or create site site_obj = self.get_site(site) if not site_obj: site_obj = self.create_site(site) # Get or create role role_obj = self._get_or_create_device_role(role) device = self.nb.dcim.devices.create( name=name, device_type=dev_type["id"], site=site_obj["id"], device_role=role_obj["id"], **kwargs ) return dict(device) except Exception as e: logger.error(f"Failed to create device: {e}") raise def get_devices(self, **filters) -> List[Dict]: """Get devices with optional filters""" if self.mock_mode: return [] try: devices = self.nb.dcim.devices.filter(**filters) return [dict(d) for d in devices] except Exception as e: logger.error(f"Failed to get devices: {e}") return [] # ==================== VLANs ==================== def create_vlan( self, vid: int, name: str, site: Optional[str] = None, description: str = "", **kwargs ) -> Dict: """Create a VLAN in NetBox""" if self.mock_mode: return {"id": 1, "vid": vid, "name": name, "description": description} try: vlan_data = { "vid": vid, "name": name, "description": description, **kwargs } if site: site_obj = self.get_site(site) if site_obj: vlan_data["site"] = site_obj["id"] vlan = self.nb.ipam.vlans.create(**vlan_data) return dict(vlan) except Exception as e: logger.error(f"Failed to create VLAN: {e}") raise def get_vlans(self, **filters) -> List[Dict]: """Get VLANs with optional filters""" if self.mock_mode: return [] try: vlans = self.nb.ipam.vlans.filter(**filters) return [dict(v) for v in vlans] except Exception as e: logger.error(f"Failed to get VLANs: {e}") return [] # ==================== Prefixes/Subnets ==================== def create_prefix( self, prefix: str, description: str = "", site: Optional[str] = None, vlan: Optional[int] = None, **kwargs ) -> Dict: """Create an IP prefix in NetBox""" if self.mock_mode: return {"id": 1, "prefix": prefix, "description": description} try: prefix_data = { "prefix": prefix, "description": description, **kwargs } if site: site_obj = self.get_site(site) if site_obj: prefix_data["site"] = site_obj["id"] if vlan: vlan_obj = self.nb.ipam.vlans.get(vid=vlan) if vlan_obj: prefix_data["vlan"] = vlan_obj.id prefix_obj = self.nb.ipam.prefixes.create(**prefix_data) return dict(prefix_obj) except Exception as e: logger.error(f"Failed to create prefix: {e}") raise def get_prefixes(self, **filters) -> List[Dict]: """Get prefixes with optional filters""" if self.mock_mode: return [] try: prefixes = self.nb.ipam.prefixes.filter(**filters) return [dict(p) for p in prefixes] except Exception as e: logger.error(f"Failed to get prefixes: {e}") return [] # ==================== Helper Methods ==================== def _get_or_create_device_type(self, model: str) -> Dict: """Get or create device type""" if self.mock_mode: return {"id": 1, "model": model} # Search for existing slug = model.lower().replace(" ", "-") device_types = self.nb.dcim.device_types.filter(slug=slug) if device_types: return dict(device_types[0]) # Create new # Get or create manufacturer manufacturer = self._get_or_create_manufacturer("Generic") device_type = self.nb.dcim.device_types.create( manufacturer=manufacturer["id"], model=model, slug=slug ) return dict(device_type) def _get_or_create_manufacturer(self, name: str) -> Dict: """Get or create manufacturer""" if self.mock_mode: return {"id": 1, "name": name} slug = name.lower().replace(" ", "-") manufacturers = self.nb.dcim.manufacturers.filter(slug=slug) if manufacturers: return dict(manufacturers[0]) manufacturer = self.nb.dcim.manufacturers.create( name=name, slug=slug ) return dict(manufacturer) def _get_or_create_device_role(self, name: str) -> Dict: """Get or create device role""" if self.mock_mode: return {"id": 1, "name": name} slug = name.lower().replace(" ", "-") roles = self.nb.dcim.device_roles.filter(slug=slug) if roles: return dict(roles[0]) role = self.nb.dcim.device_roles.create( name=name, slug=slug, color="9e9e9e" # Gray ) return dict(role) # ==================== Bulk Operations ==================== def sync_network_model(self, network_model: Dict) -> Dict: """ Sync entire network model to NetBox Creates/updates sites, devices, VLANs, prefixes Returns summary of created objects """ summary = { "sites": 0, "devices": 0, "vlans": 0, "prefixes": 0, "errors": [] } try: # Create site if specified site_name = network_model.get("name", "default-site") site = self.get_site(site_name) if not site: site = self.create_site(site_name, description=network_model.get("description", "")) summary["sites"] += 1 # Create VLANs for vlan in network_model.get("vlans", []): try: self.create_vlan( vid=vlan["id"], name=vlan["name"], site=site_name, description=vlan.get("purpose", "") ) summary["vlans"] += 1 except Exception as e: summary["errors"].append(f"VLAN {vlan['id']}: {str(e)}") # Create prefixes for subnet in network_model.get("subnets", []): try: self.create_prefix( prefix=subnet["network"], description=subnet.get("purpose", ""), site=site_name, vlan=subnet.get("vlan") ) summary["prefixes"] += 1 except Exception as e: summary["errors"].append(f"Prefix {subnet['network']}: {str(e)}") # Create devices for device in network_model.get("devices", []): try: self.create_device( name=device["name"], device_type=device.get("model", "Generic Switch"), site=site_name, role=device.get("role", "access") ) summary["devices"] += 1 except Exception as e: summary["errors"].append(f"Device {device['name']}: {str(e)}") except Exception as e: logger.error(f"Failed to sync network model: {e}") summary["errors"].append(f"Sync failed: {str(e)}") return summary