""" DockerClient — docker-py (本機 Docker socket) + paramiko SSH (遠端 docker compose) 管理租戶的 NC / OO 容器。 """ import logging from typing import Optional import httpx from app.core.config import settings logger = logging.getLogger(__name__) class DockerClient: def __init__(self): self._docker = None def _get_docker(self): if self._docker is None: import docker self._docker = docker.from_env() return self._docker def check_traefik_route(self, domain: str) -> bool: """ Traefik API: GET http://localhost:8080/api/http/routers 驗證 routers 中包含 domain,且 routers 數量 > 0 """ try: resp = httpx.get("http://localhost:8080/api/overview", timeout=5.0) if resp.status_code != 200: return False data = resp.json() # Verify actual routes exist (functional check) http_count = data.get("http", {}).get("routers", {}).get("total", 0) if http_count == 0: return False # Check domain-specific router routers_resp = httpx.get("http://localhost:8080/api/http/routers", timeout=5.0) if routers_resp.status_code != 200: return False routers = routers_resp.json() return any(domain in str(r.get("rule", "")) for r in routers) except Exception as e: logger.warning(f"Traefik check failed for {domain}: {e}") return False def ensure_container_running(self, container_name: str, tenant_code: str, realm: str) -> bool: """Check container status; start if exited; deploy via SSH if not found.""" try: docker_client = self._get_docker() container = docker_client.containers.get(container_name) if container.status == "running": return True elif container.status == "exited": container.start() container.reload() return container.status == "running" except Exception as e: if "Not Found" in str(e) or "404" in str(e): return self._ssh_compose_up(tenant_code, realm) logger.error(f"Docker check failed for {container_name}: {e}") return False return False def _ssh_compose_up(self, tenant_code: str, realm: str) -> bool: """SSH into 10.1.0.254 and run docker compose up -d""" try: import paramiko client = paramiko.SSHClient() client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) client.connect( settings.DOCKER_SSH_HOST, username=settings.DOCKER_SSH_USER, timeout=15, ) deploy_dir = f"{settings.TENANT_DEPLOY_BASE}/{tenant_code}" stdin, stdout, stderr = client.exec_command( f"cd {deploy_dir} && docker compose up -d 2>&1" ) exit_status = stdout.channel.recv_exit_status() client.close() return exit_status == 0 except Exception as e: logger.error(f"SSH compose up failed for {tenant_code}: {e}") return False