#!/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())