""" CertCenter 部署验证脚本 用法: python -m tests.test_setup """ import sys import os import json import requests from pathlib import Path # 添加项目根目录到 path sys.path.insert(0, str(Path(__file__).parent.parent)) PASS = "✅" FAIL = "❌" SKIP = "⏭️" results = [] def check(name, func): """执行检查并记录结果""" try: ok, msg = func() status = PASS if ok else FAIL results.append((name, status, msg)) print(f" {status} {name}: {msg}") except Exception as e: results.append((name, FAIL, str(e))) print(f" {FAIL} {name}: {e}") def test_python_deps(): """检查 Python 依赖""" deps = ["fastapi", "uvicorn", "sqlalchemy", "acme", "cryptography", "jinja2"] missing = [] for dep in deps: try: __import__(dep) except ImportError: missing.append(dep) if missing: return False, f"缺少依赖: {', '.join(missing)},执行 pip install -r requirements.txt" return True, "所有依赖已安装" def test_certbot(): """检查 certbot 是否可用""" import subprocess try: result = subprocess.run(["certbot", "--version"], capture_output=True, text=True, timeout=5) if result.returncode == 0: return True, result.stdout.strip() return False, "certbot 命令不可用" except FileNotFoundError: return False, "certbot 未安装,执行 pip install certbot" def test_certbot_dns_plugin(): """检查 certbot DNS 插件""" try: import certbot_dns_alicloud return True, "certbot-dns-alicloud 已安装" except ImportError: try: import certbot_dns_cloudflare return True, "certbot-dns-cloudflare 已安装" except ImportError: return False, "未检测到 DNS 插件,执行 pip install certbot-dns-alicloud" def test_env_file(): """检查 .env 文件""" env_path = Path(__file__).parent.parent / ".env" if not env_path.exists(): return False, ".env 文件不存在,执行 cp .env .env" return True, ".env 文件存在" def test_database(): """检查数据库连接""" from backend.config import get_settings settings = get_settings() db_path = settings.database_url.replace("sqlite+aiosqlite:///", "") if "sqlite" in settings.database_url: return True, f"SQLite 数据库: {db_path}" return True, f"数据库: {settings.database_url}" def test_frontend_dist(): """检查前端构建产物""" dist = Path(__file__).parent.parent / "frontend" / "dist" if dist.exists() and (dist / "index.html").exists(): return True, f"前端已构建: {dist}" return False, f"前端未构建,执行 cd frontend && npm install && npm run build" def test_cert_dir(): """检查证书存储目录""" from backend.config import get_settings settings = get_settings() cert_dir = Path(settings.cert_dir) if cert_dir.exists(): return True, f"证书目录存在: {cert_dir}" try: cert_dir.mkdir(parents=True, exist_ok=True) return True, f"证书目录已创建: {cert_dir}" except Exception as e: return False, f"无法创建证书目录: {e}" def test_base_url(): """检查 BASE_URL 配置""" from backend.config import get_settings settings = get_settings() if "example.com" in settings.base_url: return False, f"BASE_URL 仍为默认值: {settings.base_url},请修改 .env" return True, f"BASE_URL: {settings.base_url}" def test_letsencrypt_staging(): """测试 Let's Encrypt 测试环境连通性""" try: resp = requests.get( "https://acme-staging-v02.api.letsencrypt.org/directory", timeout=10, ) if resp.status_code == 200: data = resp.json() return True, f"Staging 环境可达, endpoints: {len(data)} 个" return False, f"HTTP {resp.status_code}" except Exception as e: return False, f"无法连接 Let's Encrypt Staging: {e}" def test_letsencrypt_production(): """测试 Let's Encrypt 生产环境连通性""" try: resp = requests.get( "https://acme-v02.api.letsencrypt.org/directory", timeout=10, ) if resp.status_code == 200: return True, "生产环境可达" return False, f"HTTP {resp.status_code}" except Exception as e: return False, f"无法连接 Let's Encrypt: {e}" def test_dns_credentials(): """检查 DNS 凭据是否配置""" from backend.database import engine, async_session from backend.models import AcmeConfig from sqlalchemy import select import asyncio async def _check(): async with async_session() as session: result = await session.execute(select(AcmeConfig).where(AcmeConfig.id == 1)) config = result.scalar_one_or_none() if not config: return False, "ACME 未配置,请在 WebUI 中配置" if not config.email: return False, "邮箱未配置" creds = json.loads(config.dns_credentials or "{}") if not creds.get("access_key") and not creds.get("api_token"): return False, "DNS 凭据未配置" return True, f"DNS 提商: {config.dns_provider}, 邮箱: {config.email}" return asyncio.run(_check()) def test_api_server(): """测试 FastAPI 服务是否能启动""" import subprocess import time try: proc = subprocess.Popen( [sys.executable, "-m", "backend.main"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=str(Path(__file__).parent.parent), ) time.sleep(3) # 尝试访问 try: resp = requests.get("http://127.0.0.1:8000/admin/api/stats", timeout=5) if resp.status_code == 200: proc.terminate() return True, "服务启动成功,API 正常响应" proc.terminate() return False, f"服务启动但 API 返回 {resp.status_code}" except requests.ConnectionError: proc.terminate() return False, "服务启动失败,端口 8000 无响应" except Exception as e: return False, f"启动测试失败: {e}" def main(): print("\n" + "=" * 50) print(" CertCenter 部署验证") print("=" * 50) print("\n📦 环境检查:") check("Python 依赖", test_python_deps) check("Certbot", test_certbot) check("DNS 插件", test_certbot_dns_plugin) check(".env 配置", test_env_file) check("数据库", test_database) check("前端构建", test_frontend_dist) check("证书目录", test_cert_dir) check("BASE_URL", test_base_url) print("\n🌐 网络检查:") check("Let's Encrypt Staging", test_letsencrypt_staging) check("Let's Encrypt Production", test_letsencrypt_production) print("\n⚙️ 服务检查:") check("FastAPI 服务", test_api_server) # 汇总 print("\n" + "=" * 50) passed = sum(1 for _, s, _ in results if s == PASS) failed = sum(1 for _, s, _ in results if s == FAIL) print(f" 结果: {passed} 通过, {failed} 失败") if failed: print("\n 请修复以上 ❌ 标记的问题后重试。") print(" 建议先使用 Let's Encrypt Staging 环境测试。") else: print("\n 所有检查通过!可以开始使用 CertCenter。") print(" 建议首次测试使用 Staging 环境,确认无误后再切换到生产环境。") print("=" * 50 + "\n") return 1 if failed else 0 if __name__ == "__main__": sys.exit(main())