cockpit管理qemu虚拟机

十月 09, 2025 更新: 八月 10, 2026 [虚拟化与容器] #qemu #cockpit #debug

高效登录虚拟机GUI

使用virt-viewer客户端spice协议登录

apt install virt-viewer
virt-viewer --reconnect win11
virt-viewer --wait win11
virsh domdisplay win11

连接局域网的虚拟机

#1. 如果远程宿主机也在跑 libvirtd 并允许远程连接,你还可以直接用:
# 这样 virt-viewer 会先通过 libvirt 查询到 SPICE 端口,然后自动帮你连上去(不用手动写端口)。
# 通常推荐这种方式,安全性更好(走 SSH)
virt-viewer -c qemu+ssh://user@<宿主机IP>/system <VM名称或UUID>
virt-viewer -c qemu+ssh://peter@192.168.7.99/system win11

#2. 开放spice监听外部地址
# 修改listen='0.0.0.0' 表示允许外部访问(默认可能是 127.0.0.1,那就只能本机连)。
# port 可以固定(如 5901)或自动分配
sudo virsh list
sudo virsh dominfo win11
sudo grep -C 0 --group-separator="======" listen /etc/libvirt/qemu/win11.xml
sudo sed -i'' 's/127.0.0.1/0.0.0.0/' /etc/libvirt/qemu/win11.xml
# 一般需要配置密码,否则太不安全
## <graphics type='spice' port='5901' listen='0.0.0.0'>
##   <passwd>123</passwd>
## </graphics>
virt-viewer --spice-uri spice://192.168.7.99:5901 --spice-password 123

cat <<EOF > win11.vv
[virt-viewer]
type=spice
host=192.168.7.99
port=5901
password=123
delete-this-file=1
fullscreen=0
EOF
remote-viewer vm.vv
# 或者直接双击执行

# 可能的问题
# 音频/USB 不工作 SPICE 的辅助通道(如 5902)可能被防火墙阻断,需开放连续端口范围

启动问题排查

sudo systemctl status cockpit.service
# 2月 03 14:00:57 xiao99 cockpit-tls[2028483]: cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.
# 2月 03 14:01:06 xiao99 cockpit-tls[2028483]: cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.

# 另外网页会自动刷行页面,后续的cockpit.socket 会崩溃,出现另一个报错信息, 这个应该不是根因
# Dependency failed for cockpit.service - Cockpit Web Service.

gnutls-cli --insecure -p 9090 localhost
# Processed 0 CA certificate(s).
# Resolving 'localhost:9090'...
# Connecting to '127.0.0.1:9090'...
# *** Fatal error: Error in the pull function.

echo -e "GET / HTTP/1.0\r\n\r\n" | socat - OPENSSL:127.0.0.1:9090,verify=0
# 2026/02/03 14:18:00 socat[2106042] W refusing to set empty SNI host name
# 2026/02/03 14:18:00 socat[2106042] W SSL_connect(): Connection reset by peer

证书问题解决

sudo systemd-run \
  --unit=cockpit-cert-debug \
  --pty \
  -p RuntimeDirectory=cockpit/tls \
  -p WorkingDirectory=/ \
  /usr/lib/cockpit/cockpit-certificate-ensure --for-cockpit-tls
sudo systemctl reset-failed cockpit-debug.service
sudo systemd-run \
  --unit=cockpit-debug \
  --pty \
  -p WorkingDirectory=/run \
  -p RuntimeDirectory=cockpit/tls \
  -p DynamicUser=yes \
  -p User=cockpit-systemd-service \
  -p Group=cockpit-wsinstance-socket \
  -p NoNewPrivileges=yes \
  -p ProtectSystem=strict \
  -p PrivateDevices=yes \
  -p ProtectKernelTunables=yes \
  -p MemoryDenyWriteExecute=yes \
  -p PrivateIPC=yes \
  -p PrivateNetwork=yes \
  /usr/lib/cockpit/cockpit-tls

sudo systemctl enable --now \
  cockpit.socket \
  cockpit-wsinstance-http.socket \
  cockpit-wsinstance-https-factory.socket

虚拟机标签不显示

现象:登录 Cockpit 后左侧导航只有 概览/日志/存储/网络/用户账户/服务,没有「虚拟机」标签。

原因:未安装 cockpit-machines 包(虚拟机页面的功能由它提供,与 libvirt 本体无关)。

# 查看已装 cockpit 组件
dpkg -l | grep cockpit

sudo apt install cockpit-machines

安装后强制刷新浏览器(Ctrl+Shift+R)即可出现「虚拟机器」标签。 注意:Ubuntu 24.04 若 cockpit 核心来自 backports(如 362),建议一并安装 backports 版对齐版本,避免旧版页面与新版 bridge 不兼容:

