Máquinas Virtuales y Contenedores

Esta guía describe cómo crear y gestionar VMs de Home Assistant OS en Proxmox VE, incluyendo el script de creación automática y la gestión de red.

DNS estándar del proyecto: 8.8.8.8 como primario y 8.8.4.4 como secundario. Usa el mismo criterio en Proxmox, OpenWRT y HAOS para evitar discrepancias durante el arranque inicial.

Arquitectura de red

Internet (GCP)
       │
    ens4 (IP dinámica GCP)
       │
 Proxmox Host — NAT/MASQUERADE
       ├── vmbr0 (192.168.100.1/24) — red interna Proxmox
       │       │
       │   OpenWRT VM (VMID 200)
       │       ├── eth0 → WAN → 192.168.100.2/24
       │       └── br-lan → LAN → 10.10.10.2/24
       │
       └── vmbr1 (10.10.10.1/24) — red de clientes
               │
          VMs HAOS → IP 10.10.10.<VMID>

Regla de IPs: cada VM recibe la IP 10.10.10.<VMID>. El VMID debe estar entre 1 y 254.

Requisitos previos

Script de creación de VMs HAOS

Un único script interactivo que descarga HAOS, crea la VM, verifica la configuración de discos y configura la red al primer arranque. Si algo falla durante la creación, destruye la VM automáticamente para dejar el entorno limpio.

Instalar

cat > /usr/local/bin/HA-vm.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

log()  { echo "[INFO]  $*"; }
err()  { echo "[ERROR] $*" >&2; exit 1; }
warn() { echo "[WARN]  $*"; }
ok()   { echo "[OK]    $*"; }

[[ "$EUID" -ne 0 ]] && err "Ejecuta como root"

for cmd in curl jq wget xz pvesm qm python3; do
    command -v "$cmd" >/dev/null 2>&1 || err "Dependencia no encontrada: $cmd"
done

# ── Versión ──────────────────────────────────────────────────
log "Consultando última versión de HAOS..."
VER=$(curl -sf "https://api.github.com/repos/home-assistant/operating-system/releases/latest" \
    | jq -r '.tag_name' | sed 's/^v//')
[[ -z "$VER" || "$VER" == "null" ]] && err "No se pudo obtener la versión de HAOS."
log "Versión: $VER"

# ── Parámetros ───────────────────────────────────────────────
echo ""
echo "=== Crear VM de Home Assistant OS ==="
echo ""

pvesm status | grep -E '^(local|data)\s'
echo ""
read -rp "Storage para la VM [data]: " STORAGE
STORAGE="${STORAGE:-data}"
pvesm status | grep -qE "^${STORAGE}\s" || err "Storage '${STORAGE}' no encontrado."

echo ""
while true; do
    read -rp "VMID (1–254): " VMID
    [[ "$VMID" =~ ^[0-9]+$ ]]              || { warn "VMID debe ser numérico."; continue; }
    [[ "$VMID" -ge 1 && "$VMID" -le 254 ]] || { warn "VMID debe estar entre 1 y 254."; continue; }
    qm status "$VMID" &>/dev/null && { warn "VMID $VMID ya existe."; continue; }
    break
done

read -rp "Nombre de la VM: " NAME
[[ -z "$NAME" ]] && err "El nombre no puede estar vacío."
NAME="${NAME//[ _]/-}"

HA_IP="10.10.10.${VMID}"
HA_GW="10.10.10.2"
HA_DNS="8.8.8.8"
log "IP que se asignará: ${HA_IP}"

# ── Descarga ─────────────────────────────────────────────────
IMG_XZ="/tmp/haos_${VER}.qcow2.xz"
IMG_FILE="/tmp/haos_${VER}.qcow2"
IMG_URL="https://github.com/home-assistant/operating-system/releases/download/${VER}/haos_ova-${VER}.qcow2.xz"

VMID_CREATED=0
cleanup() {
    rm -f "$IMG_XZ" "$IMG_FILE"
    if [[ "$VMID_CREATED" -eq 1 ]]; then
        warn "Error durante la creación — destruyendo VM $VMID."
        qm stop "$VMID" &>/dev/null || true
        qm destroy "$VMID" --purge &>/dev/null || true
    fi
}
trap cleanup EXIT

