"""Configuration constants and environment variables for HAProxy MCP Server.""" import os import re import logging # Configure structured logging log_level = os.getenv("LOG_LEVEL", "INFO").upper() logging.basicConfig( level=getattr(logging, log_level, logging.INFO), format='%(asctime)s [%(levelname)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S' ) logger = logging.getLogger("haproxy_mcp") # MCP Server configuration MCP_HOST: str = os.getenv("MCP_HOST", "0.0.0.0") MCP_PORT: int = int(os.getenv("MCP_PORT", "8000")) # HAProxy Runtime API configuration HAPROXY_HOST: str = os.getenv("HAPROXY_HOST", "localhost") HAPROXY_PORT: int = int(os.getenv("HAPROXY_PORT", "9999")) HAPROXY_SOCKET: tuple[str, int] = (HAPROXY_HOST, HAPROXY_PORT) # File paths (configurable via environment) STATE_FILE: str = os.getenv("HAPROXY_STATE_FILE", "/opt/haproxy/data/servers.state") MAP_FILE: str = os.getenv("HAPROXY_MAP_FILE", "/opt/haproxy/conf/domains.map") MAP_FILE_CONTAINER: str = os.getenv("HAPROXY_MAP_FILE_CONTAINER", "/usr/local/etc/haproxy/domains.map") # Wildcards map for 2-stage matching (map_dom fallback) WILDCARDS_MAP_FILE: str = os.getenv("HAPROXY_WILDCARDS_MAP_FILE", "/opt/haproxy/conf/wildcards.map") WILDCARDS_MAP_FILE_CONTAINER: str = os.getenv("HAPROXY_WILDCARDS_MAP_FILE_CONTAINER", "/usr/local/etc/haproxy/wildcards.map") SERVERS_FILE: str = os.getenv("HAPROXY_SERVERS_FILE", "/opt/haproxy/conf/servers.json") CERTS_FILE: str = os.getenv("HAPROXY_CERTS_FILE", "/opt/haproxy/conf/certificates.json") DB_FILE: str = os.getenv("HAPROXY_DB_FILE", "/opt/haproxy/conf/haproxy_mcp.db") # Certificate paths CERTS_DIR: str = os.getenv("HAPROXY_CERTS_DIR", "/opt/haproxy/certs") CERTS_DIR_CONTAINER: str = os.getenv("HAPROXY_CERTS_DIR_CONTAINER", "/etc/haproxy/certs") ACME_HOME: str = os.getenv("ACME_HOME", os.path.expanduser("~/.acme.sh")) # Pool configuration POOL_COUNT: int = int(os.getenv("HAPROXY_POOL_COUNT", "100")) MAX_SLOTS: int = int(os.getenv("HAPROXY_MAX_SLOTS", "10")) # Container configuration HAPROXY_CONTAINER: str = os.getenv("HAPROXY_CONTAINER", "haproxy") # SSH remote execution (when MCP runs on a different host from HAProxy) SSH_HOST: str = os.getenv("SSH_HOST", "") # Empty = local mode SSH_USER: str = os.getenv("SSH_USER", "root") SSH_KEY: str = os.getenv("SSH_KEY", "") # Path to SSH private key SSH_PORT: int = int(os.getenv("SSH_PORT", "22")) REMOTE_MODE: bool = bool(SSH_HOST) # Validation patterns - compiled once for performance DOMAIN_PATTERN = re.compile( r'^[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?' r'(\.[a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?)*$' ) # Backend/server names: alphanumeric, underscore, hyphen only BACKEND_NAME_PATTERN = re.compile(r'^[a-zA-Z0-9_-]+$') # Pattern for converting domain to backend name NON_ALNUM_PATTERN = re.compile(r'[^a-zA-Z0-9]') # Limits and Constants MAX_RESPONSE_SIZE = 10 * 1024 * 1024 # 10 MB max response from HAProxy SUBPROCESS_TIMEOUT = 30 # seconds for podman exec commands (config validation, reload) # STARTUP_RETRY_COUNT: Number of attempts to verify HAProxy is ready on MCP startup. # During startup, MCP needs to restore server configurations from servers.json. # HAProxy may take a few seconds to fully initialize the Runtime API socket. # Each retry waits 1 second, so 10 retries = max 10 seconds startup wait. # If HAProxy isn't ready after 10 attempts, startup proceeds but logs a warning. STARTUP_RETRY_COUNT = 10 # STATE_MIN_COLUMNS: Expected minimum column count in 'show servers state' output. # HAProxy 'show servers state' returns tab-separated values with the following fields: # 0: be_id - Backend ID # 1: be_name - Backend name # 2: srv_id - Server ID # 3: srv_name - Server name # 4: srv_addr - Server IP address # 5: srv_op_state - Operational state (0=stopped, 1=starting, 2=running, etc.) # 6: srv_admin_state - Admin state (0=ready, 1=drain, 2=maint, etc.) # 7-17: Various internal state fields (weight, check info, etc.) # 18: srv_port - Server port # Total: 19+ columns (may increase in future HAProxy versions) # Lines with fewer columns are invalid/incomplete and should be skipped. STATE_MIN_COLUMNS = 19 SOCKET_TIMEOUT = 5 # seconds for HAProxy socket connection establishment SOCKET_RECV_TIMEOUT = 30 # seconds for complete response (large stats output) MAX_BULK_SERVERS = 10 # Max servers per bulk add call (prevents oversized requests) MAX_SERVERS_JSON_SIZE = 10000 # Max size of servers JSON input (10KB, prevents abuse) # CSV field indices for HAProxy stats (show stat command) class StatField: """HAProxy CSV stat field indices.""" PXNAME = 0 # Proxy name (frontend/backend) SVNAME = 1 # Server name (or FRONTEND/BACKEND) SCUR = 4 # Current sessions SMAX = 6 # Max sessions STATUS = 17 # Status (UP/DOWN/MAINT/etc) WEIGHT = 18 # Server weight CHECK_STATUS = 36 # Check status # Field indices for HAProxy server state (show servers state command) class StateField: """HAProxy server state field indices.""" BE_ID = 0 # Backend ID BE_NAME = 1 # Backend name SRV_ID = 2 # Server ID SRV_NAME = 3 # Server name SRV_ADDR = 4 # Server address SRV_OP_STATE = 5 # Operational state SRV_ADMIN_STATE = 6 # Admin state SRV_PORT = 18 # Server port