KVM虚拟机和Cockpit自动化失效踩坑记

注意,这是一篇由AI生成的问题分析报告。

背景

在一台Fedora 44的KVM宿主机(192.168.1.64, 使用cockpit管理)上创建了一个Fedora 44 Cloud版本的虚拟机master-0001。创建完成后发现:

  1. 不知道虚拟机的root密码,无法登录。
  2. 多次在Cockpit创建界面的"自动化"区域里指定root密码和SSH公钥,全部不生效。

虚拟机的IP为192.168.122.208(libvirt默认NAT网段192.168.122.0/24)。

排查过程

第一步:通过 guest agent 应急登录

virsh dumpxml master-0001 里发现virtio-serial通道的QEMU guest agent是state='connected',说明VM内agent正常运行,可以通过它直接在虚拟机内执行命令并重置密码。

验证agent在线:

[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-ping"}'
{"return":{}}

用 guest-exec 执行任意命令(注意:agent运行在受限SELinux域中,读/etc/shadow、写/root/.ssh会被拒绝):

[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-exec",
  "arguments":{"path":"/bin/sh","arg":["-c","cat /etc/os-release"],"capture-output":true}}'
{"return":{"pid":1083}}

# 稍等后用 guest-exec-status 取回输出
NAME="Fedora Linux"
VERSION="44 (Cloud Edition)"
RELEASE_TYPE=stable

重置root密码(密码参数需要base64编码):

[root@x-cluster ~]# PW=$(echo -n 'vmpass1234' | base64)
[root@x-cluster ~]# virsh qemu-agent-command master-0001 '{"execute":"guest-set-user-password",
  "arguments":{"username":"root","password":"'"$PW"'","crypted":false}}'
{"return":{}}

第二步:控制台自动登录注入SSH公钥

只设密码还不够:Fedora默认 PermitRootLogin prohibit-password,root不能通过SSH密码登录,密钥也不能直接写入(SELinux限制agent写/root/.ssh)。因此通过控制台登录获得完整root shell,再注入公钥:

  1. 用QEMU monitor截屏控制台画面:
[root@x-cluster ~]# virsh qemu-monitor-command master-0001 '{"execute":"human-monitor-command",
  "arguments":{"command-line":"screendump /tmp/screen.ppm"}}'
  1. 把PPM拿到本机转PNG后用tesseract OCR,确认是tty1文本登录界面:
Fedora Linux 44 (Cloud Edition)
Kernel 6.19.10-300.fc44.x86_64 on x86_64 (tty1)

ens3: 192.168.122.208 ...

fedora login: _
  1. 用 virsh send-key 逐个按键输入(要放慢速度、一次一个key,否则会丢键),登录后得到 [root@fedora ~]#:
[root@x-cluster ~]# virsh send-key master-0001 KEY_R; sleep 0.5
[root@x-cluster ~]# virsh send-key master-0001 KEY_O; sleep 0.5
... 
[root@x-cluster ~]# virsh send-key master-0001 KEY_ENTER
  1. 宿主机生成密钥对,用guest agent把公钥写到VM的/tmp/pubkey,再在控制台shell里安装:
[root@x-cluster ~]# ssh-keygen -t ed25519 -f /root/.ssh/vm_key -N '' -C 'vm-master-0001'
# 在控制台shell中执行(通过send-key单字符键入)
[root@fedora ~]# cat /tmp/pubkey > /root/.ssh/authorized_keys
[root@fedora ~]# echo 'PermitRootLogin yes' > /etc/ssh/sshd_config.d/99-root.conf
[root@fedora ~]# systemctl restart sshd

至此三种登录方式全部可用:

方式 命令
SSH密钥 ssh -i /root/.ssh/vm_key [email protected]
SSH密码 sshpass -p 'vmpass1234' ssh [email protected]
控制台 用 vmpass1234 登录

第三步:定位Cockpit自动化失效的根因

静态线索

  • virsh dumpxml master-0001 里没有任何cloud-init种子盘,只有一个vda系统盘;metadata标记为install_source_type=cloud。
  • VM内 cloud-init status 显示 disabled,/var/lib/cloud/data 不存在,无任何运行日志;连镜像默认的fedora用户(uid 1000)都没有创建 → 种子从未被注入。
  • Cockpit日志出现了一批报错:
