PVE 玩久了总是要面对各种软硬件问题的, 这次就碰上了小区停电闪断, PVE 异常掉电导致 Home Assistant 虚拟机 内核损坏 无法启动的问题
问题
RT
没有现场截图了.
问题比较清晰, 突发的异常掉电导致了 VM 内的内核出现损坏, 无法正常驱动进入
方案
不外乎就2个选择:
-
重装VM, 数据从日常自动备份重新导入
日常备份文件, 是经过加密处理的, 如果密钥保存不当, 或一时找不到密钥, 就比较尴尬
而且, 全新重装导入, 很多非备份范畴的配置和修改, 就还得重新处理一遍
费事费力, 且不能保证最新 (备份是周期计划性的, 肯定不如刚掉电前的状态新)
-
尝试修复内核
不过没那么简单, 这得从 Home Assistant OS 的特性说起
Home Assistant OS 分区结构------------------------- | Boot | ------------------------- | Kernel A | ------------------------- | System A | | | ------------------------- | Kernel B | ------------------------- | System B | | | ------------------------- | Bootstate | ------------------------- | Overlay | | | ... ------------------------- | Data | | | | | -------------------------系统分区相关分区基本都是只读的
数据分区是可读写的
🧩 Home Assistant OS 的实际磁盘布局(以 16.x 系列为例)
分区 用途 文件系统类型 1 EFI FAT32 2 bootloader FAT32 3 kernel A ext4 4 kernel B ext4 5 rootfs A squashfs 6 rootfs B squashfs 7 overlay(运行层) ext4 8 data(持久化配置区) ext4 或 overlayfs/ext4
动手
实在不想重装&配置 (可能是上年纪了把, 原本爱折腾的特质开始弱化了)
选择 方案2
要相信, 办法总是会比困难多的
思路
替换掉虚拟磁盘损坏的系统分区, 保留 Data 数据分区原样不动
前提
Data 数据分区没有一起损坏, 还可以正常挂载读取, 否则就只能靠 方案1 了
- 查看磁盘分区表
fdisk -l /var/lib/vz/images/101/vm-101-disk-0.raw
101是 HA 的虚拟机ID
输出正常 (万幸)
Disk /var/lib/vz/images/101/vm-101-disk-0.raw: 32 GiB, 34359738368 bytes, 67108864 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: gpt
Disk identifier: 9F098C70-481C-402E-9A1E-B755A4C55342
Device Start End Sectors Size Type
/var/lib/vz/images/101/vm-101-disk-0.raw1 2048 67583 65536 32M EFI System
/var/lib/vz/images/101/vm-101-disk-0.raw2 67584 116735 49152 24M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw3 116736 641023 524288 256M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw4 641024 690175 49152 24M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw5 690176 1214463 524288 256M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw6 1214464 1230847 16384 8M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw7 1230848 1427455 196608 96M Linux filesystem
/var/lib/vz/images/101/vm-101-disk-0.raw8 1427456 67108830 65681375 31.3G Linux filesystem
可以看出, 第8个分区是
Data数据分区, 前7个分区覆盖重置就可以了
- 准备新的 Home Assistant OS 虚拟机用于提取
访问 官方下载页面 下载 KVM/Proxmox 的镜像
并 SCP 到 PVE 主机, 用于新建导入
# 例如, 我重新下载的目前最新镜像: haos_ova-16.3.qcow2.xz
scp pve:/tmp/haos_ova-16.3.qcow2.xz haos_ova-16.3.qcow2.xz
登录 pve, 解压镜像
ssh pve
cd /tmp
xz -d haos_ova-16.3.qcow2.xz
创建虚拟机
- 节点: local
- VM ID: 系统指派即可 (比如
108) - BIOS:
OVMF (UEFI) - 磁盘: 不添加
- CPU, 内存, 网络 都随意
导入 HAOS 镜像
qm import /tmp/haos_ova-16.3.qcow2 108
导入成功后, 硬件列表里会显示新的未使用的磁盘
local:108/vm-108-disk-0.raw
添加磁盘
qm set 108 --scsihw virtio-scsi-pci --scsi0 local:vm-108-disk-0
也可以在管理页面操作添加
新建虚拟机不需要开机
一切继续, 准备正式操作
执行
停止虚拟机 (新旧2个虚拟机都要保证停止状态)
pct stop 101
pct stop 108
备份虚拟磁盘 (保险起见, 非必须)
cp --reflink=auto /var/lib/vz/images/101/vm-101-disk-0.raw /var/lib/vz/images/101/vm-101-disk-0.raw.bak
# 或用 dd(更可靠但慢)
# dd if=/var/lib/vz/images/101/vm-101-disk-0.raw of=/var/lib/vz/images/101/vm-101-disk-0.raw.bak bs=4M status=progress conv=fsync
加载 nbd 模块
modprobe nbd max_part=16
这会在
/dev/下创建/dev/nbd0~/dev/nbd15, 用于挂载 qcow2 分区参数 max_part 表示最多支持的分区数
挂载 旧磁盘 和 新磁盘
- 旧磁盘: /var/lib/vz/images/101/vm-101-disk-0.raw
- 新磁盘: /var/lib/vz/images/108/vm-108-disk-0.raw
qemu-nbd --connect=/dev/nbd0 /var/lib/vz/images/101/vm-101-disk-0.raw
qemu-nbd --connect=/dev/nbd1 /var/lib/vz/images/101/vm-101-disk-0.raw
查看分区状态
fdisk -l /dev/nbd0
fdisk -l /dev/nbd1
会分别列出对应磁盘的
/dev/nbd0p1~/dev/nbd0p88个分区其中旧磁盘的第8个分区 (
/dev/nbd0p8) 就是Data数据分区 (后续操作务必小心 不要覆盖这个分区)
克隆新磁盘前7个分区分别覆盖到旧磁盘对应分区
dd if=/dev/nbd1p1 of=/dev/nbd0p1 bs=4M status=progress
dd if=/dev/nbd1p2 of=/dev/nbd0p2 bs=4M status=progress
dd if=/dev/nbd1p3 of=/dev/nbd0p3 bs=4M status=progress
dd if=/dev/nbd1p4 of=/dev/nbd0p4 bs=4M status=progress
dd if=/dev/nbd1p5 of=/dev/nbd0p5 bs=4M status=progress
dd if=/dev/nbd1p6 of=/dev/nbd0p6 bs=4M status=progress
dd if=/dev/nbd1p7 of=/dev/nbd0p7 bs=4M status=progress
一定注意, 不要覆盖
/dev/nbd0p8
覆盖替换完成, 断开 NBD
qemu-nbd --disconnect /dev/nbd0
qemu-nbd --disconnect /dev/nbd1
结束, 启动原HA虚拟机即可恢复正常
脚本
折腾半天, 还是总结成脚本, 方便日后万一还需要的话, 快速处理
#!/bin/bash
# ha_restore_101_from_108.sh
# 使用说明: sudo ./ha_restore_101_from_108.sh
# 该脚本会:
# - 备份旧 VM101 的磁盘镜像(完整拷贝到 .bak)
# - 使用 qemu-nbd 连接 VM101(old) 和 VM108(new) 磁盘
# - 将 VM108 的 p1..p7 逐个 dd 覆盖到 VM101 的 p1..p7(不触碰 p8)
# - 断开 nbd 并尝试启动 VM101
#
# 请先确保 VM 101 和 VM 108 均为 stopped 状态,并且宿主机有足够磁盘空间做备份。
set -euo pipefail
######### 用户可修改的变量(如果你的路径不同,请改这里) #########
VM_OLD=101
VM_NEW=108
# 映像文件位置(Proxmox 默认路径)
IMG_OLD="/var/lib/vz/images/${VM_OLD}/vm-${VM_OLD}-disk-0.raw"
IMG_NEW="/var/lib/vz/images/${VM_NEW}/vm-${VM_NEW}-disk-0.raw"
# 备份文件(会占用与IMG_OLD相同空间)
BACKUP="${IMG_OLD}.backup.$(date +%s).raw"
# nbd 设备
NBD_OLD="/dev/nbd0"
NBD_NEW="/dev/nbd1"
#########################################################################
if [ "$(id -u)" -ne 0 ]; then
echo "请以 root 运行此脚本(sudo)。"
exit 1
fi
echo "==== 检查 VM 状态 ===="
qm status $VM_OLD || true
qm status $VM_NEW || true
echo "请确认两台 VM($VM_OLD 和 $VM_NEW)都已停止。若未停止请 Ctrl-C 并先停止它们。"
read -p "确认两台 VM 已停止并继续?(y/n) " yn
if [ "$yn" != "y" ]; then
echo "已取消"
exit 1
fi
# 检查镜像文件存在
for f in "$IMG_OLD" "$IMG_NEW"; do
if [ ! -e "$f" ]; then
echo "错误: 找不到镜像文件 $f"
exit 1
fi
done
echo "==== 备份旧磁盘(可能很大,耐心等待) ===="
echo "备份 $IMG_OLD -> $BACKUP"
# 先检查是否已经有足够空间(粗略检查)
avail=$(df --output=avail -k /var | tail -n1)
size_kb=$(du -k "$IMG_OLD" | cut -f1)
if [ "$avail" -lt "$size_kb" ]; then
echo "警告: 可用空间不足以保存完整备份 ($avail KB available, $size_kb KB needed)."
read -p "仍要继续备份吗?(y/n) " yn
if [ "$yn" != "y" ]; then
echo "已取消"
exit 1
fi
fi
# 做备份(使用 dd,可改为 cp 或其他)
dd if="$IMG_OLD" of="$BACKUP" bs=4M status=progress conv=fsync
echo "备份完成: $BACKUP"
echo "==== 加载 nbd 模块 ===="
modprobe nbd max_part=16
sleep 1
echo "==== 连接镜像为 nbd 设备 ===="
qemu-nbd --connect="$NBD_OLD" --format=raw "$IMG_OLD"
qemu-nbd --connect="$NBD_NEW" --format=qcow2 "$IMG_NEW"
sleep 1
echo "==== 列出分区(核对) ===="
fdisk -l "$NBD_OLD"
fdisk -l "$NBD_NEW"
echo "请再次确认:将覆盖 VM $VM_OLD 的 p1..p7(系统分区),**不会**覆盖 p8(data)。"
read -p "确认继续覆盖 p1..p7 吗?(y/n) " yn
if [ "$yn" != "y" ]; then
echo "取消操作,断开 nbd 并退出..."
qemu-nbd --disconnect "$NBD_OLD" || true
qemu-nbd --disconnect "$NBD_NEW" || true
exit 1
fi
echo "==== 开始按分区逐个 dd 覆盖(显示进度) ===="
for part in 1 2 3 4 5 6 7; do
src="${NBD_NEW}p${part}"
dst="${NBD_OLD}p${part}"
if [ ! -b "$src" ]; then
echo "错误: 源分区不存在: $src"
qemu-nbd --disconnect "$NBD_OLD" || true
qemu-nbd --disconnect "$NBD_NEW" || true
exit 1
fi
if [ ! -b "$dst" ]; then
echo "错误: 目标分区不存在: $dst"
qemu-nbd --disconnect "$NBD_OLD" || true
qemu-nbd --disconnect "$NBD_NEW" || true
exit 1
fi
echo "覆盖 p${part}: dd if=$src of=$dst ..."
dd if="$src" of="$dst" bs=4M status=progress conv=fsync || {
echo "错误: dd 失败 for p${part}"
qemu-nbd --disconnect "$NBD_OLD" || true
qemu-nbd --disconnect "$NBD_NEW" || true
exit 1
}
sync
done
echo "==== 完成覆盖,断开 nbd 设备 ===="
qemu-nbd --disconnect "$NBD_OLD"
qemu-nbd --disconnect "$NBD_NEW"
echo "==== 启动 VM $VM_OLD ===="
qm start $VM_OLD
echo "完成:已启动 VM $VM_OLD。请到 PVE 控制台观察启动过程与日志(或在 PVE 控制台查看串口台)。"
echo "若启动失败,请不要 panic,先将 $BACKUP 恢复为原始磁盘或联系我继续排查。"
exit 0