Source snapshot: 97d7f0fae96dc04b7ddad56fc1db6a108ed662cc [SEC-CLEAN] · pre-push-clean v1.0 · 109处敏感信息已自动转乱码
185 lines
5.9 KiB
Markdown
185 lines
5.9 KiB
Markdown
# GHCS v0.2 Step2 · deploy_router.py · 一键部署代码
|
||
|
||
## 文件1 · `backend/deploy_router.py`(新建)
|
||
|
||
```python
|
||
"""
|
||
GHCS v0.2 - deploy router
|
||
一键部署:git pull → install deps → restart service
|
||
"""
|
||
|
||
import os
|
||
import logging
|
||
from datetime import datetime
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from auth import verify_token
|
||
|
||
logger = logging.getLogger("ghcs.deploy")
|
||
router = APIRouter(prefix="/api/deploy", tags=["deploy"])
|
||
|
||
SSH_KEY_PATH = os.getenv("GHCS_SSH_KEY", os.path.expanduser("~/.ssh/id_rsa"))
|
||
|
||
GHCS_SERVER = {
|
||
"host": "43.139.207.172",
|
||
"port": 22,
|
||
"username": "root",
|
||
"key_filename": SSH_KEY_PATH,
|
||
}
|
||
|
||
# 可部署的服务白名单
|
||
DEPLOY_WHITELIST = {
|
||
"console-api": {
|
||
"name": "GHCS console-api",
|
||
"server": GHCS_SERVER,
|
||
"repo_dir": "/root/guanghu-dev/ghcs-mono",
|
||
"steps": [
|
||
"cd /root/guanghu-dev/ghcs-mono && git pull origin main",
|
||
"cd /root/guanghu-dev/ghcs-mono/backend && source venv/bin/activate && pip install -r requirements.txt -q",
|
||
"systemctl restart console-api",
|
||
],
|
||
"verify_cmd": "systemctl is-active console-api",
|
||
},
|
||
}
|
||
|
||
def _ssh_exec(server_config, command, timeout=60):
|
||
"""SSH远程执行命令"""
|
||
import paramiko
|
||
client = paramiko.SSHClient()
|
||
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||
try:
|
||
client.connect(
|
||
hostname=server_config["host"],
|
||
port=server_config["port"],
|
||
username=server_config["username"],
|
||
key_filename=server_config["key_filename"],
|
||
timeout=10,
|
||
)
|
||
stdin, stdout, stderr = client.exec_command(command, timeout=timeout)
|
||
exit_code = stdout.channel.recv_exit_status()
|
||
return {
|
||
"stdout": stdout.read().decode("utf-8").strip(),
|
||
"stderr": stderr.read().decode("utf-8").strip(),
|
||
"exit_code": exit_code,
|
||
}
|
||
except Exception as e:
|
||
raise HTTPException(status_code=500, detail=f"SSH error: {str(e)}")
|
||
finally:
|
||
client.close()
|
||
|
||
@router.post("/{service_key}")
|
||
async def deploy_service(service_key: str, username: str = Depends(verify_token)):
|
||
"""一键部署:git pull → install → restart"""
|
||
if service_key not in DEPLOY_WHITELIST:
|
||
raise HTTPException(status_code=404, detail=f"unknown service: {service_key}")
|
||
|
||
svc = DEPLOY_WHITELIST[service_key]
|
||
logger.info(f"[DEPLOY] user={username} service={service_key} time={datetime.now().isoformat()}")
|
||
|
||
results = []
|
||
for i, step in enumerate(svc["steps"]):
|
||
logger.info(f"[DEPLOY STEP {i+1}] {step[:80]}")
|
||
result = _ssh_exec(svc["server"], step, timeout=120)
|
||
results.append({
|
||
"step": i + 1,
|
||
"command": step[:80],
|
||
"stdout": result["stdout"][-500:] if result["stdout"] else "",
|
||
"stderr": result["stderr"][-500:] if result["stderr"] else "",
|
||
"exit_code": result["exit_code"],
|
||
})
|
||
if result["exit_code"] != 0:
|
||
logger.error(f"[DEPLOY FAILED] step={i+1} stderr={result['stderr']}")
|
||
return {
|
||
"service": service_key,
|
||
"success": False,
|
||
"failed_step": i + 1,
|
||
"steps": results,
|
||
"timestamp": datetime.now().isoformat(),
|
||
}
|
||
|
||
# 验证服务状态
|
||
verify = _ssh_exec(svc["server"], svc["verify_cmd"])
|
||
status = verify["stdout"]
|
||
|
||
logger.info(f"[DEPLOY OK] service={service_key} status={status}")
|
||
return {
|
||
"service": service_key,
|
||
"success": True,
|
||
"status_after": status,
|
||
"steps": results,
|
||
"timestamp": datetime.now().isoformat(),
|
||
}
|
||
|
||
@router.get("/{service_key}/check")
|
||
async def check_deploy_status(service_key: str, username: str = Depends(verify_token)):
|
||
"""检查服务器代码是否落后于 Gitea"""
|
||
if service_key not in DEPLOY_WHITELIST:
|
||
raise HTTPException(status_code=404, detail=f"unknown service: {service_key}")
|
||
|
||
svc = DEPLOY_WHITELIST[service_key]
|
||
repo_dir = svc["repo_dir"]
|
||
|
||
result = _ssh_exec(
|
||
svc["server"],
|
||
f"cd {repo_dir} && git fetch origin main -q 2>&1 && echo 'LOCAL:' && git log --oneline -1 HEAD && echo 'REMOTE:' && git log --oneline -1 origin/main",
|
||
)
|
||
lines = result["stdout"].split("\n")
|
||
local_commit = ""
|
||
remote_commit = ""
|
||
for i, line in enumerate(lines):
|
||
if line.startswith("LOCAL:") and i + 1 < len(lines):
|
||
local_commit = lines[i + 1]
|
||
if line.startswith("REMOTE:") and i + 1 < len(lines):
|
||
remote_commit = lines[i + 1]
|
||
|
||
return {
|
||
"service": service_key,
|
||
"server_commit": local_commit,
|
||
"gitea_commit": remote_commit,
|
||
"needs_deploy": local_commit != remote_commit,
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 文件2 · `backend/main.py`(改动两处)
|
||
|
||
### 改动 A · 顶部加一行 import
|
||
|
||
在 `from servers_router import router as servers_router` 后面加一行:
|
||
|
||
```python
|
||
from deploy_router import router as deploy_router
|
||
```
|
||
|
||
### 改动 B · 挂载 router
|
||
|
||
在 `app.include_router(servers_router)` 后面加一行:
|
||
|
||
```python
|
||
app.include_router(deploy_router)
|
||
```
|
||
|
||
---
|
||
|
||
## 接口说明
|
||
|
||
| 接口 | 方法 | 作用 |
|
||
| --- | --- | --- |
|
||
| `POST /api/deploy/console-api` | POST | 一键部署 GHCS · git pull → pip install → restart |
|
||
| `GET /api/deploy/console-api/check` | GET | 检查服务器代码是否落后于 Gitea |
|
||
|
||
---
|
||
|
||
## 部署流程
|
||
|
||
1. Mac 本地改代码 → `git push origin main`
|
||
2. 调 `POST /api/deploy/console-api`
|
||
3. 服务器自动执行:`git pull` → `pip install` → `systemctl restart`
|
||
4. 返回每一步的执行结果 + 最终服务状态
|
||
|
||
---
|
||
|
||
## ⚠️ 注意
|
||
|
||
- venv 路径假设在 `/root/guanghu-dev/ghcs-mono/backend/venv/`,如果实际路径不同需要改 steps 里的 source 路径
|
||
- 当前只支持部署 console-api(GHCS 自身),后续可以扩展 DEPLOY_WHITELIST 加入其他服务 |