内部错误:子进程报告(status=125):无法在 trusted.libvirt.security.ref_dac 上
获取 XATTR /var/lib/libvirt/boot/virtinst-hso34tm0-cloudinit.iso: 没有那个文件或目录
Unable to remove disk metadata on vm master-0001
  from /var/lib/libvirt/boot/virtinst-hso34tm0-cloudinit.iso (disk target sda)

挖出Cockpit的调用方式

cockpit-machines的后端嵌入在/usr/share/cockpit/machines/index.js.gz里(内嵌一段python),cloud源+启动时实际执行的是:

virt-install --cloud-init user-data=/tmp/cockpit-machines-XXXX-user-data ...

种子ISO由virt-install生成在/var/lib/libvirt/boot/virtinst-<随机>-cloudinit.iso。

用inotify实测复现真相

[root@x-cluster ~]# inotifywait -m -e create,delete /var/lib/libvirt/boot

观测结果(同一个repro VM)揭示了完整生命周期:

23:40:23 virtinst-dv_rozuk-cloudinit.iso CREATE     # ISO被创建
23:40:23 virtinst-dv_rozuk-cloudinit.iso OPEN        # libvirt处理(定义+启动域)
23:40:24 virtinst-dv_rozuk-cloudinit.iso DELETE      # 1秒后被virt-install删除!
23:40:24 virtinst-*-meta-data DELETE
23:40:24 virtinst-*-user-data DELETE

而域XML仍然引用这个已被删除的ISO路径:

<disk type='file' device='cdrom'>
  <source file='/var/lib/libvirt/boot/virtinst-dv_rozuk-cloudinit.iso'/>
  <target dev='sda' bus='sata'/>
</disk>

根因链条(用 audit 日志坐实):

以用户第二次创建(uuid 17a762e8)为例,journalctl 的 audit 记录是决定性的:

23:07:31  AUDIT2500 op=start reason=booted  ... master-0001   <-- 第一次启动,带 cloud-init 种子 ISO
23:07:40  Machine qemu-21-master-0001 terminated                    <-- 仅 9 秒后,guest 自我重启
23:07:41  Unable to remove disk metadata ... virtinst-qwf1a4bk-cloudinit.iso (disk target sda)
23:07:41  AUDIT2500 op=stop  reason=shutdown ...
23:07:41  AUDIT2500 op=start reason=booted  ... master-0001   <-- 第二次启动,已不含种子盘

链条如下:

  1. virt-install的Installer.start_install()先生成一个临时种子ISO放在/var/lib/libvirt/boot/,用它启动一个临时域;随后立刻defineXML()一个不含种子盘的持久postboot配置,并在_create_guest()之后执行_cleanup()把种子ISO、meta-data、user-data一并unlink()(installer.py:413-425)。
  2. Fedora Cloud 的 UEFI+SecureBoot(+TPM) 镜像首次启动时,shim 看到空的 EFI NVRAM,会调用 fallback.efi 填充启动项,并约 9 秒时触发一次自我重启(virt-manager 源码里专门有注释描述这个组合会"打乱安装流程")。
  3. 这次重启发生时 cloud-init 还没来得及运行(ds-identify 要到第 11 秒才跑)。重启后 guest 用的是持久配置——种子盘已经不在。
  4. 于是 cloud-init 在唯一一次带种子的启动里被跳过,之后每次启动都找不到数据源并被禁用;密码/公钥自然全部无效。
  5. ref_dac XATTR 报错只是 libvirt 在重启/关机释放这个已被删除的临时ISO时发出的清理失败,与 SELinux/DAC 开关无关。

元凶是「UEFI+TPM 首次启动的 9 秒自重启」叠加「virt-install 两阶段(initial 带种子 / final 不带种子)」,而不是简单的"种子被删"。

为什么持久种子能救

把种子ISO放到 /var/lib/libvirt/boot 之外并写进持久XML后,它会跨重启一直存在:即使 shim 第 9 秒自重启,第二次启动时 sr0 仍在,第 11 秒的 ds-identify 就能找到 cidata,cloud-init 正常执行。这也是本次修复旧 master-0001 时验证过的方式(cloud-init status --long = done,DataSourceNoCloud [seed=/dev/sr0])。

