"""Read-only server discovery and conservative starter recommendations.""" import base64 import hashlib import math import socket from .docker import DockerEngine, DockerError def host_fingerprint(host: str, port: int) -> str: import paramiko transport = None try: with socket.create_connection((host, port), timeout=10) as connection: transport = paramiko.Transport(connection) transport.start_client(timeout=10) key = transport.get_remote_server_key() return "SHA256:" + base64.b64encode(hashlib.sha256(key.asbytes()).digest()).decode().rstrip("=") except Exception as exc: raise DockerError(f"读取服务器指纹失败({type(exc).__name__})") from None finally: if transport is not None: transport.close() def recommend(cpus, memory_mb, reserved_cpu=0, reserved_memory_mb=0, connection_budget=64): # Reserve at least half the host for the main app, DB and other services. cpu_budget = max(0, min(cpus / 2, cpus - reserved_cpu - 1)) memory_budget = max(0, min(memory_mb // 2, memory_mb - reserved_memory_mb - 2048)) cpu = 2 if cpu_budget >= 2 else 1 memory = 2048 count = max(0, min(4, math.floor(cpu_budget / cpu), memory_budget // memory, connection_budget // 12)) return {"desired": int(count), "cpu": cpu, "memory_mb": memory, "total_cpu": max(cpu, count * cpu), "total_memory_mb": max(memory, count * memory), "concurrency": 4, "database_pool_size": 4, "postgres_pool_size": 4} def discover(settings, instance): engine = DockerEngine(settings) info = engine.request("GET", "/info") containers = engine.request("GET", "/containers/json") cpu_reserved = 0 memory_reserved = 0 templates = [] for item in containers: if item.get("Labels", {}).get("deerflow.worker-pool") == instance: continue detail = engine.request("GET", f"/containers/{item['Id']}/json") limits = detail.get("HostConfig", {}) cpu_reserved += limits.get("NanoCpus", 0) / 1_000_000_000 memory_reserved += limits.get("Memory", 0) // (1024 ** 2) if item.get("Labels", {}).get("deerflow.worker-role") == "online": continue networks = [name for name in detail.get("NetworkSettings", {}).get("Networks", {}) if name not in {"host", "none", "bridge"}] templates.append({"name": detail.get("Name", "").lstrip("/"), "networks": networks}) cpus = int(info.get("NCPU", 0)) memory_mb = int(info.get("MemTotal", 0)) // (1024 ** 2) return {"cpus": cpus, "memory_mb": memory_mb, "containers": templates, "recommendation": recommend(cpus, memory_mb, cpu_reserved, memory_reserved, settings.database_connection_budget), "note": "按硬件总量和其他容器已配置的资源上限估算,至少为主服务、数据库及系统保留一半资源;不是实时空闲内存。建议先用最多 4 个节点,再按实际负载调整。"}