fix(vm): global eindeutige VNC-Displays (kein Doppel-Binding) + Gateway per-boot

- project_vms.add_vm: VNC-Display wird GLOBAL eindeutig vergeben (ueber alle
  Projekte). vnc_display<=0 oder belegtes Display → naechstes freies. Updates
  behalten ihr Display. Verhindert Port-Konflikt (5901) wenn mehrere VMs laufen.
  Getestet: vm1..3 → 1,2,3; Wunsch 2 (belegt) → 4; Update behaelt Display.
- vm_register-Tool + Seed-Regel: vnc_display weglassen, wird auto-vergeben.
- Gateway-IP: der Brain liest sie aus /proc/net/route bei JEDEM Boot frisch
  (Docker-IP-Aenderung nach Netz-Neuaufbau wird abgefangen); boot-Antwort
  enthaelt vnc_bind zur Transparenz.

py_compile clean. Brain-only, Deploy: brain rebuild.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 00:23:23 +02:00
co-authored by Claude Opus 4.8
parent d38d62ba21
commit 2832ab3dc9
4 changed files with 44 additions and 15 deletions
+2 -2
View File
@@ -1184,7 +1184,7 @@ META_TOOLS = [
"disk": {"type": "string", "description": "optional: qcow2-Pfad (Festplatte)."}, "disk": {"type": "string", "description": "optional: qcow2-Pfad (Festplatte)."},
"floppy": {"type": "string", "description": "optional: Disketten-Image-Pfad (-fda)."}, "floppy": {"type": "string", "description": "optional: Disketten-Image-Pfad (-fda)."},
"iso": {"type": "string", "description": "optional: Boot-ISO-Pfad."}, "iso": {"type": "string", "description": "optional: Boot-ISO-Pfad."},
"vnc_display": {"type": "integer", "description": "VNC-Display (Default 1 → Port 5901)."}, "vnc_display": {"type": "integer", "description": "meist WEGLASSEN — das VNC-Display wird global eindeutig auto-vergeben (kein Port-Konflikt bei mehreren VMs). Nur setzen wenn Du ein bestimmtes willst."},
"mem": {"type": "integer", "description": "RAM in MB (Default 1024)."}, "mem": {"type": "integer", "description": "RAM in MB (Default 1024)."},
}, },
"required": ["name", "arch"], "required": ["name", "arch"],
@@ -2846,7 +2846,7 @@ class Agent:
(arguments.get("iso") or "").strip(), (arguments.get("iso") or "").strip(),
(arguments.get("floppy") or "").strip(), (arguments.get("floppy") or "").strip(),
(arguments.get("disk") or "").strip(), (arguments.get("disk") or "").strip(),
int(arguments.get("vnc_display") or 1), int(arguments.get("vnc_display") or 0),
int(arguments.get("mem") or 1024), int(arguments.get("mem") or 1024),
) )
except (ValueError, TypeError) as exc: except (ValueError, TypeError) as exc:
+1 -1
View File
@@ -1063,7 +1063,7 @@ def project_vm_boot(project_id: str, name: str):
boot_args += ["--vnc-bind", gw] boot_args += ["--vnc-bind", gw]
rc, out, err = _ssh_aria_vm(*boot_args, timeout=40) rc, out, err = _ssh_aria_vm(*boot_args, timeout=40)
return {"ok": rc == 0, "name": name, "vnc_port": 5900 + int(vm.get("vnc_display", 1)), return {"ok": rc == 0, "name": name, "vnc_port": 5900 + int(vm.get("vnc_display", 1)),
"output": (out.strip() or err.strip())[:500]} "vnc_bind": gw or "127.0.0.1", "output": (out.strip() or err.strip())[:500]}
@app.post("/projects/{project_id}/vms/{name}/stop") @app.post("/projects/{project_id}/vms/{name}/stop")
+36 -9
View File
@@ -51,13 +51,30 @@ def get_vm(project_id: str, name: str) -> Optional[dict]:
return None return None
def _used_displays(data: dict, exclude: object = None) -> set:
"""Alle VNC-Displays, die ueber ALLE Projekte belegt sind (exclude = eine
VM-Dict-Instanz, die ignoriert wird fuer Updates)."""
used = set()
for lst in data.values():
for v in lst:
if v is exclude:
continue
try:
used.add(int(v.get("vnc_display", 1)))
except (TypeError, ValueError):
pass
return used
def add_vm(project_id: str, name: str, arch: str, iso: str = "", def add_vm(project_id: str, name: str, arch: str, iso: str = "",
floppy: str = "", disk: str = "", floppy: str = "", disk: str = "",
vnc_display: int = 1, mem: int = 1024) -> dict: vnc_display: int = 0, mem: int = 1024) -> dict:
"""Registriert/aktualisiert eine VM. Medien (disk/floppy/iso) sind optional — """Registriert/aktualisiert eine VM. Medien (disk/floppy/iso) optional —
leer = aria-vm erkennt disk.qcow2/floppy.img/cdrom.iso im VM-Ordner selbst. leer = aria-vm erkennt disk.qcow2/floppy.img/cdrom.iso im VM-Ordner selbst.
ARIA setzt hier, was sie zum Task passend gebaut hat (Festplatte fuer DOS-
Spiele, Diskette fuer OS-Dev, ISO fuer Installer).""" Das VNC-Display wird GLOBAL eindeutig vergeben (ueber alle Projekte), damit
mehrere laufende VMs nicht denselben Port doppelt binden. vnc_display<=0 oder
ein bereits belegtes Display automatisch das naechste freie."""
if not NAME_RE.match(name or ""): if not NAME_RE.match(name or ""):
raise ValueError(f"Ungueltiger VM-Name: {name!r} (nur a-z0-9_-, max 40)") raise ValueError(f"Ungueltiger VM-Name: {name!r} (nur a-z0-9_-, max 40)")
if arch not in VALID_ARCH: if arch not in VALID_ARCH:
@@ -65,13 +82,23 @@ def add_vm(project_id: str, name: str, arch: str, iso: str = "",
data = _load() data = _load()
lst = data.setdefault(project_id or "", []) lst = data.setdefault(project_id or "", [])
now = int(time.time()) now = int(time.time())
existing = next((v for v in lst if v.get("name") == name), None)
used = _used_displays(data, exclude=existing)
req = int(vnc_display or 0)
if req <= 0 and existing: # Update ohne Display-Wunsch → behalten
req = int(existing.get("vnc_display", 0) or 0)
if req <= 0 or req in used: # frei/eindeutig machen
req = 1
while req in used:
req += 1
fields = {"arch": arch, "iso": iso, "floppy": floppy, "disk": disk, fields = {"arch": arch, "iso": iso, "floppy": floppy, "disk": disk,
"vnc_display": int(vnc_display), "mem": int(mem), "updated_at": now} "vnc_display": req, "mem": int(mem), "updated_at": now}
for v in lst: if existing:
if v.get("name") == name: existing.update(fields)
v.update(fields)
_save(data) _save(data)
return v return existing
vm = {"name": name, "created_at": now, **fields} vm = {"name": name, "created_at": now, **fields}
lst.append(vm) lst.append(vm)
_save(data) _save(data)
+4 -2
View File
@@ -435,8 +435,10 @@ SEED_RULES: List[dict] = [
" - `aria-vm list` / `aria-vm stop <name>` / `aria-vm rm <name>`.\n" " - `aria-vm list` / `aria-vm stop <name>` / `aria-vm rm <name>`.\n"
"\n" "\n"
"PFLICHT nach dem Bau/Boot: `vm_register(name, arch, disk?/floppy?/iso?, " "PFLICHT nach dem Bau/Boot: `vm_register(name, arch, disk?/floppy?/iso?, "
"vnc_display, mem)` im aktuellen Projekt aufrufen — mit den Medien, die Du " "mem?)` im aktuellen Projekt aufrufen — mit den Medien, die Du gebaut hast. "
"gebaut hast. ERST DANN erscheint die VM in Stefans Desktop-Panel (Cockpit), " "vnc_display WEGLASSEN — es wird global eindeutig auto-vergeben (kein "
"Port-Konflikt, wenn mehrere VMs laufen). ERST DANN erscheint die VM in "
"Stefans Desktop-Panel (Cockpit), "
"wo er sie Starten/Stoppen/Verbinden kann. Ohne vm_register bleibt seine " "wo er sie Starten/Stoppen/Verbinden kann. Ohne vm_register bleibt seine "
"Liste leer, obwohl die VM laeuft. Der Startbefehl steht als Wert dahinter.\n" "Liste leer, obwohl die VM laeuft. Der Startbefehl steht als Wert dahinter.\n"
"\n" "\n"