Files
Mycontent/scripts/deploy_kr_btapi_verify.py
2026-04-15 19:07:02 +08:00

189 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
kr 宝塔:正式环境发布 + 宝塔面板 API 重启 Go 项目 + 线上冒烟验证。
说明(与现状一致):
- soul-api 默认 **全程宝塔 API**`/files?action=upload` 上传、`UnZip` 解压、`DeleteFile` 删包、`SiteStop/SiteStart` 重启(见 soul-api/master.py `--deploy-method btapi`,无需 SSH
- 若需旧版 SSH 上传:设置环境变量 `DEPLOY_METHOD=ssh` 或 `python master.py --deploy-method ssh`(需服务器允许密码/密钥)。
- soul-admin 静态资源仍通过 soul-admin/master.pySSH宝塔无统一「站点文件 API」封装。
- 环境变量与 soul-api/master.py 相同DEPLOY_HOST、DEPLOY_PASSWORD、BT_PANEL_URL、BT_API_KEY、BT_GO_PROJECT_NAME 等。
用法:
python3 scripts/deploy_kr_btapi_verify.py # 部署 API+管理端 + 验证
python3 scripts/deploy_kr_btapi_verify.py --verify-only # 仅公网冒烟(不部署)
python3 scripts/deploy_kr_btapi_verify.py --skip-bt-ping # 跳过宝塔 API 探活
"""
from __future__ import print_function
import argparse
import hashlib
import os
import subprocess
import sys
import time
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
SOUL_API = os.path.join(ROOT, "soul-api")
SOUL_ADMIN = os.path.join(ROOT, "soul-admin")
def _load_api_cfg():
import importlib.util
path = os.path.join(SOUL_API, "master.py")
spec = importlib.util.spec_from_file_location("soul_api_master_mod", path)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod.get_cfg()
def bt_panel_ping(cfg):
"""调用宝塔 /system?action=GetDiskInfo 校验 API 密钥与面板可达性。"""
try:
import requests
try:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except Exception:
pass
except ImportError:
print("[失败] 需要 requestspip install requests")
return False
url = (cfg.get("bt_panel_url") or "").rstrip("/")
key = cfg.get("bt_api_key") or ""
if not url or not key:
print("[失败] 缺少 BT_PANEL_URL 或 BT_API_KEY")
return False
req_time = int(time.time())
sk_md5 = hashlib.md5(key.encode()).hexdigest()
req_token = hashlib.md5(("%s%s" % (req_time, sk_md5)).encode()).hexdigest()
try:
r = requests.post(
url + "/system?action=GetDiskInfo",
data={"request_time": req_time, "request_token": req_token},
timeout=20,
verify=False,
)
if r.status_code != 200:
print("[宝塔API 探活] HTTP %s" % r.status_code)
return False
j = r.json() if "application/json" in (r.headers.get("content-type") or "") else {}
# 部分面板返回 data 为列表或非标准结构,仅当明确 status=False 视为失败
if isinstance(j, dict) and j.get("status") is False:
print("[宝塔API 探活] %s" % (j.get("msg") or j))
return False
print("[宝塔API 探活] 成功GetDiskInfo", flush=True)
return True
except Exception as e:
print("[宝塔API 探活] 异常: %s" % e)
return False
def run_cmd(cwd, args, env=None):
print("", flush=True)
print(">>> %s" % " ".join(args), flush=True)
e = os.environ.copy()
if env:
e.update(env)
r = subprocess.run(args, cwd=cwd, env=e)
return r.returncode == 0
def verify_public(base_api=None, base_admin=None):
"""公网冒烟:健康检查 + 只读业务接口 + 管理端首页。"""
try:
import requests
try:
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
except Exception:
pass
except ImportError:
print("[失败] 验证阶段需要 requests")
return False
base_api = (base_api or os.environ.get("VERIFY_API_BASE", "https://soulapi.quwanzhi.com")).rstrip("/")
base_admin = (base_admin or os.environ.get("VERIFY_ADMIN_BASE", "https://souladmin.quwanzhi.com")).rstrip("/")
checks = [
("GET %s/health" % base_api, "%s/health" % base_api, lambda r: r.status_code == 200 and '"status"' in r.text),
("GET %s/api/book/parts" % base_api, "%s/api/book/parts" % base_api, lambda r: r.status_code == 200),
("GET %s/api/config" % base_api, "%s/api/config" % base_api, lambda r: r.status_code == 200),
("GET %s/" % base_admin, "%s/" % base_admin, lambda r: r.status_code == 200 and "root" in r.text),
]
print("")
print("=" * 60)
print(" 线上冒烟验证")
print("=" * 60)
all_ok = True
for name, url, pred in checks:
try:
r = requests.get(url, timeout=20, verify=True)
ok = pred(r)
print(" [%s] %s" % ("OK" if ok else "FAIL", name))
if not ok:
print(" status=%s len=%s" % (r.status_code, len(r.text or "")))
all_ok = False
except Exception as ex:
print(" [FAIL] %s%s" % (name, ex))
all_ok = False
return all_ok
def main():
parser = argparse.ArgumentParser(description="kr 宝塔BT API 重启 + 部署 + 验证")
parser.add_argument("--verify-only", action="store_true", help="仅执行公网冒烟,不部署")
parser.add_argument("--skip-bt-ping", action="store_true", help="跳过宝塔面板 API 探活")
parser.add_argument(
"--restart-method",
choices=("btapi", "auto"),
default="auto",
help="soul-api master.py 的 --restart-method默认先宝塔 Go 插件 API失败再 SSH",
)
args = parser.parse_args()
cfg = _load_api_cfg()
if not args.verify_only and not args.skip_bt_ping:
if not bt_panel_ping(cfg):
print("[失败] 宝塔 API 探活未通过,已中止(可用 --skip-bt-ping 跳过)")
return 1
if args.verify_only:
return 0 if verify_public() else 1
py = sys.executable
# master.py 默认也会做 GetDiskInfo 探活;本脚本已探活则跳过,避免重复请求
master_args = [py, "master.py", "--restart-method", args.restart_method]
if not args.skip_bt_ping:
master_args.append("--skip-bt-ping")
if not run_cmd(SOUL_API, master_args):
print("[失败] soul-api 部署失败")
return 1
if not run_cmd(SOUL_ADMIN, [py, "master.py"]):
print("[失败] soul-admin 部署失败")
return 1
time.sleep(2)
if not verify_public():
print("")
print("[失败] 线上冒烟存在未通过项(部署已执行,请排查 Nginx/证书/业务)")
return 1
print("")
print(" 全部完成:已部署且冒烟通过。")
return 0
if __name__ == "__main__":
sys.exit(main() or 0)