log "Descargando HAOS ${VER}..."
wget -q --show-progress -O "$IMG_XZ" "$IMG_URL" || err "Fallo al descargar la imagen."
log "Descomprimiendo..."
xz -d "$IMG_XZ"
[[ -f "$IMG_FILE" ]] || err "La imagen no se descomprimió correctamente."
ok "Imagen lista."

# ── Crear VM ─────────────────────────────────────────────────
log "Creando VM $VMID ($NAME)..."
qm create "$VMID" \
    --name "$NAME" \
    --memory 4096 \
    --cores 2 \
    --net0 virtio,bridge=vmbr1 \
    --scsihw virtio-scsi-pci \
    --bios ovmf \
    --machine q35 \
    --serial0 socket \
    --vga serial0 \
    --agent enabled=1 \
    --tablet 0
VMID_CREATED=1

log "Añadiendo disco EFI (Secure Boot OFF)..."
qm set "$VMID" --efidisk0 "${STORAGE}:4,efitype=4m,pre-enrolled-keys=0"

log "Importando imagen HAOS en '${STORAGE}'..."
qm importdisk "$VMID" "$IMG_FILE" "$STORAGE"
rm -f "$IMG_FILE"

# Verificar que hay exactamente 1 disco sin adjuntar
UNUSED_COUNT=$(qm config "$VMID" | grep -cE '^unused[0-9]+: ' || true)
[[ "$UNUSED_COUNT" -eq 1 ]] || err "Se esperaba 1 disco importado sin adjuntar, se encontraron ${UNUSED_COUNT}."

DISK_VOLUME=$(qm config "$VMID" | awk -F': ' '/^unused0: /{print $2; exit}')
[[ -n "$DISK_VOLUME" ]] || err "No se encontró el disco importado (unused0)."

log "Adjuntando disco HAOS: $DISK_VOLUME"
qm set "$VMID" \
    --scsi0 "${DISK_VOLUME},discard=on,ssd=1" \
    --boot order=scsi0

# Verificar que no quedan discos sin adjuntar
REMAINING=$(qm config "$VMID" | grep -cE '^unused[0-9]+: ' || true)
[[ "$REMAINING" -eq 0 ]] || err "Quedan $REMAINING disco(s) sin adjuntar — revisar configuración."

ok "Discos:"
qm config "$VMID" | grep -E '^(efidisk|scsi|boot)'

# ── Arrancar ─────────────────────────────────────────────────
log "Arrancando VM $VMID..."
qm start "$VMID"

# ── Red ──────────────────────────────────────────────────────
log "Esperando QEMU guest agent (hasta 10 min — primer arranque resiza particiones)..."
TIMEOUT=600
ELAPSED=0
until qm guest cmd "$VMID" ping &>/dev/null; do
    sleep 5
    ELAPSED=$((ELAPSED + 5))
    [[ "$ELAPSED" -lt "$TIMEOUT" ]] || err "Timeout esperando el guest agent. Ejecuta HA-network.sh $VMID cuando la VM esté lista."
    log "  ... ${ELAPSED}s / ${TIMEOUT}s"
done
ok "Guest agent disponible."