sudo apt install cockpit-machines/noble-backports

登录断开:自签证书过期自动重新生成

现象:前一天还能打开 Cockpit,今天突然「已断开连接 / 服务器已关闭了连接」。服务器日志里大量:

cockpit-tls: gnutls_handshake failed: A TLS fatal alert has been received.

排查发现 /etc/cockpit/ws-certs.d/ 下的自签证书在当天被重新生成了(旧证书过期):

ls -la /etc/cockpit/ws-certs.d/
openssl x509 -in /etc/cockpit/ws-certs.d/0-self-signed.cert -noout -dates

浏览器缓存/信任的是旧证书,服务器换了新证书后 TLS 握手被浏览器直接掐断,表现就是「已断开连接」。这与上面 Debian 升级导致的证书问题根因不同(那次是升级破坏了证书文件,这次是正常到期轮换)。

解决(浏览器侧操作):

  1. 隐身窗口打开 https://<ip>:9090,出现"您的连接不是私密连接"时点 高级 → 继续前往
  2. 若曾把旧证书导入系统/浏览器信任库,删除旧记录
  3. 清除该站点的缓存/站点数据

服务端验证连通性:

curl -sk -o /dev/null -w 'HTTP %{http_code}\n' https://localhost:9090/

虚拟机页面「服务器已关闭了连接」:pybridge 单例缓存 bug

现象:其他标签都正常,唯独点「虚拟机」后整个会话断开,页面显示「已断开连接 / 服务器已关闭了连接」+ 重新连接按钮。每次点都崩,可稳定复现。

排查过程

  1. 服务器日志(journalctl)看到 bridge 崩溃:
journalctl -u cockpit-session@ -n 50
# cockpit.protocol.CockpitProtocolError: channel is already open
#   File ".../cockpit/router.py", line 281, in channel_control_received
# OSError: [Errno 22] sd_bus_attach_event: Invalid argument
  1. 检查运行中 bridge 的环境,发现缺少 XDG_RUNTIME_DIR 和 DBUS_SESSION_BUS_ADDRESS:
tr '\0' '\n' < /proc/<bridge-pid>/environ
# 只有 XDG_SESSION_CLASS/XDG_SESSION_TYPE/HOME/PATH
  1. 干净环境复现:sd_bus_default_user 报 No medium found(ENOMEDIUM)——web 会话没有 session bus。/etc/pam.d/cockpit 只有 pam_unix,没有 pam_systemd,所以 bridge 拿不到用户会话总线地址。

  2. 给 bridge 打临时日志补丁(router.py 记录重复打开的 channel id,dbus.py 记录打开的 bus/name),再触发一次崩溃,抓到决定性证据:

# DBG dbus-open channel=1:6!6 bus=session name=org.libvirt superuser=None
#   → attach_event EINVAL(异步异常,channel 残留在 open_channels,无 close 响应)
# DBG channel=1:6!6 already-open message={'bus': 'session', 'payload': 'dbus-json3', 'name': 'org.libvirt', ...}
#   → 页面超时后重试同一 channel id → CockpitProtocolError → bridge 死亡

根因

崩溃链完整还原:

  1. 会话内第一个 session bus 通道(如日志页的 org.freedesktop.systemd1)调用 sd_bus_default_user 失败(ENOMEDIUM),但 systemd_ctypes/bus.py 的 default_user() 先把空壳 Bus() 写入了单例缓存,失败后也不清除:
    if Bus._default_user_instance is None:
        Bus._default_user_instance = Bus()      # 先赋值(空壳!)
        Bus._default_user(byref(...))           # 失败时异常,但空壳已缓存
    
  2. 虚拟机页打开 org.libvirt session 通道(第二个 session 通道),拿到缓存的空 bus → attach_event(NULL) 返回 EINVAL → channel 半开残留在路由表
  3. 页面收不到 ready/close 响应,超时重试同一 channel id → router 抛 CockpitProtocolError('channel is already open') → 整个 bridge 进程崩溃 → 前端「服务器已关闭了连接」

修复

只把成功创建的连接写入缓存(default_user/default_system 同样处理):

@staticmethod
def default_user(attach_event: bool = True) -> 'Bus':
    if Bus._default_user_instance is None:
        instance = Bus()
        Bus._default_user(byref(instance))
        if attach_event:
            instance.attach_event(None, 0)
        Bus._default_user_instance = instance
    return Bus._default_user_instance

修复后 session 通道干净地返回 ChannelError,虚拟机页按设计回落到 system bus(superuser: "try"),虚拟机列表正常加载。

经验