feat: Add SSH remote execution for HAProxy on remote host

MCP server can now manage HAProxy running on a remote host via SSH.
When SSH_HOST env var is set, all file I/O and subprocess commands
(podman, acme.sh, openssl) are routed through SSH instead of local exec.

- Add ssh_ops.py module with remote_exec, run_command, file I/O helpers
- Modify file_ops.py to support remote reads/writes via SSH
- Update all tools (domains, certificates, health, configuration) for SSH
- Fix domains.py: replace direct fcntl usage with file_lock context manager
- Add openssh-client to Docker image for SSH connectivity
- Update k8s deployment with SSH env vars and SSH key secret mount

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
kappa
2026-02-07 22:56:54 +09:00
parent ca3975c94c
commit e40d69a1b1
10 changed files with 416 additions and 325 deletions

View File

@@ -1,7 +1,6 @@
"""Health check tools for HAProxy MCP Server."""
import json
import os
import subprocess
import time
from typing import Annotated, Any
@@ -18,6 +17,8 @@ from ..validation import validate_domain, validate_backend_name
from ..haproxy_client import haproxy_cmd
from ..file_ops import get_backend_and_prefix
from ..utils import parse_stat_csv, parse_servers_state
from ..ssh_ops import run_command, remote_file_exists
from ..config import REMOTE_MODE
def register_health_tools(mcp):
@@ -65,9 +66,9 @@ def register_health_tools(mcp):
# Check container status
try:
container_result = subprocess.run(
container_result = run_command(
["podman", "inspect", "--format", "{{.State.Status}}", HAPROXY_CONTAINER],
capture_output=True, text=True, timeout=5
timeout=5,
)
if container_result.returncode == 0:
container_status = container_result.stdout.strip()
@@ -88,7 +89,8 @@ def register_health_tools(mcp):
files_ok = True
file_status: dict[str, str] = {}
for name, path in [("map_file", MAP_FILE), ("servers_file", SERVERS_FILE)]:
if os.path.exists(path):
exists = remote_file_exists(path) if REMOTE_MODE else __import__('os').path.exists(path)
if exists:
file_status[name] = "ok"
else:
file_status[name] = "missing"