log "Detectando interfaz de red..."
HA_IFACE=$(qm guest exec "$VMID" -- ha network info 2>/dev/null | python3 -c "
import json, sys
try:
    outer = json.load(sys.stdin)
    inner = json.loads(outer.get('out-data', '{}'))
    ifaces = inner.get('data', {}).get('interfaces', [])
    if isinstance(ifaces, list):
        for i in ifaces:
            if isinstance(i, dict) and i.get('type') == 'ethernet':
                print(i.get('interface', '')); sys.exit(0)
    elif isinstance(ifaces, dict):
        for name, i in ifaces.items():
            if isinstance(i, dict) and i.get('type') == 'ethernet':
                print(name); sys.exit(0)
except Exception:
    pass
" 2>/dev/null || true)

if [[ -z "$HA_IFACE" ]]; then
    warn "ha network info sin resultado, probando ip link..."
    HA_IFACE=$(qm guest exec "$VMID" -- ip -o link show 2>/dev/null | python3 -c "
import json, sys
try:
    outer = json.load(sys.stdin)
    for line in outer.get('out-data', '').splitlines():
        parts = line.split()
        if len(parts) >= 2:
            name = parts[1].rstrip(':')
            if name != 'lo':
                print(name); sys.exit(0)
except Exception:
    pass
" 2>/dev/null || true)
fi

[[ -n "$HA_IFACE" ]] || { warn "Interfaz no detectada, usando enp6s18"; HA_IFACE="enp6s18"; }
ok "Interfaz: $HA_IFACE"

log "Aplicando red: ${HA_IFACE} → ${HA_IP}/24 gw ${HA_GW} dns ${HA_DNS}"
RESULT=$(qm guest exec "$VMID" -- \
    ha network update "$HA_IFACE" \
    --ipv4-method static \
    --ipv4-address "${HA_IP}/24" \
    --ipv4-gateway "$HA_GW" \
    --ipv4-nameserver "$HA_DNS" 2>&1)

EXITCODE=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('exitcode',1))" 2>/dev/null || echo 1)
[[ "$EXITCODE" -eq 0 ]] || err "ha network update falló (exitcode $EXITCODE). Salida: $RESULT"

trap - EXIT

echo ""
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  VM creada y configurada"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo "  VMID        : $VMID"
echo "  Nombre      : $NAME"
echo "  Versión HAOS: $VER"
echo "  Storage     : $STORAGE"
echo "  Interfaz    : $HA_IFACE"
echo "  IP          : $HA_IP"
echo "  Gateway     : $HA_GW"
echo "  DNS         : $HA_DNS"
echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
echo ""
EOF

chmod +x /usr/local/bin/HA-vm.sh

Usar

bash /usr/local/bin/HA-vm.sh

Pide storage, VMID y nombre. La IP se asigna como 10.10.10.<VMID>. Si algo falla la VM se destruye sola para no dejar basura.

Script de recuperación de red

Útil si la VM ya existe y arrancada pero no tiene IP — por ejemplo tras un timeout del primer arranque, o para reaplicar la configuración de red en cualquier momento.

cat > /usr/local/bin/HA-network.sh << 'EOF'
#!/usr/bin/env bash
set -euo pipefail

log()  { echo "[INFO]  $*"; }
err()  { echo "[ERROR] $*" >&2; exit 1; }
warn() { echo "[WARN]  $*"; }
ok()   { echo "[OK]    $*"; }

[[ "$EUID" -ne 0 ]] && err "Ejecuta como root"

VMID="${1:-}"
[[ -n "$VMID" ]] || err "Uso: HA-network.sh <VMID>"

HA_IP="10.10.10.${VMID}"
HA_GW="10.10.10.2"
HA_DNS="8.8.8.8"

qm status "$VMID" | grep -q "status: running" || err "La VM $VMID no está arrancada."

log "Esperando QEMU guest agent en VM $VMID (hasta 10 min)..."
TIMEOUT=600
ELAPSED=0
until qm guest cmd "$VMID" ping &>/dev/null; do
    sleep 5
    ELAPSED=$((ELAPSED + 5))
    [[ "$ELAPSED" -lt "$TIMEOUT" ]] || err "Timeout esperando el guest agent de la VM $VMID."
    log "  ... ${ELAPSED}s / ${TIMEOUT}s"
done
ok "Guest agent disponible."

log "Detectando interfaz de red..."
HA_IFACE=$(qm guest exec "$VMID" -- ha network info 2>/dev/null | python3 -c "
import json, sys
try:
    outer = json.load(sys.stdin)
    inner = json.loads(outer.get('out-data', '{}'))
    ifaces = inner.get('data', {}).get('interfaces', [])
    if isinstance(ifaces, list):
        for i in ifaces:
            if isinstance(i, dict) and i.get('type') == 'ethernet':
                print(i.get('interface', '')); sys.exit(0)
    elif isinstance(ifaces, dict):
        for name, i in ifaces.items():
            if isinstance(i, dict) and i.get('type') == 'ethernet':
                print(name); sys.exit(0)
except Exception:
    pass
" 2>/dev/null || true)

if [[ -z "$HA_IFACE" ]]; then
    warn "ha network info sin resultado, probando ip link..."
    HA_IFACE=$(qm guest exec "$VMID" -- ip -o link show 2>/dev/null | python3 -c "
import json, sys
try:
    outer = json.load(sys.stdin)
    for line in outer.get('out-data', '').splitlines():
        parts = line.split()
        if len(parts) >= 2:
            name = parts[1].rstrip(':')
            if name != 'lo':
                print(name); sys.exit(0)
except Exception:
    pass
" 2>/dev/null || true)
fi

[[ -n "$HA_IFACE" ]] || { warn "Interfaz no detectada, usando enp6s18"; HA_IFACE="enp6s18"; }
ok "Interfaz: $HA_IFACE"

log "Aplicando red: ${HA_IFACE} → ${HA_IP}/24 gw ${HA_GW} dns ${HA_DNS}"
RESULT=$(qm guest exec "$VMID" -- \
    ha network update "$HA_IFACE" \
    --ipv4-method static \
    --ipv4-address "${HA_IP}/24" \
    --ipv4-gateway "$HA_GW" \
    --ipv4-nameserver "$HA_DNS" 2>&1)

EXITCODE=$(echo "$RESULT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('exitcode',1))" 2>/dev/null || echo 1)
[[ "$EXITCODE" -eq 0 ]] || err "ha network update falló (exitcode $EXITCODE). Salida: $RESULT"

ok "Red configurada: ${HA_IP}/24 en VM $VMID."
EOF

chmod +x /usr/local/bin/HA-network.sh
bash /usr/local/bin/HA-network.sh <VMID>
Recuperación estándar

Si HAOS se queda en Preparando Home AssistantHA-network.sh <VMID> desde el host Proxmox. La interfaz se detecta automáticamente; el gateway es siempre 10.10.10.2 y el DN S 8.8.8.8.

Solución: HAOS 17.x arranca pero no obtiene IP

Si la VM arranca pero no responde al ping ni aparece en la red, verifica:

  1. Discos residuales: Asegura que solo hay un disco scsi0 y un efidisk0. Si hay discos unused0, elimínalos antes de arrancar.
  2. Formato de disco: El disco principal debe ser .qcow2, no .raw. Usa qm importdisk con --format qcow2.
  3. Supervisor HAOS: El script ahora espera a que el supervisor esté listo antes de aplicar la red. HAOS 17.x tarda hasta 20 min en el primer arranque (resize de particiones + instalación de HA Core).

Comandos de diagnóstico:

# Ver discos de la VM
qm config <VMID> | grep -E '(efidisk|scsi|unused)'

# Verificar que el guest agent responde
qm guest cmd <VMID> ping

# Verificar que el supervisor HAOS esté listo
qm guest exec <VMID> -- ha supervisor info

# Aplicar red manualmente (si el script falló)
bash /usr/local/bin/HA-network.sh <VMID>

El script actualizado detecta la interfaz dinámicamente y espera hasta 10 minutos al guest agent + 10 minutos al supervisor HAOS.

Si HAOS se queda en Preparando Home Assistant o no responde al ping, ejecuta HA-network.sh <VMID> desde el host Proxmox. La interfaz se detecta automáticamente; el gateway es siempre 10.10.10.2 y el DNS 8.8.8.8.

Problema conocido — BdsDxe: Access Denied

Si la VM falla al arrancar con BdsDxe: Access Denied, el problema es Secure Boot activo. Solución:

qm set <VMID> --delete efidisk0
qm set <VMID> --efidisk0 <storage>:4,efitype=4m,pre-enrolled-keys=0

Contenedor LXC

Para crear un contenedor Debian desde línea de comandos:

pct create <ID> data:vztmpl/debian-13-standard_13.1-2_amd64.tar.zst \
  --hostname <nombre> \
  --cores 1 \
  --memory 512 \
  --swap 512 \
  --rootfs data:20 \
  --net0 name=eth0,bridge=vmbr0,ip=192.168.100.X/24,gw=192.168.100.1 \
  --net1 name=eth1,bridge=vmbr1,ip=10.10.10.X/24 \
  --nameserver 8.8.8.8 \
  --unprivileged 1 \
  --password <contraseña> \
  --start 1
Note

Usar siempre data como almacenamiento, nunca local o local-lvm.

Problema conocido — SSH en Debian 13

Por defecto Debian 13 no permite login de root por contraseña. Editar /etc/ssh/sshd_config y descomentar o modificar:

PermitRootLogin yes
PasswordAuthentication yes

Reiniciar SSH:

systemctl restart ssh

Ver también: OpenWRT en Proxmox, Configuración de Proxmox, Troubleshooting Proxmox