Files
haproxy-mcp/haproxy_mcp/config.py
kaffa 57ff3dc4fa
Some checks failed
CI/CD / build-and-deploy (push) Has been cancelled
chore: anvil.it.com → inouter.com
2026-03-27 16:18:26 +00:00

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