VPS安装后KVM/Cockpit虚拟机最佳实践

背景

一台全新的 Fedora VPS 装好 cockpit + cockpit-machines 后,用 Cockpit 面板创建 KVM 虚拟机是最省事的方式:点几下就能从云端镜像拉起一台虚拟机,还能在“自动化”里填 root 密码和 SSH 公钥。

但这条路在 UEFI + Secure Boot + TPM 的 Cloud 镜像上有一个很隐蔽的坑:Cockpit(底层是 virt-install --cloud-init)生成的是一份临时 cloud-init 种子盘,而 Fedora Cloud 镜像首次启动时 shim 会因空 NVRAM 在第 9 秒左右自我重启一次,恰好卡在 cloud-init 的 ds-identify(第 11 秒)之前。重启后 guest 用的是持久配置(不含种子盘),于是 cloud-init 永远找不到数据源,你填的密码/公钥全部不生效,Cockpit 事件里还会刷 trusted.libvirt.security.ref_dac 的 XATTR 报错。

本文给出「VPS 安装完毕后」的一整套配置与操作规范,让新建虚拟机一次成型。

相关排障过程见:KVM虚拟机和Cockpit自动化失效踩坑记

一、装机后的基础配置

1.1 安装软件包

# 虚拟化与面板
dnf install -y cockpit cockpit-machines \
               libvirt-daemon-kvm libvirt-client libvirt-daemon-config-network \
               qemu-kvm virt-install

# 生成 cloud-init 种子盘、离线检查镜像(任选,xorriso/genisoimage 至少一个)
dnf install -y cloud-utils xorriso
  • cloud-utils 提供 cloud-localds;若不想装,xorriso/genisoimage 也能手动生成种子 ISO。
  • 只是需要一个“不会被删”的种子盘,见第三节。

1.2 启用服务与默认 NAT 网络

# 面板
systemctl enable --now cockpit.socket
firewall-cmd --add-service=cockpit --permanent && firewall-cmd --reload   # 可访问 9090

# libvirt:模块化守护进程(推荐)或传统单体 libvirtd 二选一
systemctl enable --now virtqemud.socket virtnetworkd.socket
# 或者:systemctl enable --now libvirtd

# libvirt 默认 NAT 网络(192.168.122.0/24)务必开机自启
virsh net-start default 2>/dev/null || true
virsh net-autostart default
virsh net-list --all

1.3 云镜像放到 qemu 可读的目录

不要把镜像放在 /root/(权限 700),qemu 用户(通常是 qemu,uid 107)读不到,作为 backing_storevirt-install 会直接报“权限不够”。

