Files
recipe-maintainer/lib/ssh.py
autonomic-bot f283a371bb recipe-maintainer: public snapshot (secrets + deployment plans removed, single commit)
Sanitized single-commit public mirror of recipe-maintainer.
- Removed test-ssh/.testenv (live creds); added test-ssh/.testenv.example placeholders.
- Removed plans/ and planned-updates/ (deployment-planning docs) so no client/
  deployment domains appear in the public repo.
- All other secret stores were already gitignored.
- docs.coopcloud.tech retained as a submodule (public upstream).
2026-06-16 20:18:24 +00:00

167 lines
5.5 KiB
Python

"""SSH command runner for direct server operations.
Used for docker service ls, reading secrets from containers,
checking service logs, and other operations that require
server-side access bypassing abra.
"""
from lib.abra import run, AbraResult
def ssh_run(server: str, command: str, *,
timeout: int = 30) -> AbraResult:
"""Run a command on a remote server via SSH."""
return run(f'ssh {server} "{command}"', check=False, timeout=timeout)
def docker_service_ls(server: str, stack_prefix: str) -> list[dict]:
"""List Docker services for a stack, parsed into dicts.
Returns list of {"name": str, "replicas": str, "image": str}.
"""
result = ssh_run(
server,
f"docker service ls --filter 'name={stack_prefix}_'"
f" --format '{{{{.Name}}}}|{{{{.Replicas}}}}|{{{{.Image}}}}'",
timeout=30,
)
if not result.ok or not result.stdout.strip():
return []
services = []
for line in result.stdout.strip().split("\n"):
line = line.strip()
if not line:
continue
parts = line.split("|")
if len(parts) >= 3:
services.append({
"name": parts[0],
"replicas": parts[1],
"image": parts[2],
})
return services
def all_replicas_ready(server: str, stack_prefix: str) -> bool:
"""Check if all Docker services have their desired replica count.
Returns True if every service shows N/N replicas (e.g. "1/1").
Returns False if any service is not ready or no services found.
"""
result = ssh_run(
server,
f"docker service ls --filter 'name={stack_prefix}_'"
f" --format '{{{{.Replicas}}}}'",
timeout=30,
)
if not result.ok:
return False
lines = [l.strip() for l in result.stdout.strip().split("\n") if l.strip()]
if not lines:
return False
for replicas in lines:
parts = replicas.split("/")
if len(parts) != 2:
return False
if parts[0] != parts[1]:
return False
return True
def read_container_secrets(server: str, stack_prefix: str) -> dict[str, str]:
"""Read all Docker secrets from running containers of a stack.
SSHes into the server, finds all containers for the stack,
reads /run/secrets/ from each container, returns {name: value}.
"""
cmd = (
f"containers=\\$(docker ps -q --filter 'name={stack_prefix}_' 2>/dev/null); "
f"if [ -z \\\"\\$containers\\\" ]; then exit 0; fi; "
f"for cid in \\$containers; do "
f" files=\\$(docker exec \\$cid ls /run/secrets/ 2>/dev/null) || continue; "
f" for f in \\$files; do "
f" val=\\$(docker exec \\$cid cat /run/secrets/\\$f 2>/dev/null) || continue; "
f" echo \\\"\\$f=\\$val\\\"; "
f" done; "
f"done"
)
result = ssh_run(server, cmd, timeout=60)
if not result.ok or not result.stdout.strip():
return {}
secrets = {}
for line in result.stdout.strip().split("\n"):
line = line.strip()
if "=" in line:
name, value = line.split("=", 1)
if name not in secrets: # first occurrence wins
secrets[name] = value
return secrets
def docker_service_logs(server: str, stack_name: str, service: str, *,
since: str = "5m", tail: int = 100) -> str:
"""Get Docker service logs via SSH (avoids abra app logs which hangs)."""
result = ssh_run(
server,
f"docker service logs {stack_name}_{service} --since {since} --tail {tail} 2>&1",
timeout=30,
)
return result.stdout if result.ok else ""
def docker_secret_ls(server: str, stack_prefix: str) -> list[str]:
"""List Docker secret names matching a stack prefix."""
result = ssh_run(
server,
f"docker secret ls --filter 'name={stack_prefix}_' --format '{{{{.Name}}}}'",
timeout=15,
)
if not result.ok or not result.stdout.strip():
return []
return [l.strip() for l in result.stdout.strip().split("\n") if l.strip()]
def docker_volume_ls(server: str, stack_prefix: str) -> list[str]:
"""List Docker volume names for a stack.
Returns a list of volume names. Raises RuntimeError if the SSH
command fails (a failed command is a real error, not "no volumes").
"""
result = ssh_run(
server,
f"docker volume ls --filter 'name={stack_prefix}_' --format '{{{{.Name}}}}'",
timeout=15,
)
if not result.ok:
raise RuntimeError(
f"Failed to list volumes on {server}: {result.stderr.strip()}"
)
if not result.stdout.strip():
return []
return [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
def docker_ps(server: str, *, filter_name: str | None = None) -> list[dict]:
"""List Docker containers on the server."""
cmd = "docker ps --format '{{.Names}}|{{.Status}}|{{.Image}}'"
if filter_name:
cmd = f"docker ps --filter 'name={filter_name}' --format '{{{{.Names}}}}|{{{{.Status}}}}|{{{{.Image}}}}'"
result = ssh_run(server, cmd, timeout=30)
if not result.ok or not result.stdout.strip():
return []
containers = []
for line in result.stdout.strip().split("\n"):
parts = line.strip().split("|")
if len(parts) >= 3:
containers.append({
"name": parts[0],
"status": parts[1],
"image": parts[2],
})
return containers