#!/usr/bin/env python3 """Start only this project's CPU-only Rundeck container (run via normal sudo).""" import argparse import json import os from pathlib import Path import socket import subprocess import sys CONTAINER = "adamwen-test-console" IMAGE = "adamwen/test-console:20261013-2" def validate_runtime(project): project = project.resolve(strict=True) runtime = project / "runtime" candidates = [runtime] + [runtime / name for name in ("auth", "data", "logs", "projects", "results", "proxy")] for path in candidates: if path.is_symlink() or not path.resolve().is_relative_to(project): raise ValueError("Runtime directories must remain inside this project without symlinks") if path.exists() and not path.is_dir(): raise ValueError("Runtime paths must be directories") return runtime def main(): from lan_config import load_settings, proxy_config, PROXY_IMAGE, NETWORK, PROXY parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--project-root", type=Path, required=True) args = parser.parse_args() project = args.project_root.resolve(strict=True) try: runtime = validate_runtime(project) except ValueError as exc: parser.error(str(exc)) settings = load_settings(project / "deploy/lan-settings.json") for name in (CONTAINER, PROXY): existing = subprocess.run(["docker", "container", "inspect", name], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) if existing.returncode == 0: parser.error(f"{name} already exists; inspect it instead of replacing it") with socket.socket() as probe: probe.bind((settings['address'], settings['port'])) network = subprocess.run(["docker", "network", "inspect", NETWORK], capture_output=True, text=True) if network.returncode == 0: if json.loads(network.stdout)[0].get('Labels', {}).get('cn.aisuan.owner') != CONTAINER: parser.error('Existing network does not belong to this project') else: subprocess.run(['docker','network','create','--label',f'cn.aisuan.owner={CONTAINER}',NETWORK],check=True) for name in ("data", "logs", "projects", "results", "proxy"): path = runtime / name if path.is_symlink(): parser.error("Symlink runtime directories are forbidden") path.mkdir(parents=True, exist_ok=True, mode=0o700) os.chown(path, 1000, 0) os.chmod(path, 0o700) config = runtime / 'proxy/nginx.conf' if config.is_symlink(): parser.error('Proxy config must not be a symlink') config.write_text(proxy_config(settings), encoding='utf-8') os.chmod(config, 0o644) url = f"http://{settings['address']}:{settings['port']}" command = ["docker", "run", "-d", "--name", CONTAINER, "--label", "cn.aisuan.owner=adamwen-test-console", "--runtime", "runc", "--user", "1000:0", "--init", "--cpus", "2", "--memory", "2g", "--memory-swap", "2g", "--pids-limit", "256", "--cap-drop", "ALL", "--security-opt", "no-new-privileges:true", "--restart", "no", "--network", f"container:{PROXY}", "--log-opt", "max-size=10m", "--log-opt", "max-file=3", "-e", f"RUNDECK_GRAILS_URL={url}", "-e", "RUNDECK_SERVER_ADDRESS=127.0.0.1", "-e", "RUNDECK_SERVER_FORWARDED=true", "-e", "RUNDECK_PREAUTH_ENABLED=true", "-e", "RUNDECK_PREAUTH_ATTRIBUTE_NAME=REMOTE_USER_GROUPS", "-e", "RUNDECK_PREAUTH_USERNAME_HEADER=X-Forwarded-Uuid", "-e", "RUNDECK_PREAUTH_ROLES_HEADER=X-Forwarded-Roles", "-e", "RUNDECK_SERVER_SESSION_TIMEOUT=1800", "-e", "RUNDECK_QUARTZ_THREADPOOL_THREADCOUNT=2", "-e", "JVM_MAX_RAM_PERCENTAGE=60", "-e", "NVIDIA_VISIBLE_DEVICES=void", "-e", "CUDA_VISIBLE_DEVICES="] mounts = [(project / 'deploy/realm.disabled.properties', "/home/rundeck/secure/realm.properties", "ro"), (runtime / "data", "/home/rundeck/server/data", "rw"), (runtime / "logs", "/home/rundeck/var/logs", "rw"), (runtime / "projects", "/home/rundeck/projects", "rw"), (runtime / "results", "/home/rundeck/test-results", "rw"), (project / "deploy" / "operator.aclpolicy", "/home/rundeck/etc/xas-operator.aclpolicy", "ro"), (project / "deploy" / "jaas-loginmodule.conf.template", "/etc/remco/templates/jaas-loginmodule.conf", "ro")] for source, destination, access in mounts: command += ["--mount", f"type=bind,src={source},dst={destination}" + (",readonly" if access == "ro" else "")] command.append(IMAGE) proxy_base = ['docker','run','--runtime','runc','--network',NETWORK,'--user','101:101', '--cpus','0.25','--memory','64m','--pids-limit','32','--cap-drop','ALL', '--security-opt','no-new-privileges:true','--read-only', '--tmpfs','/tmp:rw,noexec,nosuid,size=16m', '--mount',f'type=bind,src={config},dst=/etc/nginx/nginx.conf,readonly', '--entrypoint','nginx'] subprocess.run(proxy_base + ['--rm',PROXY_IMAGE,'-t'],check=True) subprocess.run(proxy_base + ['-d','--name',PROXY,'--label',f'cn.aisuan.owner={CONTAINER}', '--publish',f"{settings['address']}:{settings['port']}:8080", '--restart','no','--log-opt','max-size=5m','--log-opt','max-file=2', PROXY_IMAGE,'-g','daemon off;'],check=True) # Namespace loopback cannot be reached via the container IP from the shared host. subprocess.run(command, check=True) print(json.dumps({"container":CONTAINER,"proxy":PROXY,"url":url,"allowed_cidrs":settings['allowed_cidrs'],"gpu":False})) if __name__ == "__main__": main()