217 lines
7.3 KiB
Python
217 lines
7.3 KiB
Python
#!/usr/bin/env python3
|
|
"""Provision nomadro.live English WordPress + audience router (non-destructive).
|
|
|
|
What it does:
|
|
1) Uploads a fresh WordPress compose stack to /var/www/nomadro-live-wp (on 127.0.0.1:8089).
|
|
2) Uploads a dedicated audience router to /var/www/nomadro-live-router (on 127.0.0.1:4404).
|
|
3) Updates /etc/caddy/sites/nomadro.live.caddy so:
|
|
- /api/* continues to go to the existing FastAPI backend (:8015)
|
|
- other pages:
|
|
zh -> existing Next.js frontend (:3015)
|
|
en/default -> new WordPress (:8089)
|
|
4) Boots the router via systemd and runs bootstrap-wp.sh to seed 20 English articles.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import secrets
|
|
import string
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import paramiko
|
|
|
|
HOST = "107.173.30.245"
|
|
USER = "root"
|
|
|
|
PASSWORD = os.environ.get("NOMADRO_SSH_PASS", "")
|
|
|
|
REMOTE_WP_DIR = "/var/www/nomadro-live-wp"
|
|
REMOTE_ROUTER_DIR = "/var/www/nomadro-live-router"
|
|
REMOTE_SYSTEMD_SERVICE = "/etc/systemd/system/nomadro-live-router.service"
|
|
REMOTE_CADDY_SITE = "/etc/caddy/sites/nomadro.live.caddy"
|
|
|
|
LOCAL_DIR = Path(__file__).resolve().parent
|
|
|
|
WP_LOCAL_DIR = LOCAL_DIR / "wordpress"
|
|
ROUTER_LOCAL_DIR = LOCAL_DIR / "router"
|
|
SYSTEMD_LOCAL_FILE = ROUTER_LOCAL_DIR / "nomadro-live-router.service"
|
|
CADDY_LOCAL_FILE = LOCAL_DIR / "caddy" / "nomadro.live.caddy"
|
|
|
|
|
|
def gen_pass(n: int = 24) -> str:
|
|
alphabet = string.ascii_letters + string.digits
|
|
return "".join(secrets.choice(alphabet) for _ in range(n))
|
|
|
|
|
|
def run(ssh: paramiko.SSHClient, cmd: str, timeout: int = 600) -> None:
|
|
print(f"\n$ {cmd}")
|
|
_stdin, stdout, stderr = ssh.exec_command(cmd, timeout=timeout, get_pty=True)
|
|
out = stdout.read().decode(errors="replace")
|
|
err = stderr.read().decode(errors="replace")
|
|
|
|
def safe_print(s: str, file=None) -> None:
|
|
text = s.encode("ascii", "backslashreplace").decode("ascii")
|
|
print(text, file=file) if file is not None else print(text)
|
|
|
|
if out.strip():
|
|
safe_print(out[-4000:])
|
|
if err.strip():
|
|
safe_print(err[-2000:], file=sys.stderr)
|
|
code = stdout.channel.recv_exit_status()
|
|
if code != 0:
|
|
raise RuntimeError(f"Command failed ({code}): {cmd}")
|
|
|
|
|
|
def sftp_mkdirs(sftp: paramiko.SFTPClient, remote: str) -> None:
|
|
parts = remote.strip("/").split("/")
|
|
cur = ""
|
|
for part in parts:
|
|
cur += "/" + part
|
|
try:
|
|
sftp.stat(cur)
|
|
except FileNotFoundError:
|
|
sftp.mkdir(cur)
|
|
|
|
|
|
def upload_tree(sftp: paramiko.SFTPClient, local: Path, remote: str) -> None:
|
|
sftp_mkdirs(sftp, remote)
|
|
text_ext = {".sh", ".mjs", ".js", ".json", ".yml", ".yaml", ".html", ".php", ".txt", ".md", ".caddy", ".example", ".service"}
|
|
for path in local.rglob("*"):
|
|
if path.name.startswith(".env") or path.suffix in {".pyc"}:
|
|
continue
|
|
rel = path.relative_to(local).as_posix()
|
|
target = f"{remote}/{rel}"
|
|
if path.is_dir():
|
|
sftp_mkdirs(sftp, target)
|
|
else:
|
|
sftp_mkdirs(sftp, str(Path(target).parent).replace("\\", "/"))
|
|
print(f"put {rel}")
|
|
if path.suffix.lower() in text_ext or path.name in {"Caddyfile", "Dockerfile"}:
|
|
data = path.read_bytes().replace(b"\r\n", b"\n").replace(b"\r", b"\n")
|
|
sftp.putfo(__import__("io").BytesIO(data), target)
|
|
else:
|
|
sftp.put(str(path), target)
|
|
|
|
|
|
def wait_wp_http(ssh: paramiko.SSHClient, port: int, tries: int = 60) -> None:
|
|
for _ in range(tries):
|
|
_stdin, stdout, _stderr = ssh.exec_command(
|
|
f"curl -fsS -o /dev/null http://127.0.0.1:{port} || true",
|
|
timeout=20,
|
|
get_pty=True,
|
|
)
|
|
ok = stdout.channel.recv_exit_status() == 0
|
|
if ok:
|
|
print("WP HTTP ready on", f"127.0.0.1:{port}")
|
|
return
|
|
time.sleep(2)
|
|
raise RuntimeError(f"WordPress did not become ready on :{port}")
|
|
|
|
|
|
def main() -> int:
|
|
if not PASSWORD:
|
|
print("Set NOMADRO_SSH_PASS env var", file=sys.stderr)
|
|
return 1
|
|
|
|
if not LOCAL_DIR.exists():
|
|
raise RuntimeError("Local deploy/nomadro-live folder missing")
|
|
|
|
mysql_pass = gen_pass()
|
|
mysql_root = gen_pass()
|
|
|
|
# User requested defaults
|
|
wp_admin_user = "hackrobot"
|
|
wp_admin_pass = "Xiao4669805@"
|
|
wp_admin_email = "hello@nomadro.live"
|
|
|
|
env_text = "\n".join(
|
|
[
|
|
f"MYSQL_PASSWORD={mysql_pass}",
|
|
f"MYSQL_ROOT_PASSWORD={mysql_root}",
|
|
f"WP_ADMIN_USER={wp_admin_user}",
|
|
f"WP_ADMIN_PASSWORD={wp_admin_pass}",
|
|
f"WP_ADMIN_EMAIL={wp_admin_email}",
|
|
"",
|
|
]
|
|
)
|
|
|
|
ssh = paramiko.SSHClient()
|
|
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
|
ssh.connect(HOST, username=USER, password=PASSWORD, timeout=30, allow_agent=False, look_for_keys=False)
|
|
sftp = ssh.open_sftp()
|
|
|
|
# Do not rotate DB passwords if the stack already exists.
|
|
env_exists = False
|
|
try:
|
|
sftp.stat(f"{REMOTE_WP_DIR}/.env")
|
|
env_exists = True
|
|
except FileNotFoundError:
|
|
env_exists = False
|
|
|
|
print("==> Upload nomadro.live WordPress stack")
|
|
upload_tree(sftp, WP_LOCAL_DIR, REMOTE_WP_DIR)
|
|
if not env_exists:
|
|
with sftp.file(f"{REMOTE_WP_DIR}/.env", "w") as f:
|
|
f.write(env_text)
|
|
else:
|
|
print("Keeping existing remote .env (DB already initialized)")
|
|
sftp.chmod(f"{REMOTE_WP_DIR}/bootstrap-wp.sh", 0o755)
|
|
|
|
# 2) Upload router code
|
|
print("\n==> Upload nomadro.live router")
|
|
upload_tree(sftp, ROUTER_LOCAL_DIR, REMOTE_ROUTER_DIR)
|
|
|
|
# 3) Configure Caddy site (overwrite just this one file)
|
|
print("\n==> Update Caddy site for nomadro.live")
|
|
sftp.put(str(CADDY_LOCAL_FILE), REMOTE_CADDY_SITE)
|
|
sftp.chmod(REMOTE_CADDY_SITE, 0o644)
|
|
|
|
# 4) Configure systemd service
|
|
print("\n==> Install systemd service for router")
|
|
sftp.put(str(SYSTEMD_LOCAL_FILE), REMOTE_SYSTEMD_SERVICE)
|
|
sftp.chmod(REMOTE_SYSTEMD_SERVICE, 0o644)
|
|
|
|
sftp.close()
|
|
|
|
# 5) Start docker stack
|
|
print("\n==> Start WordPress stack (docker compose up -d)")
|
|
run(ssh, f"cd {REMOTE_WP_DIR} && docker compose up -d", timeout=600)
|
|
|
|
# Wait WP
|
|
wait_wp_http(ssh, 8089, tries=60)
|
|
|
|
# 6) Seed content + trust pages
|
|
print("\n==> Bootstrap WordPress content (20 English posts)")
|
|
run(ssh, f"cd {REMOTE_WP_DIR} && bash bootstrap-wp.sh", timeout=900)
|
|
|
|
# 7) Install router deps + start service (keep Caddy unchanged until router is ready)
|
|
print("\n==> Start nomadro-live-router")
|
|
run(ssh, f"cd {REMOTE_ROUTER_DIR} && npm install --omit=dev", timeout=600)
|
|
run(ssh, "systemctl daemon-reload", timeout=60)
|
|
# Restart is safe: service does not modify other apps/data.
|
|
run(
|
|
ssh,
|
|
"systemctl enable --now nomadro-live-router.service || systemctl restart nomadro-live-router.service",
|
|
timeout=60,
|
|
)
|
|
|
|
# 8) Validate Caddy and reload
|
|
print("\n==> Reload Caddy (activate nomadro.live split)")
|
|
run(ssh, "caddy validate --config /etc/caddy/Caddyfile", timeout=60)
|
|
run(ssh, "systemctl reload caddy", timeout=60)
|
|
|
|
ssh.close()
|
|
print("\n=== nomadro.live provisioning done ===")
|
|
print(f"WP admin user: {wp_admin_user}")
|
|
print(f"WP admin pass: {wp_admin_pass}")
|
|
print(f"WP admin URL: https://nomadro.live/wp-admin/")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|
|
|