feat: Add batch operations, container health, code cleanup

7. Add haproxy_set_domain_state() tool
   - Set all servers of a domain to ready/drain/maint at once
   - Useful for maintenance windows and deployments
   - Reports changed servers and any errors

8. Add container status to haproxy_health()
   - Check HAProxy container state via podman inspect
   - Reports "ok" when running, actual state otherwise
   - Sets overall health to "unhealthy" on container issues

9. Extract configure_server_slot() helper
   - Eliminates duplicated server configuration code
   - Used by haproxy_add_server() and haproxy_add_servers()
   - Cleaner, more maintainable code

MCP Tools: 20 → 21

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
kaffa
2026-02-01 13:58:14 +00:00
parent 4e7d0a8969
commit c48456dc18

View File

@@ -584,6 +584,28 @@ def get_server_suffixes(http_port: int) -> List[Tuple[str, int]]:
return [("", http_port)]
def configure_server_slot(backend: str, server_prefix: str, slot: int, ip: str, http_port: int) -> str:
"""Configure a server slot in HAProxy.
Args:
backend: Backend name (e.g., 'pool_1')
server_prefix: Server name prefix (e.g., 'pool_1')
slot: Slot number (1-10)
ip: Server IP address
http_port: HTTP port
Returns:
Server name that was configured
Raises:
HaproxyError: If HAProxy command fails
"""
server = f"{server_prefix}_{slot}"
haproxy_cmd_checked(f"set server {backend}/{server} addr {ip} port {http_port}")
haproxy_cmd_checked(f"set server {backend}/{server} state ready")
return server
def get_backend_and_prefix(domain: str) -> Tuple[str, str]:
"""Look up backend and determine server name prefix for a domain.
@@ -938,14 +960,8 @@ def haproxy_add_server(domain: str, slot: int, ip: str, http_port: int = 80) ->
add_server_to_config(domain, slot, ip, http_port)
try:
results = []
for suffix, port in get_server_suffixes(http_port):
server = f"{server_prefix}{suffix}_{slot}"
haproxy_cmd_checked(f"set server {backend}/{server} addr {ip} port {port}")
haproxy_cmd_checked(f"set server {backend}/{server} state ready")
results.append(f"{server}{ip}:{port}")
return f"Added to {domain} ({backend}) slot {slot}:\n" + "\n".join(results)
server = configure_server_slot(backend, server_prefix, slot, ip, http_port)
return f"Added to {domain} ({backend}) slot {slot}:\n{server}{ip}:{http_port}"
except HaproxyError as e:
# Rollback config on HAProxy failure
remove_server_from_config(domain, slot)
@@ -1066,11 +1082,7 @@ def haproxy_add_servers(domain: str, servers: str) -> str:
http_port = srv["http_port"]
try:
for suffix, port in get_server_suffixes(http_port):
server = f"{server_prefix}{suffix}_{slot}"
haproxy_cmd_checked(f"set server {backend}/{server} addr {ip} port {port}")
haproxy_cmd_checked(f"set server {backend}/{server} state ready")
server = configure_server_slot(backend, server_prefix, slot, ip, http_port)
# Save to persistent config
add_server_to_config(domain, slot, ip, http_port)
added.append(f"slot {slot}: {ip}:{http_port}")
@@ -1128,6 +1140,71 @@ def haproxy_remove_server(domain: str, slot: int) -> str:
return f"Error: {e}"
@mcp.tool()
def haproxy_set_domain_state(domain: str, state: str) -> str:
"""Set state for all servers of a domain at once.
Useful for maintenance windows or deployments - set all servers to drain/maint
before updates, then back to ready after.
Args:
domain: The domain name
state: Target state - ready, drain, or maint
Returns:
Summary of state changes or error
Example:
# Put all servers in maintenance for deployment
haproxy_set_domain_state("api.example.com", "maint")
# Re-enable all servers after deployment
haproxy_set_domain_state("api.example.com", "ready")
"""
if not validate_domain(domain):
return "Error: Invalid domain format"
if state not in ["ready", "drain", "maint"]:
return "Error: State must be 'ready', 'drain', or 'maint'"
try:
backend, server_prefix = get_backend_and_prefix(domain)
except ValueError as e:
return f"Error: {e}"
# Get active servers for this domain
try:
state_output = haproxy_cmd("show servers state")
except HaproxyError as e:
return f"Error: {e}"
changed = []
errors = []
for line in state_output.split("\n"):
parts = line.split()
if len(parts) >= STATE_MIN_COLUMNS and parts[StateField.BE_NAME] == backend:
server_name = parts[StateField.SRV_NAME]
addr = parts[StateField.SRV_ADDR]
# Only change state for configured servers (not 0.0.0.0)
if addr != "0.0.0.0":
try:
haproxy_cmd_checked(f"set server {backend}/{server_name} state {state}")
changed.append(server_name)
except HaproxyError as e:
errors.append(f"{server_name}: {e}")
if not changed and not errors:
return f"No active servers found for {domain}"
result = f"Set {len(changed)} servers to '{state}' for {domain}"
if changed:
result += ":\n" + "\n".join(f"{s}" for s in changed)
if errors:
result += f"\n\nErrors ({len(errors)}):\n" + "\n".join(f"{e}" for e in errors)
return result
@mcp.tool()
def haproxy_stats() -> str:
"""Get HAProxy status and statistics.
@@ -1194,6 +1271,27 @@ def haproxy_health() -> str:
result["components"]["haproxy"]["error"] = str(e)
result["status"] = "degraded"
# Check container status
try:
container_result = subprocess.run(
["podman", "inspect", "--format", "{{.State.Status}}", HAPROXY_CONTAINER],
capture_output=True, text=True, timeout=5
)
if container_result.returncode == 0:
container_status = container_result.stdout.strip()
result["components"]["container"] = {
"status": "ok" if container_status == "running" else container_status,
"state": container_status
}
else:
result["components"]["container"] = {"status": "error", "error": container_result.stderr.strip()}
result["status"] = "unhealthy"
except subprocess.TimeoutExpired:
result["components"]["container"] = {"status": "timeout"}
result["status"] = "unhealthy"
except Exception as e:
result["components"]["container"] = {"status": "error", "error": str(e)}
# Check configuration files
files_ok = True
file_status: Dict[str, str] = {}