#!/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 <