""" DockerClient — paramiko SSH (遠端 docker / traefik 查詢) 所有容器都在 10.1.0.254,透過 SSH 操作。 3-state 回傳: None=未設定(灰), True=正常(綠), False=異常(紅) """ import logging from typing import Optional import httpx from app.core.config import settings logger = logging.getLogger(__name__) class DockerClient: def _ssh(self): """建立 SSH 連線到 10.1.0.254""" 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, ) return client def check_traefik_domain(self, domain: str) -> Optional[bool]: """ None = domain 在 Traefik 沒有路由設定(灰) True = 路由存在且服務存活(綠) False = 路由存在但服務不通(紅) """ try: resp = httpx.get(f"http://{settings.DOCKER_SSH_HOST}:8080/api/http/routers", timeout=5.0) if resp.status_code != 200: return False routers = resp.json() route_found = any(domain in str(r.get("rule", "")) for r in routers) if not route_found: return None # Route exists — probe the service try: probe = httpx.get( f"https://{domain}", timeout=5.0, follow_redirects=True, ) return probe.status_code < 500 except Exception: return False except Exception as e: logger.warning(f"Traefik check failed for {domain}: {e}") return False def check_container_ssh(self, container_name: str) -> Optional[bool]: """ SSH 到 10.1.0.254 查詢容器狀態。 None = 容器不存在(未部署) True = 容器正在執行 False = 容器存在但未執行(exited/paused) """ try: client = self._ssh() _, stdout, _ = client.exec_command( f"docker inspect --format='{{{{.State.Status}}}}' {container_name} 2>/dev/null" ) output = stdout.read().decode().strip() client.close() if not output: return None return output == "running" except Exception as e: logger.error(f"SSH container check failed for {container_name}: {e}") return False def ssh_compose_up(self, tenant_code: str) -> bool: """SSH 到 10.1.0.254 執行 docker compose up -d""" try: client = self._ssh() deploy_dir = f"{settings.TENANT_DEPLOY_BASE}/{tenant_code}" _, stdout, _ = 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 def get_oo_disk_usage_gb(self, container_name: str) -> Optional[float]: """取得 OO 容器磁碟使用量(GB),容器不存在回傳 None""" try: client = self._ssh() _, stdout, _ = client.exec_command( f"docker exec {container_name} df -B1 / 2>/dev/null | awk 'NR==2 {{print $3}}'" ) output = stdout.read().decode().strip() client.close() if output.isdigit(): return round(int(output) / (1024 ** 3), 3) return None except Exception as e: logger.warning(f"OO disk usage check failed for {container_name}: {e}") return None