mv /root/Fedora-Cloud-Base-*.qcow2 /var/lib/libvirt/images/
chmod 644 /var/lib/libvirt/images/*.qcow2
restorecon -Rv /var/lib/libvirt/images/

1.4 保持 libvirt 默认安全驱动,不要“关 DAC”

网上有“把 security_driver 改成 none 就能消除 XATTR 报错”的说法,这是错的

# /etc/libvirt/qemu.conf(libvirt 自带注释)
# The DAC security driver is always enabled; as a result, the
# value of security_driver cannot contain "dac".

security_driver 只控制 MAC(SELinux/AppArmor),DAC 驱动永远启用;trusted.libvirt.security.ref_dac 正是 DAC 驱动的记账 XATTR。看到 ref_dac 报错,说明域 XML 里引用了一个已被删除的种子 ISO,正确做法是清掉那个悬空引用,而不是关安全驱动。

# 保持默认(注释掉即可),不要写 security_driver = "none"
grep -nE '^#?security_driver' /etc/libvirt/qemu.conf

# 如遇 ref_dac 报错,先找悬空引用:
virsh dumpxml <vmname> | grep -n cloudinit.iso
# 有则 virsh edit 删掉对应 <disk> 后重新 define

二、用 Cockpit 创建虚拟机时的关键选项

2.1 固件 / Secure Boot:Cockpit 能做什么

先说结论(以 cockpit-machines-356 为例,扒过前端代码验证):

  • Cockpit 「Firmware(固件)」对话框,关机状态下可在 BIOS / UEFI 之间切换(选项只有 biosefi)。
  • Cockpit 没有 Secure Boot 开关。它其实解析了 libvirt 的 secure/readonly loader 能力,但没有做进 UI;UEFI 下 Secure Boot 由 libvirt/osinfo 按系统自动决定,通常默认开启

第 9 秒自重启正是 Secure Boot/TPM 的首次启动流程触发的。想让它不发生,只能绕过 UI 直接编辑域 XML(或换工具创建)。

2.1.1 用 XML 关闭 Secure Boot

在 VM 首次启动之前 virsh edit <vmname>,让 libvirt 自动挑非 secboot 的 OVMF:

<os firmware='efi'>
  <firmware>
    <feature enabled='no' name='secure-boot'/>
    <feature enabled='no' name='enrolled-keys'/>
  </firmware>
</os>

注意事项:

  • 若 XML 里已经写死<loader readonly='yes' secure='yes' type='pflash'>…/OVMF_CODE_4M.secboot.qcow2</loader> 和 secboot 的 <nvram>,上面的 <firmware> 特性会被显式 loader 覆盖。此时应删除显式的 <loader>/<nvram> 交给 libvirt 自动选,或改成非 secboot 的路径(本机为 /usr/share/edk2/ovmf/OVMF_CODE_4M.qcow2 + OVMF_VARS_4M.qcow2)。
  • 已生成的 secboot NVRAM(/var/lib/libvirt/qemu/nvram/<vm>_VARS.qcow2)与新固件不匹配,通常要删掉让它按新模板重建。
  • 已经启动过的虚拟机中途改固件容易出问题,最好在首次启动前改,或者干脆重建。

2.1.2 换用 virt-manager / virt-install

  • virt-manager(GUI,可 SSH X11 转发)在创建向导里有完整的固件 / Secure Boot 选项。

  • virt-install 可显式指定 firmware feature(--boot=? 里支持 firmware.feature[0-9]*.name/enabled):

    virt-install --name my-vm --import \
        --disk /var/lib/libvirt/images/my-vm.qcow2 \
        --boot uefi,firmware.feature0.name=secure-boot,firmware.feature0.enabled=no
    

其实只要用持久种子方案(第三节),Secure Boot 开不开都无所谓,不必非在 Cockpit 里把它关掉。关闭 Secure Boot 只是让 Cockpit 自带自动化能生效的“备选思路”。

2.2 使用 cloud 镜像时务必给 guest agent 留通道

Cockpit 的“设置 root 密码/发送按键/读 IP”等功能依赖 QEMU guest agent。cloud 镜像不一定默认安装并启动它,建议在 cloud-init 里补上(见第三节 packages:)。

# guest 内确认
systemctl is-active qemu-guest-agent
# 宿主机确认
virsh qemu-agent-command <vmname> '{"execute":"guest-ping"}'

2.3 cdrom 的目标总线用 sata

手动挂盘时(virsh attach-disk)若不指定总线,libvirt 会挂到 scsi(lsilogic 控制器),而 Fedora Cloud 内核默认不加载 mptspiguest 里根本不出现 sr0,cloud-init 也就找不到种子。指定 --targetbus sata

virsh attach-disk <vmname> /path/to/seed.iso sda \
    --type cdrom --mode readonly --targetbus sata --config

三、可靠方案:持久 cloud-init 种子

不管用不用 Cockpit,最稳的做法都是:把种子 ISO 放在 /var/lib/libvirt/boot 之外,写进持久 XML,让它跨重启一直存在

3.1 准备 seed.yaml

#cloud-config
disable_root: false
ssh_pwauth: true
packages:
  - qemu-guest-agent
users:
  - name: root
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...your_public_key... vps-vm
chpasswd:
  list: |
    root:YourStrongPassword
  expire: false
runcmd:
  - [ sh, -c, "echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/99-root.conf" ]
  - [ systemctl, restart, sshd ]
  - [ systemctl, enable, --now, qemu-guest-agent ]

3.2 生成种子 ISO 并挂载

# 方式 A:cloud-localds(推荐,自动带正确的 cidata 卷标)
cloud-localds /opt/data/cloud-seed/<vmname>-seed.iso /opt/data/cloud-seed/seed.yaml

# 方式 B:无 cloud-utils 时用 xorriso 手动生成(卷标必须是 cidata)
# cd /opt/data/cloud-seed && xorrisofs -output <vmname>-seed.iso -volid cidata -joliet -rock user-data meta-data

virsh attach-disk <vmname> /opt/data/cloud-seed/<vmname>-seed.iso sda \
    --type cdrom --mode readonly --targetbus sata --config
virsh destroy <vmname> && virsh start <vmname>

注意 xorriso 单独用时,user-data/meta-data 要放在 ISO 根目录,且卷标为 cidata,否则 NoCloud 数据源识别不到。

3.3 辅助脚本 cloudvm-seed

给(包括 Cockpit 刚创建的)任意 cloud 虚拟机一键补持久种子:

#!/bin/bash
# /usr/local/bin/cloudvm-seed
# 用法: cloudvm-seed <vmname> <root密码> [ssh公钥文件]
set -euo pipefail
VM="${1:?用法: cloudvm-seed <vmname> <root密码> [ssh公钥文件]}"
PW="${2:?缺少 root 密码}"
KEY="${3:-/root/.ssh/vm_key.pub}"
SEEDDIR=/opt/data/cloud-seed
mkdir -p "$SEEDDIR"
UD="$(mktemp)"; MD="$(mktemp)"
cat >"$UD" <<YAML
#cloud-config
disable_root: false
ssh_pwauth: true
packages:
  - qemu-guest-agent
users:
  - name: root
    ssh_authorized_keys:
      - $(cat "$KEY")
chpasswd:
  list: |
    root:${PW}
  expire: false
runcmd:
  - [ sh, -c, "echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/99-root.conf" ]
  - [ systemctl, restart, sshd ]
  - [ systemctl, enable, --now, qemu-guest-agent ]
YAML
echo "instance-id: ${VM}-$(date +%s)" >"$MD"
ISO="$SEEDDIR/${VM}-seed.iso"
xorrisofs -output "$ISO" -volid cidata -joliet -rock "$UD" "$MD" >/dev/null
rm -f "$UD" "$MD"
virsh detach-disk "$VM" sda --config >/dev/null 2>&1 || true
virsh attach-disk "$VM" "$ISO" sda --type cdrom --mode readonly --targetbus sata --config
virsh destroy "$VM" >/dev/null 2>&1 || true
virsh start "$VM" >/dev/null
echo "OK: 已挂载持久种子 $ISO 并重启 $VM"
chmod +x /usr/local/bin/cloudvm-seed
cloudvm-seed my-vm 'YourStrongPassword' /root/.ssh/vm_key.pub

脚本每次会写入新的 instance-id,便于重复下发;种子 ISO 位于 /opt/data/cloud-seed/,不会被任何人清理。

四、创建完成后的验证清单

# 1) 宿主机:确认 guest agent 在线
virsh qemu-agent-command <vmname> '{"execute":"guest-ping"}'

# 2) guest 内:确认种子被消费
lsblk | grep sr0                                  # 应出现 sr0
cloud-init status --long                          # status: done; DataSourceNoCloud [seed=/dev/sr0]

# 3) guest 内:确认用户/密钥/agent
id
systemctl is-active qemu-guest-agent

# 4) 宿主机:拿 IP 并登录(Libvirt NAT 的 IP 可能变化)
virsh domifaddr <vmname> --source agent
ssh -i /root/.ssh/vm_key root@<vm-ip>

五、常见故障对照表

现象 根因 处理
Cockpit 事件刷 无法在 trusted.libvirt.security.ref_dac 上获取 XATTR ... status=125 域 XML 引用了已被删除的临时 cloud-init ISO 清掉该 <disk> 引用;改用持久种子
填了 root 密码/公钥但登录不上;guest 内 cloud-init statusdisabled、无 sr0 种子盘没被 cloud-init 消费(UEFI+TPM 首次启动 9 秒自重启,临时种子被丢) 用持久种子并重启;或创建时关 Secure Boot/TPM
guest 内不出现 sr0 cdrom 挂到了 scsi(lsilogic),guest 无 mptspi virsh attach-disk ... --targetbus sata
virt-install 报 backing file “权限不够” 云镜像放在 /root(700) 移到 /var/lib/libvirt/images/restorecon
Cockpit 显示“虚拟机代理不响应:QEMU 客户机代理未连接” guest 内没装/没启动 qemu-guest-agent 在 cloud-init packages: 中加入并在 runcmd 里 enable
SSH 到旧 IP 失败 libvirt NAT 的 DHCP 租约变化 virsh domifaddr 重新取 IP;或配置 DHCP 保留/静态 IP
security_driver 改成 none 后报错依旧 该选项关不掉 DAC 驱动 恢复默认;从悬空引用入手

六、可选:固定虚拟机 IP

Libvirt NAT 默认在 192.168.122.0/24 动态分配,地址可能变化。需要固定时:

# 给某个 MAC 绑定固定 IP(改完重启 libvirt 网络)
virsh net-update default add ip-dhcp-host \
  "<host mac='52:54:00:xx:xx:xx' name='my-vm' ip='192.168.122.50'/>" \
  --live --config

或在 guest 内用 cloud-init/NetworkManager 配置静态地址。

七、本次排障对宿主机的改动清单(供对照)

在新环境上按上文配置即可,无需重复以下“补救”操作。本次为修复现场做了:

  1. /etc/libvirt/qemu.confsecurity_driver = "none" 恢复为默认(注释掉),并备份为 qemu.conf.bak.<时间戳>systemctl daemon-reload && systemctl restart virtqemud
  2. 新建持久种子 /opt/data/cloud-seed/master-ci.isocidata,含 root 公钥/密码/PermitRootLogin yes),以 sata 挂到 master-0001sda,重启后 cloud-init 正常执行。
  3. 安装辅助脚本 /usr/local/bin/cloudvm-seed
  4. 验证:cloud-init status --long = done,密钥与密码登录均可用,ref_dac 报错消失。
  5. 清理:删除排障用的临时脚本/测试虚拟机(qa-ci)与残留 NBD 挂载,rmmod nbd

总结

  • 首选:新建云主机时用非 Secure Boot 的 UEFI / 关闭 TPM,并用 Cockpit 的自动化;或者干脆用持久种子 ISO,一步到位且不怕重启。
  • 永远不要把 cloud-init 种子放在 /var/lib/libvirt/boot(那里是 virt-install 的临时目录,用完即删)。
  • 不要关 security_driverref_dac 报错指向悬空文件引用,清引用即可。
  • 云镜像放 /var/lib/libvirt/images/,挂盘用 sata,guest 内装 qemu-guest-agent,再配合 virsh domifaddr / DHCP 保留管理地址。