128 lines
5.6 KiB
Python
128 lines
5.6 KiB
Python
"""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")
|
|
REMOTE_DB_FILE: str = os.getenv("HAPROXY_REMOTE_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"))
|
|
|
|
# Custom multi-part TLDs (e.g., "it.com" treated as a TLD so "inouter.com" is a base domain)
|
|
# Comma-separated list via env var, or default
|
|
CUSTOM_TLDS: frozenset[str] = frozenset(
|
|
t.strip() for t in os.getenv("HAPROXY_CUSTOM_TLDS", "it.com").split(",") if t.strip()
|
|
)
|
|
|
|
# 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
|