guest 内的铁证:cloud-init 从未运行

通过可用的 guest agent 进入虚拟机取证(guest-exec 执行命令):

cat /var/log/cloud-init.log         -> No such file(任何一次启动都没有产生日志)
/run/cloud-init/disabled            -> 存在
cat /run/cloud-init/ds-identify.log -> ISO9660_DEVS=   DSNAME=   ... Disabled cloud-init [1]
lsblk                               -> 只有 vda,没有 sr0/cdrom
getent passwd (uid>=1000)           -> 空(连镜像默认的 fedora 用户都没有)

ds-identify 在开机 11.45s 时找不到任何 ISO9660 数据源,于是把 cloud-init 整体禁用。结合主机侧持久XML里已经没有种子盘,可以确认 root 密码/公钥一次都没有到达 guest——不是 Cockpit 写错,而是种子从未被 cloud-init 消费。

隐藏的坑

云镜像放在了/root/下(权限700),qemu用户(uid 107)没有读权限,作为backing_store时virt-install直接报权限不够失败。镜像应放置到qemu可读的目录(如/var/lib/libvirt/images/)。

解决方案

应急手段(本次已生效)

  • guest agent重置密码 + 控制台注入SSH公钥 + 放开 PermitRootLogin yes,三种登录方式均可用。

根因修复(正确做法:清掉悬空引用 + 持久种子)

纠错:security_driver = "none" 关不掉 DAC

qemu.conf 里的 security_driver 只能控制 MAC 驱动(SELinux/AppArmor),无法关闭 DAC 驱动。libvirt 自己的配置注释就写明了(/etc/libvirt/qemu.conf:523-525):

# Notes: The DAC security driver is always enabled; as a result, the
# value of security_driver cannot contain "dac". ...

而 trusted.libvirt.security.ref_dac 正是 DAC 驱动记录磁盘属主引用计数的 XATTR。所以改成 "none" 并重启后,日志里 DAC 照常工作:先打印 Configured security driver "none" disables default policy to create confined guests,随后 audit 里依然是 model=dac,并继续报同款 XATTR/status=125。报错与 SELinux/DAC 开关无关,它是"文件已删、引用仍在"的症状。

真正相关的开关是 dynamic_ownership / remember_owner(qemu.conf:577-584),它们只控制 DAC 是否改属主/记属主;但正确做法是清掉悬空引用,而不是关闭安全机制。

步骤1:清掉悬空种子引用

[root@x-cluster ~]# virsh dumpxml master-0001 | grep -n cloudinit.iso   # 应无输出
# 若有,用 virsh edit 删除对应 <disk> 后 virsh define 重新定义

步骤2:改用持久种子 ISO(推荐)

种子 ISO 放在 /var/lib/libvirt/boot 之外,virt-install/Cockpit 不会删它;域 XML 引用的是真实存在的文件,DAC 驱动能正常读写其 XATTR:

[root@x-cluster ~]# mkdir -p /opt/data/cloud-seed
[root@x-cluster ~]# cat > /opt/data/cloud-seed/seed.yaml <<'EOF'
#cloud-config
disable_root: false
ssh_pwauth: true
users:
  - name: root
    ssh_authorized_keys:
      - ssh-ed25519 AAAA...root_pub_key...
chpasswd:
  list: |
    root:你的密码
  expire: false
EOF
[root@x-cluster ~]# dnf install -y cloud-utils
[root@x-cluster ~]# cloud-localds /opt/data/cloud-seed/master-ci.iso /opt/data/cloud-seed/seed.yaml
[root@x-cluster ~]# virsh attach-disk master-0001 /opt/data/cloud-seed/master-ci.iso \
    sda --type cdrom --mode readonly --targetbus sata --config
[root@x-cluster ~]# virsh destroy master-0001 && virsh start master-0001

踩坑:cdrom 的目标总线必须是 sata。 直接 virsh attach-disk ... sda 不加 --targetbus 时 libvirt 会挂到 scsi(lsilogic 控制器),而 Fedora Cloud 内核默认不加载 mptspi,guest 里根本不出现 sr0,cloud-init 依旧找不到数据源。用 --targetbus sata 后 lsblk 才会出现 sr0。

