#!/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 [size] Disk anlegen (qcow2) # aria-vm boot [optionen] VM starten (VNC 127.0.0.1:) # aria-vm screenshot PNG-Screenshot → Shared-Uploads # aria-vm list laufende/vorhandene VMs # aria-vm stop VM beenden # aria-vm rm VM + Disk loeschen # # boot-Optionen: # --iso Boot-ISO (setzt Boot-Reihenfolge auf CD) # --disk-boot von der Festplatte booten (Default nach Installation) # --vnc-display VNC-Display (Port = 5900+N, Default 1) # --mem RAM (Default 1024) # --machine 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:). 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 ' 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?)" # Standard: ins VM-Verzeichnis schreiben (dem aria-User gehoerend) — NICHT # nach /root/... (da kommt der aria-User nicht hin). Der Brain holt das PNG # danach per SSH (base64). Ueberschreibbar via ARIA_VM_SHOT_DIR. local out_dir="${ARIA_VM_SHOT_DIR:-${d}}" mkdir -p "${out_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="${out_dir}/${name}-${ts}.png" if command -v convert >/dev/null; then convert "${ppm}" "${out}" && rm -f "${ppm}" else out="${out_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 "$@"