From d0f81b74329712aff325eab01832859235bdcbc6 Mon Sep 17 00:00:00 2001 From: drjones Date: Mon, 24 Aug 2026 21:13:56 -0700 Subject: [PATCH] node agent v2: spawn/destroy GPU Ubuntu sessions (noVNC + 4080 passthrough via docker --gpus all) --- vortex-node-agent-linux.py | 100 ++++++++++++++----------------------- 1 file changed, 38 insertions(+), 62 deletions(-) diff --git a/vortex-node-agent-linux.py b/vortex-node-agent-linux.py index e4346dc..258be53 100644 --- a/vortex-node-agent-linux.py +++ b/vortex-node-agent-linux.py @@ -1,17 +1,14 @@ #!/usr/bin/env python3 """ -VORTEX_GPU — Linux Host Node Agent (port of vortex-node-agent.ps1) +VORTEX_GPU — Linux Host Node Agent (v2 — Ubuntu-session provisioning) Target: Ubuntu (nightmare .128, RTX 4080 SUPER 16GB) -Responsibilities (mirrors the Windows agent): - - Register this GPU box + stream nvidia-smi telemetry every N sec. - - Poll the gateway for jobs: - * shell : run an arbitrary cmd, return stdout - * hashcat / comfyui : run the command against the local GPU - * provision_comfyui : launch an ISOLATED ComfyUI instance on a dedicated - port + dedicated user/output/input dirs (tenant sees - a clean private machine; the physical GPU is shared/hidden) - * destroy_instance : kill the ComfyUI process for an instance +Spawns in-browser Ubuntu desktop sessions, each with the 4080 attached (--gpus all): + - provision_ubuntu : docker run a full Ubuntu LXDE desktop (noVNC) with GPU, on a + dedicated port. Tenant gets a clean private machine; the physical + GPU is shared/hidden. + - destroy_ubuntu : docker rm -f the session container. + - shell / hashcat / comfyui : run an arbitrary command against the local GPU. Auth: X-Node-Secret header (matches server.ts nodeAuthorized). Runs as a systemd service: vortex-node-agent.service @@ -28,13 +25,8 @@ SECRET = os.environ.get("VORTEX_NODE_SECRET", "99496a5bf30b5a7411d3a60bf096ca6 INTERVAL = int(os.environ.get("VORTEX_INTERVAL", "5")) HOSTNAME = socket.gethostname() -HOME = os.path.expanduser("~") -COMFY_DIR = os.path.join(HOME, "ComfyUI") -COMFY_PY = os.path.join(COMFY_DIR, "venv", "bin", "python") -INST_DIR = os.path.join(HOME, "vortex-agent", "instances") -os.makedirs(INST_DIR, exist_ok=True) - -_instance_pids = {} # instanceId -> pid +SESSION_IMAGE = os.environ.get("VORTEX_SESSION_IMAGE", "dorowu/ubuntu-desktop-lxde-vnc:latest") +HOME = os.path.expanduser("~") def http(method, path, body=None): @@ -106,6 +98,10 @@ def uptime_sec(): return int(float(open("/proc/uptime").read().split()[0])) +def _docker(args, timeout=180): + return subprocess.run(["docker"] + args, capture_output=True, text=True, timeout=timeout) + + def run_shell(command): try: r = subprocess.run(["bash", "-c", command], capture_output=True, text=True, timeout=600) @@ -114,46 +110,25 @@ def run_shell(command): return False, f"error: {e}" -def provision_comfyui(instance_id, port, user_dir): - os.makedirs(user_dir, exist_ok=True) - for sub in ("user", "output", "input", "models", "custom_nodes"): - os.makedirs(os.path.join(user_dir, sub), exist_ok=True) - if not os.path.exists(os.path.join(COMFY_DIR, "main.py")): - return False, "ComfyUI base not found on this node" - if not os.path.exists(COMFY_PY): - return False, "ComfyUI venv not found" - args = [COMFY_PY, os.path.join(COMFY_DIR, "main.py"), - "--listen", "0.0.0.0", "--port", str(port), - "--user-directory", os.path.join(user_dir, "user"), - "--output-directory", os.path.join(user_dir, "output"), - "--input-directory", os.path.join(user_dir, "input")] - try: - proc = subprocess.Popen(args, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - start_new_session=True) - _instance_pids[instance_id] = proc.pid - with open(os.path.join(user_dir, "instance.pid"), "w") as f: - f.write(str(proc.pid)) - return True, f"launched pid={proc.pid} port={port}" - except Exception as e: - return False, f"failed to launch: {e}" +def provision_ubuntu(instance_id, port, password, resolution): + """Spawn a full Ubuntu desktop session with the 4080 attached (noVNC on mapped port).""" + name = f"vortex-{instance_id}" + _docker(["rm", "-f", name], timeout=30) # clean any stale instance + r = _docker(["run", "-d", "--gpus", "all", "--name", name, + "-p", f"{port}:80", + "-e", f"VNC_PASSWORD={password}", + "-e", f"RESOLUTION={resolution or '1440x900'}", + SESSION_IMAGE]) + if r.returncode == 0: + cid = r.stdout.strip()[:12] + return True, f"launched container={name} id={cid} port={port}" + return False, f"failed: {r.stderr.strip()[:400]}" -def destroy_instance(instance_id): - pid = _instance_pids.pop(instance_id, None) - if pid is None: - pidfile = os.path.join(INST_DIR, instance_id, "instance.pid") - if os.path.exists(pidfile): - try: - pid = int(open(pidfile).read().strip()) - except Exception: - pid = None - if pid: - try: - os.kill(pid, 15) - return True, f"killed pid={pid}" - except Exception as e: - return True, f"kill pid={pid}: {e}" - return True, "no pid found" +def destroy_ubuntu(instance_id): + name = f"vortex-{instance_id}" + r = _docker(["rm", "-f", name], timeout=30) + return True, (r.stdout.strip() or f"removed {name}") def handle_job(job): @@ -162,14 +137,15 @@ def handle_job(job): payload = job.get("payload", {}) or {} if kind in ("shell", "hashcat", "comfyui"): ok, result = run_shell(cmd) - elif kind == "provision_comfyui": - ok, result = provision_comfyui(payload.get("instanceId", "inst"), - payload.get("port", 8189), - payload.get("userDir") or os.path.join(INST_DIR, payload.get("instanceId", "inst"))) - elif kind == "destroy_instance": - ok, result = destroy_instance(payload.get("instanceId", "")) + elif kind == "provision_ubuntu": + ok, result = provision_ubuntu(payload.get("instanceId", "inst"), + int(payload.get("port", 6090)), + payload.get("password", "vortex"), + payload.get("resolution", "1440x900")) + elif kind == "destroy_ubuntu": + ok, result = destroy_ubuntu(payload.get("instanceId", "")) else: - ok, result = False, "unknown job kind" + ok, result = False, f"unknown job kind: {kind}" try: http("POST", f"/api/node/jobs/{job['id']}/result", {"ok": ok, "result": result}) except Exception as e: