Skip to main content
Glama

NetBox Read/Write MCP Server

config.py19 kB
#!/usr/bin/env python3 """ Configuration management for NetBox MCP Server Supports YAML and TOML configuration files with environment variable overrides. Configuration hierarchy (highest priority first): 1. Environment variables (via secrets management) 2. Configuration file 3. Default values Safety-focused configuration with write operation controls and enterprise secrets management. """ import logging from pathlib import Path from typing import Dict, Any, Optional from dataclasses import dataclass, field from .secrets import get_secrets_manager, validate_secrets try: import yaml except ImportError: yaml = None try: import tomllib # Python 3.11+ except ImportError: try: import tomli as tomllib # Fallback for older Python versions except ImportError: tomllib = None logger = logging.getLogger(__name__) @dataclass class SafetyConfig: """Safety configuration for write operations.""" # Global safety controls dry_run_mode: bool = False # Global dry-run mode require_confirmation: bool = True # Require confirm=True for write ops enable_write_operations: bool = True # Master switch for all write ops # Operation timeouts and limits write_timeout: int = 60 # Timeout for write operations max_batch_size: int = 100 # Maximum objects per batch operation # Audit and logging audit_all_operations: bool = True # Log all operations (read/write) audit_write_details: bool = True # Detailed logging for write operations # Rollback and recovery enable_transaction_mode: bool = True # Enable transaction-like operations auto_rollback_on_error: bool = True # Auto-rollback on partial failures @dataclass class CacheTTLConfig: """ TTL configuration following Gemini's caching strategy. TTLs range from static (long TTL) to dynamic (short TTL) based on how frequently the data changes in typical NetBox deployments. """ # Priority 1: Static objects (high cache value, low risk) manufacturers: int = 86400 # 1 day - manufacturers rarely change device_types: int = 86400 # 1 day - device types rarely change sites: int = 3600 # 1 hour - sites change occasionally device_roles: int = 86400 # 1 day - device roles rarely change # Priority 2: Semi-static objects devices: int = 300 # 5 minutes - devices change more frequently # Priority 3: Dynamic objects (conservative TTL) ip_addresses: int = 60 # 1 minute - IP addresses very dynamic device_interfaces: int = 60 # 1 minute - interfaces change frequently vlans: int = 300 # 5 minutes - VLANs moderately dynamic # System status (always fresh) status: int = 30 # 30 seconds - status should be fresh health: int = 30 # 30 seconds - health should be fresh # Default for unlisted operations (conservative) default: int = 300 # 5 minutes default @dataclass class CacheConfig: """Configuration for response caching.""" # Basic settings enabled: bool = True backend: str = "memory" # 'memory' or 'disk' # Size limits size_limit_mb: int = 200 # Cache size limit in megabytes max_items: int = 2000 # Maximum number of cached items # File-based cache settings (disk backend only) path: Optional[str] = "/tmp/netbox_mcp_cache" # TTL configuration ttl: CacheTTLConfig = field(default_factory=CacheTTLConfig) # Advanced features warm_on_startup: bool = False # Whether to warm cache on startup compression: bool = False # Whether to compress cached data # Statistics enable_stats: bool = True # Whether to track cache statistics @dataclass class LoggingConfig: """Structured logging configuration for enterprise deployment.""" # Basic logging settings level: str = "INFO" format: str = "json" # "json" for structured, "text" for human-readable # File logging settings enable_file_logging: bool = False log_file_path: Optional[str] = None max_file_size_mb: int = 10 backup_count: int = 5 # Service identification service_name: str = "netbox-mcp" service_version: str = "0.6.0" # Component-specific log levels component_levels: Dict[str, str] = field(default_factory=lambda: { "netbox_mcp.client": "INFO", "netbox_mcp.server": "INFO", "netbox_mcp.tools": "INFO", "netbox_mcp.secrets": "WARNING", "urllib3": "WARNING", "requests": "WARNING", "pynetbox": "INFO" }) # Performance and correlation enable_correlation_ids: bool = True enable_performance_logging: bool = True # Production detection auto_detect_production: bool = True # Auto-switch to JSON in container environments @dataclass class NetBoxConfig: """Configuration settings for NetBox MCP Server""" # Required NetBox connection settings url: str = "" token: str = "" # Optional connection settings timeout: int = 30 verify_ssl: bool = True # Server settings log_level: str = "INFO" # Legacy field, use logging.level instead health_check_port: int = 8080 # Safety configuration (CRITICAL) safety: SafetyConfig = field(default_factory=SafetyConfig) # Performance settings default_page_size: int = 50 # Smaller default for NetBox max_results: int = 1000 # Feature flags enable_health_server: bool = True enable_degraded_mode: bool = True enable_read_operations: bool = True # Advanced settings custom_headers: Dict[str, str] = field(default_factory=dict) # Cache configuration cache: CacheConfig = field(default_factory=CacheConfig) # Logging configuration logging: LoggingConfig = field(default_factory=LoggingConfig) def __post_init__(self): """Validate configuration after initialization""" if not self.url: raise ValueError("NetBox URL must be specified (NETBOX_URL)") if not self.token: raise ValueError("NetBox API token must be specified (NETBOX_TOKEN)") # Normalize URL self.url = self.url.rstrip('/') # Validate URL format if not (self.url.startswith('http://') or self.url.startswith('https://')): raise ValueError("NetBox URL must start with http:// or https://") # Validate log level valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] if self.log_level.upper() not in valid_levels: raise ValueError(f"Invalid log level: {self.log_level}. Must be one of {valid_levels}") # Validate numeric values if self.timeout <= 0: raise ValueError("Timeout must be positive") if self.health_check_port <= 0 or self.health_check_port > 65535: raise ValueError("Health check port must be between 1 and 65535") if self.default_page_size <= 0: raise ValueError("Default page size must be positive") if self.max_results <= 0: raise ValueError("Max results must be positive") # Safety validations if self.safety.write_timeout <= 0: raise ValueError("Write timeout must be positive") if self.safety.max_batch_size <= 0: raise ValueError("Max batch size must be positive") # Log safety configuration warnings if self.safety.dry_run_mode: logger.warning("NetBox MCP running in DRY-RUN mode - no actual writes will be performed") if not self.safety.require_confirmation: logger.warning("Confirmation requirement DISABLED - write operations can execute without confirm=True") if not self.safety.enable_write_operations: logger.info("Write operations DISABLED - server will be read-only") # Log secure connection information secrets_manager = get_secrets_manager() connection_info = secrets_manager.get_connection_info() logger.info(f"NetBox connection configured: {connection_info['url']}") logger.debug(f"Connection security: SSL cert={connection_info['has_ssl_cert']}, " f"SSL key={connection_info['has_ssl_key']}, CA cert={connection_info['has_ca_cert']}") # Validate that required secrets are available secret_validation = validate_secrets() missing_secrets = [key for key, available in secret_validation.items() if not available] if missing_secrets: logger.warning(f"Missing required secrets: {missing_secrets}") else: logger.info("All required secrets validated successfully") class ConfigurationManager: """Manages configuration loading from multiple sources""" DEFAULT_CONFIG_PATHS = [ "netbox-mcp.yaml", "netbox-mcp.yml", "netbox-mcp.toml", "config/netbox-mcp.yaml", "config/netbox-mcp.yml", "config/netbox-mcp.toml", ".netbox-mcp.yaml", ".netbox-mcp.yml", ".netbox-mcp.toml", "/etc/netbox-mcp/config.yaml", "/etc/netbox-mcp/config.yml", "/etc/netbox-mcp/config.toml", ] ENV_PREFIX = "NETBOX_" @classmethod def load_config(cls, config_path: Optional[str] = None) -> NetBoxConfig: """ Load configuration from environment variables and optional config file. Args: config_path: Optional path to configuration file Returns: NetBoxConfig: Loaded and validated configuration """ # Start with default configuration config_data = {} # Load from configuration file if found if config_path or cls._find_config_file(): file_path = config_path or cls._find_config_file() config_data = cls._load_config_file(file_path) logger.info(f"Loaded configuration from {file_path}") # Override with environment variables and secrets env_config = cls._load_from_environment_and_secrets() config_data.update(env_config) # Create and validate configuration try: # Handle nested dataclass creation config_data = cls._process_nested_config(config_data) config = NetBoxConfig(**config_data) logger.info("Configuration loaded and validated successfully") return config except Exception as e: logger.error(f"Configuration validation failed: {e}") raise @classmethod def _find_config_file(cls) -> Optional[str]: """Find the first existing configuration file from default paths.""" for path in cls.DEFAULT_CONFIG_PATHS: if Path(path).exists(): return path return None @classmethod def _load_config_file(cls, file_path: str) -> Dict[str, Any]: """Load configuration from YAML or TOML file.""" path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"Configuration file not found: {file_path}") try: with open(path, 'r', encoding='utf-8') as f: if path.suffix.lower() in ['.yaml', '.yml']: if yaml is None: raise ImportError("PyYAML is required for YAML configuration files") return yaml.safe_load(f) or {} elif path.suffix.lower() == '.toml': if tomllib is None: raise ImportError("tomli/tomllib is required for TOML configuration files") content = f.read() return tomllib.loads(content) else: raise ValueError(f"Unsupported configuration file format: {path.suffix}") except Exception as e: logger.error(f"Failed to load configuration file {file_path}: {e}") raise @classmethod def _load_from_environment_and_secrets(cls) -> Dict[str, Any]: """Load configuration from environment variables and secrets management.""" config = {} secrets_manager = get_secrets_manager() # Direct mappings for main config with secrets management env_mappings = { 'NETBOX_URL': 'url', 'NETBOX_TOKEN': 'token', 'NETBOX_TIMEOUT': ('timeout', int), 'NETBOX_VERIFY_SSL': ('verify_ssl', cls._parse_bool), 'NETBOX_LOG_LEVEL': 'log_level', 'NETBOX_HEALTH_CHECK_PORT': ('health_check_port', int), 'NETBOX_DEFAULT_PAGE_SIZE': ('default_page_size', int), 'NETBOX_MAX_RESULTS': ('max_results', int), 'NETBOX_ENABLE_HEALTH_SERVER': ('enable_health_server', cls._parse_bool), 'NETBOX_ENABLE_DEGRADED_MODE': ('enable_degraded_mode', cls._parse_bool), 'NETBOX_ENABLE_READ_OPERATIONS': ('enable_read_operations', cls._parse_bool), } # Safety configuration mappings safety_mappings = { 'NETBOX_DRY_RUN': ('safety.dry_run_mode', cls._parse_bool), 'NETBOX_REQUIRE_CONFIRMATION': ('safety.require_confirmation', cls._parse_bool), 'NETBOX_ENABLE_WRITE_OPERATIONS': ('safety.enable_write_operations', cls._parse_bool), 'NETBOX_WRITE_TIMEOUT': ('safety.write_timeout', int), 'NETBOX_MAX_BATCH_SIZE': ('safety.max_batch_size', int), 'NETBOX_AUDIT_ALL_OPERATIONS': ('safety.audit_all_operations', cls._parse_bool), 'NETBOX_AUDIT_WRITE_DETAILS': ('safety.audit_write_details', cls._parse_bool), 'NETBOX_ENABLE_TRANSACTION_MODE': ('safety.enable_transaction_mode', cls._parse_bool), 'NETBOX_AUTO_ROLLBACK_ON_ERROR': ('safety.auto_rollback_on_error', cls._parse_bool), } # Cache configuration mappings cache_mappings = { 'NETBOX_CACHE_ENABLED': ('cache.enabled', cls._parse_bool), 'NETBOX_CACHE_BACKEND': ('cache.backend', str), 'NETBOX_CACHE_SIZE_LIMIT_MB': ('cache.size_limit_mb', int), 'NETBOX_CACHE_MAX_ITEMS': ('cache.max_items', int), 'NETBOX_CACHE_PATH': ('cache.path', str), 'NETBOX_CACHE_ENABLE_STATS': ('cache.enable_stats', cls._parse_bool), } # Logging configuration mappings logging_mappings = { 'NETBOX_LOG_LEVEL': ('logging.level', str), 'NETBOX_LOG_FORMAT': ('logging.format', str), 'NETBOX_LOG_FILE_ENABLED': ('logging.enable_file_logging', cls._parse_bool), 'NETBOX_LOG_FILE_PATH': ('logging.log_file_path', str), 'NETBOX_LOG_FILE_MAX_SIZE_MB': ('logging.max_file_size_mb', int), 'NETBOX_LOG_FILE_BACKUP_COUNT': ('logging.backup_count', int), 'NETBOX_LOG_SERVICE_NAME': ('logging.service_name', str), 'NETBOX_LOG_SERVICE_VERSION': ('logging.service_version', str), 'NETBOX_LOG_ENABLE_CORRELATION_IDS': ('logging.enable_correlation_ids', cls._parse_bool), 'NETBOX_LOG_ENABLE_PERFORMANCE': ('logging.enable_performance_logging', cls._parse_bool), } # Combine all mappings all_mappings = {**env_mappings, **safety_mappings, **cache_mappings, **logging_mappings} for env_var, config_key in all_mappings.items(): # Use secrets manager to get values (handles all sources) value = secrets_manager.get_secret(env_var) if value is not None: if isinstance(config_key, tuple): key, converter = config_key try: value = converter(value) except (ValueError, TypeError) as e: logger.warning(f"Invalid value for {env_var}: {secrets_manager.mask_for_logging(env_var)} ({e})") continue else: key = config_key # Handle nested configuration cls._set_nested_value(config, key, value) return config @classmethod def _load_from_environment(cls) -> Dict[str, Any]: """Legacy method - redirects to secrets management.""" return cls._load_from_environment_and_secrets() @staticmethod def _parse_bool(value: str) -> bool: """Parse boolean value from string.""" if isinstance(value, bool): return value return value.lower() in ('true', '1', 'yes', 'on', 'enabled') @staticmethod def _set_nested_value(config: Dict[str, Any], key: str, value: Any): """Set nested configuration value using dot notation.""" keys = key.split('.') current = config for k in keys[:-1]: if k not in current: current[k] = {} current = current[k] current[keys[-1]] = value @classmethod def _process_nested_config(cls, config_data: Dict[str, Any]) -> Dict[str, Any]: """Process nested configuration to create proper dataclass instances.""" processed = config_data.copy() # Handle safety configuration if 'safety' in processed and isinstance(processed['safety'], dict): processed['safety'] = SafetyConfig(**processed['safety']) # Handle cache configuration if 'cache' in processed and isinstance(processed['cache'], dict): cache_config = processed['cache'].copy() # Handle cache TTL configuration if 'ttl' in cache_config and isinstance(cache_config['ttl'], dict): cache_config['ttl'] = CacheTTLConfig(**cache_config['ttl']) processed['cache'] = CacheConfig(**cache_config) # Handle logging configuration if 'logging' in processed and isinstance(processed['logging'], dict): processed['logging'] = LoggingConfig(**processed['logging']) return processed def load_config(config_path: Optional[str] = None) -> NetBoxConfig: """ Convenience function to load configuration. Args: config_path: Optional path to configuration file Returns: NetBoxConfig: Loaded and validated configuration """ return ConfigurationManager.load_config(config_path)

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Deployment-Team/netbox-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server