Live-VNC rendert nie, weil die Bridge die VM nie erreichte: QEMU band 127.0.0.1 (Host-Loopback) und host.docker.internal resolved in der Bridge gar nicht. - aria-vm: --vnc-bind <ip> (Default 127.0.0.1 / env ARIA_VM_VNC_BIND). - Brain: _docker_gateway() (aus /proc/net/route) → boot bindet VNC an die Docker-Gateway-IP (container-erreichbar, NICHT im LAN/Internet). - docker-compose: bridge bekommt extra_hosts host.docker.internal:host-gateway. - Screenshot-Endpoint kopiert das PNG zusaetzlich nach /shared/projects/<pid>/screenshots/ → erscheint im Dateien-Panel. End-to-end auf dem Host validiert: Bridge liest den RFB-Handshake (b'RFB 003.008\n') von der Gateway-gebundenen VNC. aria-vm bereits live. py/bash clean. Deploy: brain rebuild + bridge NEU (extra_hosts). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
216 lines
8.0 KiB
Bash
Executable File
216 lines
8.0 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# aria-vm — ARIAs QEMU-VM-Verwaltung fuer alle Architekturen (laeuft auf dem
|
|
# Host). ARIA ruft das per SSH (aria-wohnung) ueber den qemu-vm-Skill auf.
|
|
#
|
|
# Unterkommandos:
|
|
# aria-vm create <name> <arch> [size] Disk anlegen (qcow2)
|
|
# aria-vm boot <name> [optionen] VM starten (VNC 127.0.0.1:<display>)
|
|
# aria-vm screenshot <name> PNG-Screenshot → Shared-Uploads
|
|
# aria-vm list laufende/vorhandene VMs
|
|
# aria-vm stop <name> VM beenden
|
|
# aria-vm rm <name> VM + Disk loeschen
|
|
#
|
|
# boot-Optionen:
|
|
# --iso <pfad> Boot-ISO (setzt Boot-Reihenfolge auf CD)
|
|
# --disk-boot von der Festplatte booten (Default nach Installation)
|
|
# --vnc-display <N> VNC-Display (Port = 5900+N, Default 1)
|
|
# --mem <MB> RAM (Default 1024)
|
|
# --machine <typ> QEMU-Maschine ueberschreiben
|
|
#
|
|
# VNC bindet immer nur an 127.0.0.1 — von aussen erreichbar ausschliesslich
|
|
# ueber den RVS-Tunnel der Bridge (host.docker.internal:<port>).
|
|
set -euo pipefail
|
|
|
|
VM_ROOT="${ARIA_VM_ROOT:-/var/lib/aria-vms}"
|
|
# Wohin Screenshots geschrieben werden — Host-Pfad des /shared-Volumes, damit
|
|
# Bridge/App sie sehen. Ueberschreibbar via ARIA_VM_SHOT_DIR.
|
|
SHOT_DIR="${ARIA_VM_SHOT_DIR:-/root/ARIA-AGENT/aria-shared/uploads}"
|
|
|
|
die() { echo "aria-vm: $*" >&2; exit 1; }
|
|
|
|
qemu_bin_for() {
|
|
case "$1" in
|
|
x86_64|amd64) echo qemu-system-x86_64 ;;
|
|
i386|i686|x86) echo qemu-system-i386 ;;
|
|
arm|armv7) echo qemu-system-arm ;;
|
|
aarch64|arm64) echo qemu-system-aarch64 ;;
|
|
mips) echo qemu-system-mips ;;
|
|
mipsel) echo qemu-system-mipsel ;;
|
|
mips64) echo qemu-system-mips64 ;;
|
|
ppc) echo qemu-system-ppc ;;
|
|
ppc64) echo qemu-system-ppc64 ;;
|
|
riscv64) echo qemu-system-riscv64 ;;
|
|
sparc) echo qemu-system-sparc ;;
|
|
*) echo "" ;;
|
|
esac
|
|
}
|
|
|
|
vm_dir() { echo "${VM_ROOT}/$1"; }
|
|
vm_pid() { local d; d="$(vm_dir "$1")"; [[ -f "${d}/pid" ]] && cat "${d}/pid" || echo ""; }
|
|
vm_running() {
|
|
local p; p="$(vm_pid "$1")"
|
|
[[ -n "${p}" ]] && kill -0 "${p}" 2>/dev/null
|
|
}
|
|
|
|
cmd_create() {
|
|
local name="${1:?name}" arch="${2:?arch}" size="${3:-10G}"
|
|
local bin; bin="$(qemu_bin_for "${arch}")"
|
|
[[ -n "${bin}" ]] || die "unbekannte Architektur: ${arch}"
|
|
command -v "${bin}" >/dev/null || die "${bin} nicht installiert (qemu-setup.sh?)"
|
|
local d; d="$(vm_dir "${name}")"
|
|
[[ -f "${d}/arch" ]] && die "VM '${name}' existiert schon"
|
|
mkdir -p "${d}"
|
|
echo "${arch}" > "${d}/arch"
|
|
# size='none' oder '0' → keine Festplatte (VM bootet von --iso/--floppy,
|
|
# z.B. OS-Entwicklung von Diskette). Sonst eine qcow2-Disk anlegen.
|
|
if [[ "${size}" == "none" || "${size}" == "0" ]]; then
|
|
echo "VM '${name}' angelegt (${arch}, ohne Disk — bootet von ISO/Diskette)."
|
|
else
|
|
qemu-img create -f qcow2 "${d}/disk.qcow2" "${size}" >/dev/null
|
|
echo "VM '${name}' angelegt (${arch}, ${size})."
|
|
fi
|
|
}
|
|
|
|
cmd_boot() {
|
|
local name="${1:?name}"; shift || true
|
|
local d; d="$(vm_dir "${name}")"
|
|
[[ -f "${d}/arch" || -f "${d}/disk.qcow2" ]] || die "VM '${name}' nicht gefunden (erst 'create')"
|
|
vm_running "${name}" && die "VM '${name}' laeuft bereits"
|
|
local arch; arch="$(cat "${d}/arch" 2>/dev/null || echo x86_64)"
|
|
local bin; bin="$(qemu_bin_for "${arch}")"
|
|
|
|
local iso="" floppy="" disk="" bootdev="" display=1 mem=1024 machine=""
|
|
# VNC bindet an 127.0.0.1 (Loopback) — von aussen nur ueber den RVS-Tunnel der
|
|
# Bridge erreichbar. Die Bridge (Container) kann Loopback aber NICHT erreichen;
|
|
# der Brain gibt deshalb per --vnc-bind die Docker-Gateway-IP mit (container-
|
|
# intern, NICHT im LAN/Internet). Default bleibt Loopback.
|
|
local vncbind="${ARIA_VM_VNC_BIND:-127.0.0.1}"
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--iso) iso="${2:?}"; shift 2 ;;
|
|
--floppy) floppy="${2:?}"; shift 2 ;; # -fda (Disketten-Boot, OS-Dev)
|
|
--disk) disk="${2:?}"; shift 2 ;; # explizite qcow2 statt Auto
|
|
--boot) bootdev="${2:?}"; shift 2 ;; # Boot-Reihenfolge (a/c/d)
|
|
--disk-boot) bootdev="c"; shift ;;
|
|
--vnc-display) display="${2:?}"; shift 2 ;;
|
|
--vnc-bind) vncbind="${2:?}"; shift 2 ;; # Bind-Adresse fuer -vnc
|
|
--mem) mem="${2:?}"; shift 2 ;;
|
|
--machine) machine="${2:?}"; shift 2 ;;
|
|
*) die "unbekannte Option: $1" ;;
|
|
esac
|
|
done
|
|
|
|
# Auto-Erkennung der Medien im VM-Ordner, falls nicht explizit angegeben.
|
|
[[ -z "${disk}" && -f "${d}/disk.qcow2" ]] && disk="${d}/disk.qcow2"
|
|
[[ -z "${floppy}" && -f "${d}/floppy.img" ]] && floppy="${d}/floppy.img"
|
|
[[ -z "${iso}" && -f "${d}/cdrom.iso" ]] && iso="${d}/cdrom.iso"
|
|
[[ -n "${disk}${floppy}${iso}" ]] || \
|
|
die "Keine Boot-Medien fuer '${name}' (disk.qcow2 / --iso / --floppy). Erst 'create <name> <arch> <groesse>' oder ein Medium angeben."
|
|
|
|
local args=(-name "${name}" -m "${mem}"
|
|
-vnc "${vncbind}:${display}"
|
|
-monitor "unix:${d}/monitor.sock,server,nowait"
|
|
-pidfile "${d}/pid" -daemonize)
|
|
[[ -n "${disk}" ]] && args+=(-drive "file=${disk},format=qcow2")
|
|
[[ -n "${floppy}" ]] && args+=(-fda "${floppy}")
|
|
[[ -n "${iso}" ]] && args+=(-cdrom "${iso}")
|
|
|
|
# KVM nur fuer x86 auf x86-Host.
|
|
case "${arch}" in
|
|
x86_64|amd64|i386|i686|x86)
|
|
[[ -e /dev/kvm ]] && args+=(-enable-kvm) ;;
|
|
esac
|
|
# ARM/AArch64 brauchen eine Maschine (kein Default).
|
|
if [[ -z "${machine}" ]]; then
|
|
case "${arch}" in
|
|
arm|armv7|aarch64|arm64) machine="virt" ;;
|
|
esac
|
|
fi
|
|
[[ -n "${machine}" ]] && args+=(-M "${machine}")
|
|
|
|
# Boot-Reihenfolge: explizit, sonst automatisch (ISO→d, nur Diskette→a, sonst c).
|
|
if [[ -z "${bootdev}" ]]; then
|
|
if [[ -n "${iso}" ]]; then bootdev="d"
|
|
elif [[ -n "${floppy}" && -z "${disk}" ]]; then bootdev="a"
|
|
else bootdev="c"; fi
|
|
fi
|
|
args+=(-boot "${bootdev}")
|
|
|
|
"${bin}" "${args[@]}"
|
|
echo "VM '${name}' gestartet (${arch}) — VNC ${vncbind}:${display} (Port $((5900+display)))."
|
|
echo "vnc_display=${display} vnc_port=$((5900+display))"
|
|
}
|
|
|
|
cmd_screenshot() {
|
|
local name="${1:?name}"
|
|
local d; d="$(vm_dir "${name}")"
|
|
vm_running "${name}" || die "VM '${name}' laeuft nicht"
|
|
command -v socat >/dev/null || die "socat fehlt (qemu-setup.sh?)"
|
|
mkdir -p "${SHOT_DIR}"
|
|
local ts; ts="$(date +%s)"
|
|
local ppm="${d}/shot-${ts}.ppm"
|
|
printf 'screendump %s\n' "${ppm}" | socat - "unix-connect:${d}/monitor.sock" >/dev/null
|
|
sleep 0.3
|
|
local out="${SHOT_DIR}/${name}-${ts}.png"
|
|
if command -v convert >/dev/null; then
|
|
convert "${ppm}" "${out}" && rm -f "${ppm}"
|
|
else
|
|
out="${SHOT_DIR}/${name}-${ts}.ppm"; mv "${ppm}" "${out}"
|
|
fi
|
|
echo "screenshot=${out}"
|
|
}
|
|
|
|
cmd_list() {
|
|
[[ -d "${VM_ROOT}" ]] || { echo "(keine VMs)"; return; }
|
|
local any=0
|
|
for d in "${VM_ROOT}"/*/; do
|
|
[[ -d "${d}" ]] || continue
|
|
any=1
|
|
local name arch state
|
|
name="$(basename "${d}")"
|
|
arch="$(cat "${d}/arch" 2>/dev/null || echo '?')"
|
|
if vm_running "${name}"; then state="laeuft (pid $(vm_pid "${name}"))"; else state="gestoppt"; fi
|
|
echo "${name} [${arch}] ${state}"
|
|
done
|
|
[[ "${any}" -eq 1 ]] || echo "(keine VMs)"
|
|
}
|
|
|
|
cmd_stop() {
|
|
local name="${1:?name}"
|
|
local d; d="$(vm_dir "${name}")"
|
|
if vm_running "${name}"; then
|
|
printf 'quit\n' | socat - "unix-connect:${d}/monitor.sock" >/dev/null 2>&1 || true
|
|
sleep 0.5
|
|
vm_running "${name}" && kill "$(vm_pid "${name}")" 2>/dev/null || true
|
|
echo "VM '${name}' gestoppt."
|
|
else
|
|
echo "VM '${name}' lief nicht."
|
|
fi
|
|
rm -f "${d}/pid" "${d}/monitor.sock"
|
|
}
|
|
|
|
cmd_rm() {
|
|
local name="${1:?name}"
|
|
vm_running "${name}" && cmd_stop "${name}"
|
|
rm -rf "$(vm_dir "${name}")"
|
|
echo "VM '${name}' geloescht."
|
|
}
|
|
|
|
main() {
|
|
local sub="${1:-}"; shift || true
|
|
case "${sub}" in
|
|
create) cmd_create "$@" ;;
|
|
boot) cmd_boot "$@" ;;
|
|
screenshot) cmd_screenshot "$@" ;;
|
|
list) cmd_list "$@" ;;
|
|
stop) cmd_stop "$@" ;;
|
|
rm) cmd_rm "$@" ;;
|
|
""|-h|--help)
|
|
sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//' ;;
|
|
*) die "unbekanntes Kommando: ${sub} (siehe --help)" ;;
|
|
esac
|
|
}
|
|
|
|
main "$@"
|