Files
Nexus/server/api/btpanel.py
T
r 82426b1941 feat(btpanel): 独立宝塔模块与 SSH 自动获取 API
新增侧栏「宝塔面板」菜单、代理 API 与连接配置页;通过 SSH 读写子机
api.json 自动写入凭据,5 分钟后台重试,含审计修复、检测缓存与并发锁。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-21 09:13:14 +08:00

429 lines
13 KiB
Python

"""Baota (BT Panel) management API — standalone module, not merged with servers UI."""
from __future__ import annotations
import logging
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from server.api.auth_jwt import get_current_admin
from server.api.btpanel_schemas import (
BtPanelApplyCert,
BtPanelBatchBootstrap,
BtPanelConfigUpdate,
BtPanelCreateDatabase,
BtPanelCreateSite,
BtPanelDbBackup,
BtPanelDbPassword,
BtPanelDomainAdd,
BtPanelDomainDelete,
BtPanelGlobalSettingsUpdate,
BtPanelServiceAction,
BtPanelSetSsl,
BtPanelSiteAction,
)
from server.api.dependencies import get_db
from server.application.services.btpanel_service import BtPanelService
from server.domain.models import Admin
from server.infrastructure.btpanel.client import BtPanelApiError
logger = logging.getLogger("nexus.api.btpanel")
router = APIRouter(prefix="/api/btpanel", tags=["btpanel"])
def _client_ip(request: Request) -> str | None:
return request.client.host if request.client else None
def _svc(db: AsyncSession) -> BtPanelService:
return BtPanelService(db)
def _http_error(exc: Exception) -> HTTPException:
if isinstance(exc, ValueError):
return HTTPException(status_code=400, detail=str(exc))
if isinstance(exc, BtPanelApiError):
return HTTPException(status_code=502, detail=str(exc))
if isinstance(exc, RuntimeError):
return HTTPException(status_code=502, detail=str(exc))
logger.exception("btpanel error")
return HTTPException(status_code=500, detail="宝塔操作失败")
@router.get("/servers")
async def list_bt_servers(
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
) -> list[dict[str, Any]]:
return await _svc(db).list_server_statuses()
@router.get("/servers/{server_id}/config")
async def get_bt_config(
server_id: int,
refresh_bt_installed: bool = Query(False, description="强制 SSH 重新检测宝塔是否安装"),
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
try:
return await _svc(db).get_config(server_id, refresh_bt_installed=refresh_bt_installed)
except Exception as exc:
raise _http_error(exc) from exc
@router.put("/servers/{server_id}/config")
async def update_bt_config(
server_id: int,
body: BtPanelConfigUpdate,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
try:
return await _svc(db).update_config(
server_id,
base_url=body.base_url,
api_key=body.api_key,
verify_ssl=body.verify_ssl,
auto_bootstrap=body.auto_bootstrap,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/settings")
async def get_bt_global_settings(
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
return await _svc(db).get_global_settings()
@router.put("/settings")
async def update_bt_global_settings(
body: BtPanelGlobalSettingsUpdate,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
try:
return await _svc(db).update_global_settings(
bt_panel_source_ip=body.bt_panel_source_ip,
bt_panel_auto_bootstrap_enabled=body.bt_panel_auto_bootstrap_enabled,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/bootstrap")
async def bootstrap_bt_server(
server_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
try:
return await _svc(db).bootstrap_server(
server_id,
source="manual",
admin_username=admin.username,
ip_address=_client_ip(request),
force=True,
)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/batch-bootstrap")
async def batch_bootstrap_bt_servers(
body: BtPanelBatchBootstrap,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
) -> dict[str, Any]:
try:
return await _svc(db).batch_bootstrap(
body.server_ids,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/login-url")
async def create_bt_login_url(
server_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
) -> dict[str, str]:
try:
return await _svc(db).create_login_url(
server_id,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/system/total")
async def bt_system_total(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).system_total(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/system/disk")
async def bt_system_disk(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).system_disk(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/system/network")
async def bt_system_network(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).system_network(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/sites")
async def bt_list_sites(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).list_sites(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/sites/start")
async def bt_site_start(
server_id: int,
body: BtPanelSiteAction,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).site_start(server_id, body.site_id, body.name)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/sites/stop")
async def bt_site_stop(
server_id: int,
body: BtPanelSiteAction,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).site_stop(server_id, body.site_id, body.name)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/sites/php-versions")
async def bt_php_versions(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).php_versions(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/sites")
async def bt_create_site(
server_id: int,
body: BtPanelCreateSite,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
):
import json
webname_obj = {"domain": body.webname, "domainlist": [], "count": 0}
payload = {
"webname": json.dumps(webname_obj, ensure_ascii=False),
"path": body.path,
"type_id": body.type_id,
"type": body.type,
"version": body.version,
"port": body.port,
"ps": body.ps,
"ftp": "true" if body.ftp else "false",
"sql": "true" if body.sql else "false",
}
try:
return await _svc(db).create_site(
server_id, payload,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/sites/{site_id}/domains")
async def bt_list_domains(
server_id: int,
site_id: int,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).list_domains(server_id, site_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/domains")
async def bt_add_domain(
server_id: int,
body: BtPanelDomainAdd,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).add_domain(server_id, body.model_dump())
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/domains/delete")
async def bt_delete_domain(
server_id: int,
body: BtPanelDomainDelete,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).delete_domain(server_id, body.model_dump())
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/ssl/sites")
async def bt_ssl_sites(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).list_ssl_sites(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/ssl/set")
async def bt_set_ssl(
server_id: int,
body: BtPanelSetSsl,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).set_ssl(
server_id, body.model_dump(),
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/ssl/apply")
async def bt_apply_ssl(
server_id: int,
body: BtPanelApplyCert,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).apply_letsencrypt(server_id, body.model_dump())
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/databases")
async def bt_list_databases(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).list_databases(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/databases")
async def bt_create_database(
server_id: int,
body: BtPanelCreateDatabase,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).create_database(
server_id, body.model_dump(),
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/databases/password")
async def bt_db_password(
server_id: int,
body: BtPanelDbPassword,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).reset_database_password(server_id, body.model_dump())
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/databases/backup")
async def bt_db_backup(
server_id: int,
body: BtPanelDbBackup,
db: AsyncSession = Depends(get_db),
_admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).backup_database(server_id, body.id)
except Exception as exc:
raise _http_error(exc) from exc
@router.get("/servers/{server_id}/crontab")
async def bt_crontab(server_id: int, db: AsyncSession = Depends(get_db), _admin: Admin = Depends(get_current_admin)):
try:
return await _svc(db).list_crontab(server_id)
except Exception as exc:
raise _http_error(exc) from exc
@router.post("/servers/{server_id}/services")
async def bt_service_admin(
server_id: int,
body: BtPanelServiceAction,
request: Request,
db: AsyncSession = Depends(get_db),
admin: Admin = Depends(get_current_admin),
):
try:
return await _svc(db).service_admin(
server_id, body.name, body.type,
admin_username=admin.username,
ip_address=_client_ip(request),
)
except Exception as exc:
raise _http_error(exc) from exc