背景
一台全新的 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_store 时 virt-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 之间切换(选项只有
bios、efi)。 - Cockpit 没有 Secure Boot 开关。它其实解析了 libvirt 的
secure/readonlyloader 能力,但没有做进 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 内核默认不加载 mptspi,guest 里根本不出现 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 status 为 disabled、无 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 配置静态地址。
七、本次排障对宿主机的改动清单(供对照)
在新环境上按上文配置即可,无需重复以下“补救”操作。本次为修复现场做了:
- 把
/etc/libvirt/qemu.conf的security_driver = "none"恢复为默认(注释掉),并备份为qemu.conf.bak.<时间戳>;systemctl daemon-reload && systemctl restart virtqemud。 - 新建持久种子
/opt/data/cloud-seed/master-ci.iso(cidata,含 root 公钥/密码/PermitRootLogin yes),以 sata 挂到master-0001的sda,重启后 cloud-init 正常执行。 - 安装辅助脚本
/usr/local/bin/cloudvm-seed。 - 验证:
cloud-init status --long=done,密钥与密码登录均可用,ref_dac报错消失。 - 清理:删除排障用的临时脚本/测试虚拟机(
qa-ci)与残留 NBD 挂载,rmmod nbd。
总结
- 首选:新建云主机时用非 Secure Boot 的 UEFI / 关闭 TPM,并用 Cockpit 的自动化;或者干脆用持久种子 ISO,一步到位且不怕重启。
- 永远不要把 cloud-init 种子放在
/var/lib/libvirt/boot(那里是virt-install的临时目录,用完即删)。 - 不要关
security_driver;ref_dac报错指向悬空文件引用,清引用即可。 - 云镜像放
/var/lib/libvirt/images/,挂盘用sata,guest 内装qemu-guest-agent,再配合virsh domifaddr/ DHCP 保留管理地址。