2026-10-14 09:38:58 +08:00
|
|
|
"""First deployment / explicit port cutover. Only manages this application's resources."""
|
|
|
|
|
import argparse
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
import shutil
|
|
|
|
|
import socket
|
|
|
|
|
import subprocess
|
|
|
|
|
import sys
|
|
|
|
|
|
|
|
|
|
ROOT = Path("/adamwen-work-space/xas-test-library")
|
|
|
|
|
NAME = "adamwen-script-console"
|
2026-10-14 10:25:07 +08:00
|
|
|
IMAGE = "adamwen/script-console:20261014-1"
|
2026-10-14 09:38:58 +08:00
|
|
|
UNIT = "adamwen-script-worker.service"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def validate_runtime(root):
|
|
|
|
|
for path in (root / "runtime", *(root / "runtime" / name for name in ("console-data", "library", "runs", "native-ipc"))):
|
|
|
|
|
if not path.resolve().is_relative_to(root.resolve()):
|
|
|
|
|
raise ValueError("Runtime must remain inside this project")
|
|
|
|
|
if any(part.is_symlink() for part in (path, *path.parents)):
|
|
|
|
|
raise ValueError("Runtime symlink rejected")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def command(*args, timeout=30):
|
|
|
|
|
return subprocess.check_output(args, text=True, timeout=timeout).strip()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def web_arguments(port):
|
|
|
|
|
validate_runtime(ROOT)
|
|
|
|
|
runtime = ROOT / "runtime"
|
|
|
|
|
args = ["docker", "run", "-d", "--name", NAME, "--label", "xas.application=script-console",
|
|
|
|
|
"--runtime", "runc", "--network", "host", "--user", "0:0", "--cap-drop", "ALL",
|
|
|
|
|
"--security-opt", "no-new-privileges", "--read-only", "--memory", "512m", "--cpus", "1",
|
|
|
|
|
"--pids-limit", "96", "--tmpfs", "/tmp:rw,nosuid,nodev,noexec,size=67108864",
|
|
|
|
|
"--log-driver", "local", "--log-opt", "max-size=10m", "--log-opt", "max-file=2",
|
|
|
|
|
"--env", "XAS_CONSOLE_ROOT=" + str(runtime)]
|
|
|
|
|
for name in ("console-data", "library", "runs", "native-ipc"):
|
|
|
|
|
path = runtime / name
|
|
|
|
|
args += ["--mount", f"type=bind,src={path},dst={path}" + (",readonly" if name in {"runs", "native-ipc"} else "")]
|
|
|
|
|
sources = json.loads((ROOT / "catalog/external-sources.json").read_text())["sources"]
|
|
|
|
|
for source in sources:
|
|
|
|
|
path = Path(source["path"])
|
|
|
|
|
if path.is_dir():
|
|
|
|
|
args += ["--mount", f"type=bind,src={path},dst={path},readonly"]
|
|
|
|
|
if path.name == "scripts":
|
|
|
|
|
for folder in ("logs", "results"):
|
|
|
|
|
logs = path.parent / folder
|
|
|
|
|
if logs.is_dir():
|
|
|
|
|
args += ["--mount", f"type=bind,src={logs},dst={logs},readonly"]
|
|
|
|
|
return args + [IMAGE, "--port", str(port)]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def prepare():
|
|
|
|
|
if os.geteuid() != 0 or ROOT.resolve() != ROOT or not (ROOT / "console/worker.py").is_file():
|
|
|
|
|
raise RuntimeError("Run through the documented sudo flow in the confirmed project")
|
|
|
|
|
validate_runtime(ROOT)
|
|
|
|
|
for name in ("console-data", "library", "runs", "native-ipc"):
|
|
|
|
|
path = ROOT / "runtime" / name
|
|
|
|
|
if path.is_symlink():
|
|
|
|
|
raise RuntimeError("Runtime symlink rejected")
|
|
|
|
|
path.mkdir(mode=0o750, exist_ok=True)
|
|
|
|
|
runner = command("docker", "image", "inspect", "--format", "{{.Id}}", "adamwen/test-console:20261013-2")
|
|
|
|
|
config = {"project": str(ROOT), "runner_image": runner,
|
|
|
|
|
"smoke_upstream": "http://100.64.0.55:18100",
|
|
|
|
|
"smoke_credential": "/dengyuyun-joy-work-space/workpc-claude-code/20261010-dsv41-trial/configs/vllm.env"}
|
|
|
|
|
path = ROOT / "deploy/console-worker.json"
|
|
|
|
|
if path.exists() and json.loads(path.read_text()) != config:
|
|
|
|
|
raise RuntimeError("Existing worker configuration differs; review before replacing")
|
|
|
|
|
path.write_text(json.dumps(config, indent=2))
|
|
|
|
|
path.chmod(0o600)
|
|
|
|
|
# Existing roots are 0700/root. Container root has no capabilities and read-only mounts.
|
|
|
|
|
# Runtime code used by non-root jobs needs read access, without changing original scripts.
|
|
|
|
|
for p in (ROOT / "console").rglob("*"):
|
|
|
|
|
p.chmod(0o755 if p.is_dir() else 0o644)
|
|
|
|
|
(ROOT / "console").chmod(0o755)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def start(port):
|
|
|
|
|
existing = command("docker", "ps", "-aq", "--filter", "name=^/" + NAME + "$")
|
|
|
|
|
if existing:
|
|
|
|
|
raise RuntimeError("Console container already exists; inspect before replacement")
|
|
|
|
|
with socket.socket() as probe:
|
|
|
|
|
probe.bind(("10.88.118.188", port))
|
|
|
|
|
print(command(*web_arguments(port)))
|
|
|
|
|
service = Path("/etc/systemd/system") / UNIT
|
|
|
|
|
content = (ROOT / "deploy/console-worker.service").read_bytes()
|
|
|
|
|
if service.exists() and service.read_bytes() != content:
|
|
|
|
|
raise RuntimeError("Existing systemd service differs")
|
|
|
|
|
service.write_bytes(content)
|
|
|
|
|
command("systemctl", "daemon-reload")
|
|
|
|
|
# Start only; no automatic boot scheduling is enabled by this deployment.
|
|
|
|
|
command("systemctl", "start", UNIT)
|
|
|
|
|
print(command("systemctl", "is-active", UNIT))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
parser = argparse.ArgumentParser()
|
|
|
|
|
parser.add_argument("--port", type=int, choices=[14440, 14442], default=14442)
|
|
|
|
|
options = parser.parse_args()
|
|
|
|
|
prepare()
|
|
|
|
|
start(options.port)
|