node agent v2: spawn/destroy GPU Ubuntu sessions (noVNC + 4080 passthrough via docker --gpus all)

This commit is contained in:
drjones
2026-08-24 21:13:56 -07:00
parent 28b2c7339f
commit d0f81b7432

View File

@@ -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: