#!/bin/bash # fix-libvirt-smbios.sh — Apply per-VM SMBIOS to a libvirt domain so the # VM can produce a valid MukenVault auth_dna (Tier 1+2). # # What this does: # 1. Backup current domain XML to /var/backups/mukenvault-libvirt-smbios/ # 2. Shutdown the VM (acpi → 60s timeout → destroy fallback) # 3. Edit XML in place (Python stdlib xml.etree): # a. Ensure # b. Replace with system/baseBoard/chassis # entries (fresh per-VM serials; SMBIOS uuid = existing domain # uuid so libvirt's UUID consistency check passes) # The stays untouched — libvirt refuses to redefine # an existing domain with a changed root uuid AND requires the # sysinfo system uuid to match it. The existing domain uuid is # already unique per VM (libvirt enforces at define time), so the # guest will see a unique /sys/class/dmi/id/product_uuid simply # by surfacing that same uuid via SMBIOS Type 1. # 4. virsh define with the new XML # 5. virsh start # # Rollback: # virsh define /var/backups/mukenvault-libvirt-smbios/-.xml # # Usage (libvirt host, as root): # sudo curl -fsSL https://install.mukenvault.com/fix-libvirt-smbios.sh | \ # sudo bash -s -- # # Caveats: # - VM downtime ~10-30s (graceful shutdown) or up to 60s + destroy. # - Requires libvirt host privileges (root); does NOT touch the guest. # - After this finishes, run /verify-smbios.sh inside the guest to # confirm the new DMI values are visible. set -euo pipefail IFS=$'\n\t' red() { printf '\033[1;31m%s\033[0m' "$1"; } green() { printf '\033[1;32m%s\033[0m' "$1"; } yellow() { printf '\033[1;33m%s\033[0m' "$1"; } log() { printf '\033[1;36m[fix-libvirt]\033[0m %s\n' "$*"; } warn() { printf '\033[1;33m[fix-libvirt WARN]\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31m[fix-libvirt FAIL]\033[0m %s\n' "$*" >&2; exit 1; } if [[ "$(id -u)" -ne 0 ]]; then fail "must run as root on the libvirt host (re-run with sudo)" fi VM="${1:-}" if [[ -z "$VM" ]]; then cat >&2 <<'EOF' [fix-libvirt FAIL] missing argument: Usage: sudo curl -fsSL https://install.mukenvault.com/fix-libvirt-smbios.sh | \ sudo bash -s -- List VMs to find the name: virsh list --all EOF exit 2 fi # ── Prerequisites ────────────────────────────────────────────────── command -v virsh >/dev/null 2>&1 || fail "virsh not found (apt install libvirt-clients)" command -v python3 >/dev/null 2>&1 || fail "python3 not found (apt install python3)" command -v uuidgen >/dev/null 2>&1 || fail "uuidgen not found (apt install uuid-runtime)" # Domain existence check. `virsh dominfo` exits non-zero for unknown VMs # and our `set -e` would then take us out — handle explicitly to give a # better error. if ! virsh dominfo "$VM" >/dev/null 2>&1; then fail "VM not found: '$VM'. Run 'virsh list --all' to see available names." fi BACKUP_DIR="/var/backups/mukenvault-libvirt-smbios" mkdir -p "$BACKUP_DIR" TS=$(date +%Y%m%d-%H%M%S) BACKUP_XML="${BACKUP_DIR}/${VM}-${TS}.xml" NEW_XML="$(mktemp /tmp/mukenvault-libvirt-smbios.XXXXXX.xml)" trap 'rm -f "$NEW_XML"' EXIT # ── 1. Backup current XML (inactive view; --security-info preserves any # passphrases inside, which we then write 0600 root-only). ────── log "backing up current domain XML → $BACKUP_XML" virsh dumpxml --inactive --security-info "$VM" > "$BACKUP_XML" chmod 600 "$BACKUP_XML" # ── 2. Shutdown if running ───────────────────────────────────────── STATE=$(virsh domstate "$VM" | tr -d '[:space:]') if [[ "$STATE" == "running" || "$STATE" == "paused" ]]; then log "VM state: $STATE → sending acpi shutdown (up to 60s)" virsh shutdown "$VM" >/dev/null 2>&1 || true for _ in $(seq 1 30); do STATE=$(virsh domstate "$VM" 2>/dev/null | tr -d '[:space:]' || true) # libvirt returns "shut off" (with a space). After tr -d it # becomes "shutoff" — match either form just in case. [[ "$STATE" == "shutoff" || "$STATE" == "shut off" ]] && break sleep 2 done STATE=$(virsh domstate "$VM" | tr -d '[:space:]') if [[ "$STATE" != "shutoff" ]]; then warn "graceful shutdown timed out — forcing destroy" virsh destroy "$VM" >/dev/null 2>&1 || true fi fi log "VM is stopped, editing domain XML" # ── 3. Edit XML in Python ────────────────────────────────────────── # The Python step reads the existing domain from the backup XML # and reuses it as the SMBIOS uuid (libvirt requires equality). Only the # serials are freshly generated per call. RAND=$(openssl rand -hex 3 2>/dev/null || head -c 3 /dev/urandom | xxd -p) SERIAL_SYS="MUKENVAULT-${TS}-${RAND}" SERIAL_BOARD="MUKENVAULT-BOARD-${TS}-${RAND}" SERIAL_CHASSIS="MUKENVAULT-CHASSIS-${TS}-${RAND}" # Python prints SMBIOS_UUID=... on stdout so bash can show it in the # summary block. Capture into a file so we still see errors live. PY_OUT=$(mktemp /tmp/mukenvault-libvirt-smbios.py-out.XXXXXX) trap 'rm -f "$NEW_XML" "$PY_OUT"' EXIT python3 - "$BACKUP_XML" "$NEW_XML" "$SERIAL_SYS" "$SERIAL_BOARD" "$SERIAL_CHASSIS" >"$PY_OUT" <<'PYEOF' import sys import xml.etree.ElementTree as ET src, dst, serial_sys, serial_board, serial_chassis = sys.argv[1:6] tree = ET.parse(src) root = tree.getroot() if root.tag != 'domain': print(f'unexpected root tag: {root.tag} (expected )', file=sys.stderr) sys.exit(1) # Read the existing domain . libvirt always populates this at # define time and requires the sysinfo system uuid to match. uuid_el = root.find('uuid') if uuid_el is None or not (uuid_el.text or '').strip(): print('domain XML has no — refusing to inject SMBIOS ' '(define the domain first)', file=sys.stderr) sys.exit(3) domain_uuid = uuid_el.text.strip() # 1) os_el = root.find('os') if os_el is None: print('domain XML has no element — refusing to inject SMBIOS', file=sys.stderr) sys.exit(2) smbios = os_el.find('smbios') if smbios is None: smbios = ET.SubElement(os_el, 'smbios') smbios.set('mode', 'sysinfo') # 2) with system/baseBoard/chassis sysinfo = None for s in root.findall('sysinfo'): if s.get('type') == 'smbios': sysinfo = s break if sysinfo is None: sysinfo = ET.SubElement(root, 'sysinfo') sysinfo.set('type', 'smbios') # Drop any existing system/baseBoard/chassis blocks — we own those. for tag in ('system', 'baseBoard', 'chassis'): for el in sysinfo.findall(tag): sysinfo.remove(el) def add_entry(parent, name, val): e = ET.SubElement(parent, 'entry') e.set('name', name) e.text = val system = ET.SubElement(sysinfo, 'system') add_entry(system, 'manufacturer', 'MukenVaultPartner') add_entry(system, 'product', 'PartnerVM') add_entry(system, 'version', '1.0') add_entry(system, 'serial', serial_sys) add_entry(system, 'uuid', domain_uuid) add_entry(system, 'sku', 'mukenvault') add_entry(system, 'family', 'MukenVault') board = ET.SubElement(sysinfo, 'baseBoard') add_entry(board, 'manufacturer', 'MukenVaultPartner') add_entry(board, 'product', 'PartnerBoard') add_entry(board, 'version', '1.0') add_entry(board, 'serial', serial_board) chassis = ET.SubElement(sysinfo, 'chassis') add_entry(chassis, 'manufacturer', 'MukenVaultPartner') add_entry(chassis, 'version', '1.0') add_entry(chassis, 'serial', serial_chassis) add_entry(chassis, 'asset', f'asset-{serial_chassis[-6:]}') tree.write(dst, encoding='utf-8', xml_declaration=False) print(f'SMBIOS_UUID={domain_uuid}') PYEOF # Surface the SMBIOS uuid python resolved so the summary can print it. SMBIOS_UUID=$(grep -oP '(?<=SMBIOS_UUID=).+' "$PY_OUT" || true) rm -f "$PY_OUT" # ── 4. Define new XML ───────────────────────────────────────────── log "applying new domain XML via 'virsh define'" virsh define "$NEW_XML" >/dev/null # ── 5. Start ────────────────────────────────────────────────────── log "starting VM" virsh start "$VM" >/dev/null # Wait for VM to be running. libvirt may briefly report transient # states (e.g. "in shutdown") before "running" stabilizes. for _ in $(seq 1 30); do STATE=$(virsh domstate "$VM" 2>/dev/null | tr -d '[:space:]' || true) [[ "$STATE" == "running" ]] && break sleep 1 done # ── Summary ─────────────────────────────────────────────────────── cat <