bug修复
This commit is contained in:
@@ -12,7 +12,7 @@ import logging
|
|||||||
import subprocess
|
import subprocess
|
||||||
import tempfile
|
import tempfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
@@ -326,7 +326,7 @@ class AcmeService:
|
|||||||
try:
|
try:
|
||||||
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
|
cert = x509.load_pem_x509_certificate(cert_path.read_bytes())
|
||||||
not_after = cert.not_valid_after_utc
|
not_after = cert.not_valid_after_utc
|
||||||
days_left = (not_after - datetime.utcnow()).days
|
days_left = (not_after - datetime.now(timezone.utc)).days
|
||||||
return days_left <= self.config.renew_days, days_left
|
return days_left <= self.config.renew_days, days_left
|
||||||
except Exception:
|
except Exception:
|
||||||
return True, 0
|
return True, 0
|
||||||
|
|||||||
@@ -22,6 +22,35 @@ async def get_db():
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
# ────────────── CRUD 工具函数 ──────────────
|
||||||
|
|
||||||
|
async def db_add(db: AsyncSession, obj, *, refresh: bool = False):
|
||||||
|
"""创建记录并提交。refresh=True 会在提交后刷新对象(获取生成的 id 等)。"""
|
||||||
|
db.add(obj)
|
||||||
|
await db.flush()
|
||||||
|
if refresh:
|
||||||
|
await db.refresh(obj)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def db_update(db: AsyncSession, obj, data: dict):
|
||||||
|
"""批量更新字段并提交。data 一般来自 model_dump(exclude_unset=True)。"""
|
||||||
|
for key, value in data.items():
|
||||||
|
setattr(obj, key, value)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def db_delete(db: AsyncSession, obj):
|
||||||
|
"""删除记录并提交。"""
|
||||||
|
await db.delete(obj)
|
||||||
|
await db.commit()
|
||||||
|
|
||||||
|
|
||||||
|
async def db_flush(db: AsyncSession):
|
||||||
|
"""将待写操作刷到数据库(不提交事务),用于需要获取生成 ID 等中间状态的场景。"""
|
||||||
|
await db.flush()
|
||||||
|
|
||||||
|
|
||||||
async def init_db():
|
async def init_db():
|
||||||
async with engine.begin() as conn:
|
async with engine.begin() as conn:
|
||||||
# 迁移旧表:acme_config → acme_configs
|
# 迁移旧表:acme_config → acme_configs
|
||||||
|
|||||||
+15
-23
@@ -14,7 +14,7 @@ import time
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db, db_add, db_update, db_delete, db_flush
|
||||||
from backend.models import Server, Domain, DeployLog, AcmeConfig, AcmeLog
|
from backend.models import Server, Domain, DeployLog, AcmeConfig, AcmeLog
|
||||||
from backend.config import get_settings
|
from backend.config import get_settings
|
||||||
|
|
||||||
@@ -212,9 +212,7 @@ async def get_server(server_id: int, db: AsyncSession = Depends(get_db), _: bool
|
|||||||
@router.post("/servers", status_code=201)
|
@router.post("/servers", status_code=201)
|
||||||
async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
async def create_server(data: ServerCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||||||
server = Server(**data.model_dump())
|
server = Server(**data.model_dump())
|
||||||
db.add(server)
|
await db_add(db, server, refresh=True)
|
||||||
await db.flush()
|
|
||||||
await db.refresh(server)
|
|
||||||
return {"id": server.id, "name": server.name}
|
return {"id": server.id, "name": server.name}
|
||||||
|
|
||||||
|
|
||||||
@@ -224,8 +222,7 @@ async def update_server(server_id: int, data: ServerUpdate, db: AsyncSession = D
|
|||||||
server = result.scalar_one_or_none()
|
server = result.scalar_one_or_none()
|
||||||
if not server:
|
if not server:
|
||||||
raise HTTPException(404, "Server not found")
|
raise HTTPException(404, "Server not found")
|
||||||
for key, value in data.model_dump(exclude_unset=True).items():
|
await db_update(db, server, data.model_dump(exclude_unset=True))
|
||||||
setattr(server, key, value)
|
|
||||||
return {"id": server.id}
|
return {"id": server.id}
|
||||||
|
|
||||||
|
|
||||||
@@ -235,7 +232,7 @@ async def delete_server(server_id: int, db: AsyncSession = Depends(get_db), _: b
|
|||||||
server = result.scalar_one_or_none()
|
server = result.scalar_one_or_none()
|
||||||
if not server:
|
if not server:
|
||||||
raise HTTPException(404, "Server not found")
|
raise HTTPException(404, "Server not found")
|
||||||
await db.delete(server)
|
await db_delete(db, server)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -311,9 +308,7 @@ async def create_domain(data: DomainCreate, db: AsyncSession = Depends(get_db),
|
|||||||
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
|
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
|
||||||
|
|
||||||
domain = Domain(**dump)
|
domain = Domain(**dump)
|
||||||
db.add(domain)
|
await db_add(db, domain, refresh=True)
|
||||||
await db.flush()
|
|
||||||
await db.refresh(domain)
|
|
||||||
return {"id": domain.id, "domain": domain.domain}
|
return {"id": domain.id, "domain": domain.domain}
|
||||||
|
|
||||||
|
|
||||||
@@ -343,8 +338,7 @@ async def update_domain(domain_id: int, data: DomainUpdate, db: AsyncSession = D
|
|||||||
if dup:
|
if dup:
|
||||||
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
|
raise HTTPException(409, "该域名在此服务器下已存在相同 ACME 配置的记录")
|
||||||
|
|
||||||
for key, value in update_fields.items():
|
await db_update(db, domain, update_fields)
|
||||||
setattr(domain, key, value)
|
|
||||||
return {"id": domain.id}
|
return {"id": domain.id}
|
||||||
|
|
||||||
|
|
||||||
@@ -354,7 +348,7 @@ async def delete_domain(domain_id: int, db: AsyncSession = Depends(get_db), _: b
|
|||||||
domain = result.scalar_one_or_none()
|
domain = result.scalar_one_or_none()
|
||||||
if not domain:
|
if not domain:
|
||||||
raise HTTPException(404, "Domain not found")
|
raise HTTPException(404, "Domain not found")
|
||||||
await db.delete(domain)
|
await db_delete(db, domain)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -448,9 +442,7 @@ async def get_acme_config(config_id: int, db: AsyncSession = Depends(get_db), _:
|
|||||||
@router.post("/acme/configs", status_code=201)
|
@router.post("/acme/configs", status_code=201)
|
||||||
async def create_acme_config(data: AcmeConfigCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
async def create_acme_config(data: AcmeConfigCreate, db: AsyncSession = Depends(get_db), _: bool = Depends(require_auth)):
|
||||||
config = AcmeConfig(**data.model_dump())
|
config = AcmeConfig(**data.model_dump())
|
||||||
db.add(config)
|
await db_add(db, config, refresh=True)
|
||||||
await db.flush()
|
|
||||||
await db.refresh(config)
|
|
||||||
return {"id": config.id, "name": config.name}
|
return {"id": config.id, "name": config.name}
|
||||||
|
|
||||||
|
|
||||||
@@ -460,8 +452,7 @@ async def update_acme_config(config_id: int, data: AcmeConfigUpdate, db: AsyncSe
|
|||||||
config = result.scalar_one_or_none()
|
config = result.scalar_one_or_none()
|
||||||
if not config:
|
if not config:
|
||||||
raise HTTPException(404, "ACME config not found")
|
raise HTTPException(404, "ACME config not found")
|
||||||
for key, value in data.model_dump(exclude_unset=True).items():
|
await db_update(db, config, data.model_dump(exclude_unset=True))
|
||||||
setattr(config, key, value)
|
|
||||||
return {"id": config.id}
|
return {"id": config.id}
|
||||||
|
|
||||||
|
|
||||||
@@ -475,7 +466,7 @@ async def delete_acme_config(config_id: int, db: AsyncSession = Depends(get_db),
|
|||||||
result = await db.execute(select(Domain).where(Domain.acme_config_id == config_id))
|
result = await db.execute(select(Domain).where(Domain.acme_config_id == config_id))
|
||||||
for d in result.scalars().all():
|
for d in result.scalars().all():
|
||||||
d.acme_config_id = None
|
d.acme_config_id = None
|
||||||
await db.delete(config)
|
await db_delete(db, config)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
@@ -517,7 +508,7 @@ async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool
|
|||||||
# 记录开始
|
# 记录开始
|
||||||
log = AcmeLog(domain_id=domain_id, action="issue", status="pending", message=f"Issuing certificate for {domain.domain}")
|
log = AcmeLog(domain_id=domain_id, action="issue", status="pending", message=f"Issuing certificate for {domain.domain}")
|
||||||
db.add(log)
|
db.add(log)
|
||||||
await db.flush()
|
await db_flush(db)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
success, msg, cert_info, raw_output = service.issue_certificate(domain.domain)
|
success, msg, cert_info, raw_output = service.issue_certificate(domain.domain)
|
||||||
@@ -537,12 +528,12 @@ async def issue_cert(domain_id: int, db: AsyncSession = Depends(get_db), _: bool
|
|||||||
domain.version = str(int(domain.version or "0") + 1)
|
domain.version = str(int(domain.version or "0") + 1)
|
||||||
logger.warning(f"无法获取证书信息: {domain.domain}")
|
logger.warning(f"无法获取证书信息: {domain.domain}")
|
||||||
|
|
||||||
await db.flush()
|
await db.commit()
|
||||||
return {"success": success, "message": msg}
|
return {"success": success, "message": msg}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.status = "failed"
|
log.status = "failed"
|
||||||
log.message = str(e)
|
log.message = str(e)
|
||||||
await db.flush()
|
await db.commit()
|
||||||
raise HTTPException(500, str(e))
|
raise HTTPException(500, str(e))
|
||||||
|
|
||||||
|
|
||||||
@@ -583,7 +574,7 @@ async def auto_renew_all(db: AsyncSession = Depends(get_db), _: bool = Depends(r
|
|||||||
if need_renew:
|
if need_renew:
|
||||||
log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain} (config: {config.name})")
|
log = AcmeLog(domain_id=d.id, action="renew", status="pending", message=f"Auto-renewing {d.domain} (config: {config.name})")
|
||||||
db.add(log)
|
db.add(log)
|
||||||
await db.flush()
|
await db_flush(db)
|
||||||
|
|
||||||
success, msg, cert_info, raw_output = service.renew_certificate(d.domain)
|
success, msg, cert_info, raw_output = service.renew_certificate(d.domain)
|
||||||
log.status = "success" if success else "failed"
|
log.status = "success" if success else "failed"
|
||||||
@@ -601,6 +592,7 @@ async def auto_renew_all(db: AsyncSession = Depends(get_db), _: bool = Depends(r
|
|||||||
else:
|
else:
|
||||||
results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left})
|
results.append({"domain": d.domain, "success": True, "message": f"Skipped, {days_left} days left", "days_left": days_left})
|
||||||
|
|
||||||
|
await db.commit()
|
||||||
return {"results": results}
|
return {"results": results}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from jinja2 import Environment, FileSystemLoader
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import os
|
import os
|
||||||
|
|
||||||
from backend.database import get_db
|
from backend.database import get_db, db_add
|
||||||
from backend.models import Server, Domain, DeployLog
|
from backend.models import Server, Domain, DeployLog
|
||||||
from backend.config import get_settings
|
from backend.config import get_settings
|
||||||
|
|
||||||
@@ -142,5 +142,5 @@ async def report_deploy(
|
|||||||
d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
d = await _find_domain(db, domain, server_id=server.id, acme_config_id=acme_config_id)
|
||||||
|
|
||||||
log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message)
|
log = DeployLog(domain_id=d.id, server_id=server.id, hostname=hostname, status=status, message=message)
|
||||||
db.add(log)
|
await db_add(db, log)
|
||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
|
# 补全 PATH(cron 等非交互式环境默认 PATH 不含 /usr/sbin 等目录)
|
||||||
|
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH}"
|
||||||
|
|
||||||
# === 由 CertCenter 生成 ===
|
# === 由 CertCenter 生成 ===
|
||||||
DOMAIN="{{ domain }}"
|
DOMAIN="{{ domain }}"
|
||||||
BASE_URL="{{ base_url }}"
|
BASE_URL="{{ base_url }}"
|
||||||
|
|||||||
Reference in New Issue
Block a user