步骤3:guest 内验证

lsblk                    -> 出现 sr0(如 370K rom)
cloud-init status --long -> status: done, DataSourceNoCloud [seed=/dev/sr0]

一劳永逸:cloudvm-seed 辅助脚本

既然 Cockpit 的自动化在当前 UEFI+SecureBoot 组合下不可靠,可在宿主机放一个脚本,给任意(包括 Cockpit 刚建的)cloud 虚拟机补持久种子:

[root@x-cluster ~]# cloudvm-seed <vmname> '你的root密码' [ssh公钥文件]
# 生成 /opt/data/cloud-seed/<vmname>-seed.iso(卷标 cidata),以 sata 挂到 sda 并重启该域

等 60~90 秒后即可 ssh -i /root/.ssh/vm_key root@<VM的IP>。

另一种让 Cockpit 自动化直接生效的思路

9 秒自重启由 SecureBoot/TPM 的首次启动流程触发。若新建 VM 时改用非 SecureBoot 的 UEFI(或关闭 TPM),首次启动即不再自重启,virt-install 的临时种子就能在这次启动中被 cloud-init 消费,Cockpit 的“自动化”可望正常生效(可在创建向导的固件选项里调整,或把 <loader secure='yes'> 改为非 secboot 的 OVMF 一试)。

可选:只想止血时的正确 DAC 开关

若暂时不想重建种子,只想消除 XATTR 报错,可关掉 DAC 的属主记忆(不是 security_driver):

[root@x-cluster ~]# cp -a /etc/libvirt/qemu.conf /etc/libvirt/qemu.conf.bak.$(date +%Y%m%d%H%M%S)
[root@x-cluster ~]# sed -i 's|^#remember_owner = 1|remember_owner = 0|' /etc/libvirt/qemu.conf
[root@x-cluster ~]# systemctl restart virtqemud

权衡:会放弃 libvirt 对磁盘属主的记录/还原。单机可用;多租户/高安全场景请用持久种子并把安全驱动恢复默认。

关键命令速查

用途 命令
Guest agent在线检查 virsh qemu-agent-command <域> '{"execute":"guest-ping"}'
重置密码(密码需base64) virsh qemu-agent-command <域> '{"execute":"guest-set-user-password",...}'
转储域XML virsh dumpxml [--inactive] <域>
控制台截屏 virsh qemu-monitor-command <域> '{"execute":"human-monitor-command","arguments":{"command-line":"screendump /tmp/x.ppm"}}'
控制台按键 virsh send-key <域> KEY_R(逐个发送+延时,避免丢键)
监控种子ISO生命周期 inotifywait -m -e create,delete /var/lib/libvirt/boot

总结

  • 原因不在cloud镜像,也不是简单的"密码设置失败",而是UEFI+SecureBoot(+TPM)镜像首次启动约9秒时的自我重启,恰好卡在ds-identify(约11秒)之前,把这次唯一带种子的启动打断;而virt-install的种子ISO是临时的(_cleanup在域创建后立即unlink),持久XML又是不含种子的postboot配置——种子再也回不来,cloud-init于是永远找不到数据源。ref_dac XATTR报错只是libvirt始终启用的DAC驱动事后清理这个悬空文件时发出的症状,status=125掩盖了真实情况。
  • 纠错:security_driver="none" 只能关 MAC(SELinux/AppArmor),关不掉 DAC;"单机关 DAC"是个无效方案,正确的止血开关是 dynamic_ownership / remember_owner。
  • 排查思路的关键是:先用guest agent拿到VM内的执行权(guest-ping/guest-exec/guest-set-user-password)并用它读guest内证据,再用screendump+OCR+send-key打通控制台,最后用源码分析和inotify事件复现按下葫芦浮起瓢的完整生命周期。
  • 修复上,先清掉悬空引用,再用持久种子ISO(放在/var/lib/libvirt/boot之外)让cloud-init真正生效;把云镜像放到qemu可读的目录(如/var/lib/libvirt/images/)。