PVE 下 Home Assistant OS 掉电内核损坏之无损恢复

2025-11-14
5分钟阅读时长
2293字
阅读
ha , pve , linux

PVE 玩久了总是要面对各种软硬件问题的, 这次就碰上了小区停电闪断, PVE 异常掉电导致 Home Assistant 虚拟机 内核损坏 无法启动的问题

问题

RT

没有现场截图了.

问题比较清晰, 突发的异常掉电导致了 VM 内的内核出现损坏, 无法正常驱动进入

方案

不外乎就2个选择:

  1. 重装VM, 数据从日常自动备份重新导入

    日常备份文件, 是经过加密处理的, 如果密钥保存不当, 或一时找不到密钥, 就比较尴尬

    而且, 全新重装导入, 很多非备份范畴的配置和修改, 就还得重新处理一遍

    费事费力, 且不能保证最新 (备份是周期计划性的, 肯定不如刚掉电前的状态新)

  2. 尝试修复内核

    不过没那么简单, 这得从 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

  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个分区覆盖重置就可以了

  1. 准备新的 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/nbd0p8 8个分区

其中旧磁盘的第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
Avatar
zfkun 喜欢游戏、热爱技术、追求艺术、崇尚自由、渴望精彩、最爱唠叨
下一页 AMS Lite Tiny