Files
lzwlab_cn/deploy_transfer.py
T

213 lines
8.6 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.
"""将 lzwlab.cn 文件快传部署到远程服务器(幂等,可重复执行)。
用法(凭据从 Authentication.md 解析;应用登录密码通过环境变量传入,不落盘):
LZWLAB_APP_PASSWORD='你的应用登录密码' python deploy_transfer.py --username admin
服务器端安装:
- apt 安装 python3-flask、gunicorn
- 应用代码安装到 /opt/lzwlab-transferroot 所有)
- 运行用户 lzwlabsystemd 服务,监听 127.0.0.1:8090
- 数据目录 /var/lib/lzwlab-transfer,配置 /etc/lzwlab-transfer/config.json(密码仅存 scrypt 哈希)
- systemd 定时器每 10 分钟清理过期文件
- 更新 nginx 反代配置与首页链接
"""
import argparse
import json
import os
import re
import sys
import time
import paramiko
ROOT = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, os.path.join(ROOT, "transfer"))
import auth # noqa: E402
REMOTE_TMP = "/tmp/lzwlab-transfer-deploy"
APP_DIR = "/opt/lzwlab-transfer"
DATA_DIR = "/var/lib/lzwlab-transfer"
CONFIG_PATH = "/etc/lzwlab-transfer/config.json"
LOG_DIR = "/var/log/lzwlab-transfer"
SERVICE_USER = "lzwlab"
PORT = 8090
LOCAL_FILES = [
("transfer/app.py", "app.py"),
("transfer/auth.py", "auth.py"),
("transfer/storage.py", "storage.py"),
("transfer/cleanup.py", "cleanup.py"),
("transfer/templates/index.html", "templates/index.html"),
]
SYSTEMD_FILES = [
("transfer/systemd/lzwlab-transfer.service", "lzwlab-transfer.service"),
("transfer/systemd/lzwlab-transfer-cleanup.service", "lzwlab-transfer-cleanup.service"),
("transfer/systemd/lzwlab-transfer-cleanup.timer", "lzwlab-transfer-cleanup.timer"),
]
def parse_auth() -> tuple[str, str, str]:
text = open(os.path.join(ROOT, "Authentication.md"), encoding="utf-8").read()
def find(label: str) -> str:
m = re.search(label + r"\s*[:]\s*(\S+)", text)
if not m:
sys.exit("无法解析 Authentication.md 中的 " + label)
return m.group(1)
return find("IP地址"), find("用户名"), find("密码")
def connect(ip: str, user: str, pw: str) -> paramiko.SSHClient:
cli = paramiko.SSHClient()
cli.set_missing_host_key_policy(paramiko.AutoAddPolicy())
cli.connect(ip, port=22, username=user, password=pw, timeout=20,
look_for_keys=False, allow_agent=False, banner_timeout=30)
return cli
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> tuple[int, str, str]:
print(" $ " + cmd)
_in, out, err = ssh.exec_command(cmd, timeout=timeout)
out_s = out.read().decode("utf-8", "replace")
err_s = err.read().decode("utf-8", "replace")
code = out.channel.recv_exit_status()
if out_s.strip():
print(out_s.strip())
if err_s.strip():
print("[stderr]", err_s.strip())
return code, out_s, err_s
def must(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> str:
code, out, err = run(ssh, cmd, timeout)
if code != 0:
sys.exit("命令失败(exit=%d):%s" % (code, cmd))
return out
def sftp_mkdir_p(sftp: paramiko.SFTPClient, remote_dir: str) -> None:
try:
sftp.stat(remote_dir)
except IOError:
parent = os.path.dirname(remote_dir.rstrip("/"))
if parent and parent != remote_dir:
sftp_mkdir_p(sftp, parent)
sftp.mkdir(remote_dir)
def upload(ssh: paramiko.SSHClient, local: str, remote: str) -> None:
print(" [upload] %s -> %s" % (local, remote))
sftp = ssh.open_sftp()
try:
sftp_mkdir_p(sftp, os.path.dirname(remote))
sftp.put(os.path.join(ROOT, local), remote)
finally:
sftp.close()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--username", default="admin", help="应用登录用户名(默认 admin")
ap.add_argument("--password-env", default="LZWLAB_APP_PASSWORD",
help="应用登录密码所在的环境变量名")
ap.add_argument("--skip-apt", action="store_true", help="跳过 apt 安装依赖")
args = ap.parse_args()
password = os.environ.get(args.password_env, "")
if len(password) < 8:
sys.exit("请通过环境变量 %s 提供至少 8 位的应用登录密码" % args.password_env)
ip, ssh_user, ssh_pw = parse_auth()
print("目标服务器:%s@%s" % (ssh_user, ip))
ssh = connect(ip, ssh_user, ssh_pw)
try:
# 1. 依赖
if not args.skip_apt:
code, _, err = run(ssh, "sudo -n env DEBIAN_FRONTEND=noninteractive "
"apt-get install -y python3-flask gunicorn", timeout=900)
if code != 0:
print("直接安装失败,尝试先 apt-get update …")
must(ssh, "sudo -n apt-get update -qq", timeout=900)
must(ssh, "sudo -n env DEBIAN_FRONTEND=noninteractive "
"apt-get install -y python3-flask gunicorn", timeout=900)
# 2. 目录、运行用户与数据目录
must(ssh, "sudo -n mkdir -p %s %s/templates %s/files %s /etc/lzwlab-transfer"
% (APP_DIR, APP_DIR, DATA_DIR, LOG_DIR))
code, _, _ = run(ssh, "id -u %s >/dev/null 2>&1" % SERVICE_USER)
if code != 0:
must(ssh, "sudo -n useradd --system --home-dir %s --shell /usr/sbin/nologin %s"
% (APP_DIR, SERVICE_USER))
must(ssh, "sudo -n chown -R %s:%s %s %s" % (SERVICE_USER, SERVICE_USER, DATA_DIR, LOG_DIR))
must(ssh, "sudo -n chmod 750 %s" % LOG_DIR)
must(ssh, "sudo -n rm -rf %s && mkdir -p %s" % (REMOTE_TMP, REMOTE_TMP))
# 3. 上传应用文件
for local, name in LOCAL_FILES:
upload(ssh, local, "%s/%s" % (REMOTE_TMP, name))
for local, name in SYSTEMD_FILES:
upload(ssh, local, "%s/systemd/%s" % (REMOTE_TMP, name))
upload(ssh, "index.html", "%s/index.html" % REMOTE_TMP)
upload(ssh, "lzwlab.nginx.conf", "%s/lzwlab.cn.conf" % REMOTE_TMP)
# 4. 生成配置(仅包含密码哈希,不保存明文)
cfg = {
"secret_key": auth.random_secret(),
"cookie_secure": True,
"max_content_length": 5 * 1024 ** 3,
"data_dir": DATA_DIR,
"database_path": DATA_DIR + "/files.db", # 远端为 Linux 路径,不能用 Windows 的 os.path.join
"session_days": 7,
"users": {
args.username: {"password_hash": auth.hash_password(password)}
},
}
print(" [generate] 应用配置(用户:%s" % args.username)
sftp = ssh.open_sftp()
try:
with sftp.open("%s/config.json" % REMOTE_TMP, "w") as fh:
fh.write(json.dumps(cfg, ensure_ascii=False, indent=2))
finally:
sftp.close()
# 5. 安装应用、配置与 systemd
for _, name in LOCAL_FILES:
must(ssh, "sudo -n install -m 644 -o root -g root %s/%s %s/%s"
% (REMOTE_TMP, name, APP_DIR, name))
must(ssh, "sudo -n install -m 640 -o root -g %s %s/config.json %s"
% (SERVICE_USER, REMOTE_TMP, CONFIG_PATH))
for _, name in SYSTEMD_FILES:
must(ssh, "sudo -n install -m 644 -o root -g root %s/systemd/%s /etc/systemd/system/%s"
% (REMOTE_TMP, name, name))
must(ssh, "sudo -n install -m 644 -o root -g root %s/index.html /var/www/lzwlab.cn/index.html"
% REMOTE_TMP)
must(ssh, "sudo -n install -m 644 -o root -g root %s/lzwlab.cn.conf "
"/etc/nginx/sites-available/lzwlab.cn" % REMOTE_TMP)
# 6. 启动服务与定时器
must(ssh, "sudo -n systemctl daemon-reload")
must(ssh, "sudo -n systemctl enable --now lzwlab-transfer.service lzwlab-transfer-cleanup.timer")
must(ssh, "sudo -n systemctl restart lzwlab-transfer.service")
must(ssh, "sudo -n systemctl enable lzwlab-transfer-cleanup.timer && "
"sudo -n systemctl start lzwlab-transfer-cleanup.timer")
time.sleep(1)
# 7. 校验并重载 nginx
must(ssh, "sudo -n nginx -t")
must(ssh, "sudo -n systemctl reload nginx")
must(ssh, "sudo -n rm -rf %s" % REMOTE_TMP)
print()
print("== 部署完成,服务状态 ==")
run(ssh, "systemctl is-active lzwlab-transfer.service lzwlab-transfer-cleanup.timer")
run(ssh, "curl -s -o /dev/null -w 'local https /transfer/ => %{http_code}\n' "
"-k https://127.0.0.1/transfer/ -H 'Host: lzwlab.cn'")
print("应用入口:https://lzwlab.cn/transfer/")
print("登录用户:%s(密码为环境变量 %s 中提供的值)" % (args.username, args.password_env))
finally:
ssh.close()
if __name__ == "__main__":
main()