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:
124
mcp/server.py
124
mcp/server.py
@@ -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] = {}
|
||||
|
||||
Reference in New Issue
Block a user