初始化新时空数据应用
This commit is contained in:
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import os
|
||||
from pathlib import Path
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
|
||||
PROJECT_DIR = Path(__file__).resolve().parents[1]
|
||||
DEFAULT_REMOTE_HOST = "121.199.172.246"
|
||||
DEFAULT_REMOTE_USER = "root"
|
||||
DEFAULT_REMOTE_PORT = 22222
|
||||
DEFAULT_REMOTE_DIR = "/root/新时空数据"
|
||||
DEFAULT_IDENTITY_FILE = Path.home() / ".ssh" / "xsk_records_vps_ed25519"
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser(description="部署新时空课程记录查询网页到 VPS")
|
||||
parser.add_argument("--remote-host", default=os.getenv("XSK_REMOTE_HOST", DEFAULT_REMOTE_HOST))
|
||||
parser.add_argument("--remote-user", default=os.getenv("XSK_REMOTE_USER", DEFAULT_REMOTE_USER))
|
||||
parser.add_argument("--remote-port", type=int, default=int(os.getenv("XSK_REMOTE_PORT", str(DEFAULT_REMOTE_PORT))))
|
||||
parser.add_argument("--remote-dir", default=os.getenv("XSK_REMOTE_DIR", DEFAULT_REMOTE_DIR))
|
||||
parser.add_argument(
|
||||
"--identity-file",
|
||||
default=os.getenv("XSK_IDENTITY_FILE", str(DEFAULT_IDENTITY_FILE) if DEFAULT_IDENTITY_FILE.exists() else ""),
|
||||
help="SSH 私钥路径;默认使用 ~/.ssh/xsk_records_vps_ed25519(如果存在)",
|
||||
)
|
||||
parser.add_argument("--app-port", default=os.getenv("XSK_APP_PORT", "18080"))
|
||||
parser.add_argument(
|
||||
"--python-image",
|
||||
default=os.getenv(
|
||||
"XSK_PYTHON_IMAGE",
|
||||
"swr.cn-north-4.myhuaweicloud.com/ddn-k8s/docker.io/library/python:3.12-slim",
|
||||
),
|
||||
help="Docker 基础镜像;VPS 无法访问 Docker Hub 时可使用镜像站",
|
||||
)
|
||||
parser.add_argument("--auth-user", default=os.getenv("XSK_WEB_USER", "wolfydw"))
|
||||
parser.add_argument("--auth-password", default=os.getenv("XSK_WEB_PASSWORD"))
|
||||
parser.add_argument("--use-sshpass", action="store_true", default=os.getenv("XSK_USE_SSHPASS", "") == "1")
|
||||
return parser.parse_args()
|
||||
|
||||
|
||||
def run(command: list[str], cwd: Path | None = None) -> None:
|
||||
env = os.environ.copy()
|
||||
if env.get("XSK_SSH_PASSWORD") and not env.get("SSHPASS"):
|
||||
env["SSHPASS"] = env["XSK_SSH_PASSWORD"]
|
||||
result = subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=False, env=env)
|
||||
if result.returncode != 0:
|
||||
redacted = ["***" if part == os.getenv("XSK_SSH_PASSWORD") else part for part in command]
|
||||
raise RuntimeError(
|
||||
f"命令失败({result.returncode}): {' '.join(shlex.quote(part) for part in redacted)}\n"
|
||||
f"STDOUT: {result.stdout.strip()}\nSTDERR: {result.stderr.strip()}"
|
||||
)
|
||||
if result.stdout.strip():
|
||||
print(result.stdout.strip())
|
||||
|
||||
|
||||
def base_remote(args: argparse.Namespace) -> str:
|
||||
return f"{args.remote_user}@{args.remote_host}"
|
||||
|
||||
|
||||
def ssh_options(args: argparse.Namespace) -> list[str]:
|
||||
options = ["-p", str(args.remote_port), "-o", "StrictHostKeyChecking=accept-new"]
|
||||
if args.identity_file:
|
||||
options.extend(["-i", str(Path(args.identity_file).expanduser()), "-o", "IdentitiesOnly=yes"])
|
||||
return options
|
||||
|
||||
|
||||
def ssh_command(args: argparse.Namespace) -> list[str]:
|
||||
command: list[str] = []
|
||||
if args.use_sshpass:
|
||||
password = os.getenv("XSK_SSH_PASSWORD")
|
||||
if not password:
|
||||
raise RuntimeError("启用 --use-sshpass 时必须设置 XSK_SSH_PASSWORD")
|
||||
command.extend(["sshpass", "-e"])
|
||||
command.extend(["ssh", *ssh_options(args), base_remote(args)])
|
||||
return command
|
||||
|
||||
|
||||
def rsync_command(args: argparse.Namespace) -> list[str]:
|
||||
command: list[str] = []
|
||||
if args.use_sshpass:
|
||||
password = os.getenv("XSK_SSH_PASSWORD")
|
||||
if not password:
|
||||
raise RuntimeError("启用 --use-sshpass 时必须设置 XSK_SSH_PASSWORD")
|
||||
command.extend(["sshpass", "-e"])
|
||||
ssh_command_text = " ".join(shlex.quote(part) for part in ["ssh", *ssh_options(args)])
|
||||
command.extend(
|
||||
[
|
||||
"rsync",
|
||||
"-az",
|
||||
"--delete",
|
||||
"--exclude",
|
||||
".env",
|
||||
"--exclude",
|
||||
"*.log",
|
||||
"-e",
|
||||
ssh_command_text,
|
||||
]
|
||||
)
|
||||
return command
|
||||
|
||||
|
||||
def write_env(args: argparse.Namespace) -> Path:
|
||||
if not args.auth_password:
|
||||
raise RuntimeError("必须通过 XSK_WEB_PASSWORD 或 --auth-password 设置网页访问密码")
|
||||
env_text = "\n".join(
|
||||
[
|
||||
f"APP_PORT={args.app_port}",
|
||||
"TZ=Asia/Shanghai",
|
||||
f"PYTHON_IMAGE={args.python_image}",
|
||||
f"BASIC_AUTH_USERNAME={args.auth_user}",
|
||||
f"BASIC_AUTH_PASSWORD={args.auth_password}",
|
||||
"CLASSNOTES_PATH=/data/classnotes.txt",
|
||||
"ACCOUNTS_PATH=/data/学生课时账户.md",
|
||||
"",
|
||||
]
|
||||
)
|
||||
handle = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False)
|
||||
try:
|
||||
handle.write(env_text)
|
||||
return Path(handle.name)
|
||||
finally:
|
||||
handle.close()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
args = parse_args()
|
||||
app_dir = f"{args.remote_dir.rstrip('/')}/app"
|
||||
data_dir = f"{args.remote_dir.rstrip('/')}/data"
|
||||
env_file = write_env(args)
|
||||
try:
|
||||
run(ssh_command(args) + [f"mkdir -p {shlex.quote(app_dir)} {shlex.quote(data_dir)}"])
|
||||
run(rsync_command(args) + [f"{PROJECT_DIR}/", f"{base_remote(args)}:{app_dir}/"])
|
||||
run(rsync_command(args) + [str(env_file), f"{base_remote(args)}:{app_dir}/.env"])
|
||||
run(ssh_command(args) + [f"cd {shlex.quote(app_dir)} && docker compose up -d --build"])
|
||||
print(f"部署完成: http://{args.remote_host}:{args.app_port}/")
|
||||
finally:
|
||||
env_file.unlink(missing_ok=True)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user