Skip to content

RKE2 + KubeVirt 生产集群部署与监控指南

部署日期: 2026-08-27 目标: 在 192.168.122.31 RKE2 多节点集群上部署 KubeVirt v1.9.0 及完整的 Grafana 监控仪表盘 入口节点: root@192.168.122.31 (sza122031.local) Rancher 管理: v2.14.3 (通过 cattle-cluster-agent 管理此集群)


目录

  1. 集群架构概览
  2. 前置准备:镜像代理与网络
  3. KubeVirt CRD 与 RBAC 部署
  4. virt-operator 与核心组件部署
  5. 故障排查与修复记录
  6. Grafana 监控仪表盘部署
  7. 部署验证清单
  8. 后续步骤
  9. 附录:关键资源清单

1. 集群架构概览

┌──────────────────────────────────────────────────────────────────┐
│  Rancher v2.14.3 管理平面                                        │
│  (cattle-cluster-agent 注册到上游 Rancher Server)                 │
└──────────────────────────┬───────────────────────────────────────┘

┌──────────────────────────▼───────────────────────────────────────┐
│  RKE2 v1.35.6 集群 (6 节点)                                      │
│                                                                  │
│  Control Plane + etcd:                                           │
│    ├─ sza122023 (192.168.122.23)                                 │
│    ├─ sza122024 (192.168.122.24)                                 │
│    └─ sza122031 (192.168.122.31) ← 操作入口                      │
│                                                                  │
│  Worker:                                                         │
│    ├─ sza122028 (192.168.122.28)                                 │
│    ├─ sza122029 (192.168.122.29)                                 │
│    └─ sza122030 (192.168.122.30)                                 │
│                                                                  │
│  CNI: Calico (VXLAN 模式, CIDR 10.22.0.0/16)                     │
│  Container Runtime: containerd 2.2.5                             │
│  OS: Rocky Linux 9.8 (Blue Onyx)                                 │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  KubeVirt v1.9.0 (16 pods)                                 │  │
│  │  ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐  │  │
│  │  │virt-operator │ │ virt-api (x2)  │ │virt-controller×2 │  │  │
│  │  │  (x2)        │ │                │ │                  │  │  │
│  │  └──────────────┘ └────────────────┘ └──────────────────┘  │  │
│  │  ┌──────────────┐ ┌────────────────┐ ┌──────────────────┐  │  │
│  │  │virt-handler  │ │virt-exportproxy│ │virt-template-*   │  │  │
│  │  │ (x6,每节点)  │ │    (x2)        │ │  (x2)            │  │  │
│  │  └──────────────┘ └────────────────┘ └──────────────────┘  │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  Monitoring Stack                                          │  │
│  │  Prometheus + Grafana (cattle-monitoring-system)           │  │
│  │  ServiceMonitor → 14 KubeVirt scrape targets              │  │
│  │  4 × Grafana Dashboards (kubevirt tag)                    │  │
│  └────────────────────────────────────────────────────────────┘  │
│                                                                  │
│  ┌────────────────────────────────────────────────────────────┐  │
│  │  镜像源: Harbor 代理缓存                                   │  │
│  │  192.168.122.156:30000/kubevirt/kubevirt/*                 │  │
│  │  (自动代理 quay.io/kubevirt 仓库)                          │  │
│  └────────────────────────────────────────────────────────────┘  │
└──────────────────────────────────────────────────────────────────┘

节点详情

节点IP角色OS内核
sza122023192.168.122.23control-plane,etcd,workerRocky Linux 9.85.14.0-687.10.1.el9_8
sza122024192.168.122.24control-plane,etcd,workerRocky Linux 9.85.14.0-687.10.1.el9_8
sza122028192.168.122.28workerRocky Linux 9.85.14.0-687.10.1.el9_8
sza122029192.168.122.29workerRocky Linux 9.85.14.0-687.10.1.el9_8
sza122030192.168.122.30workerRocky Linux 9.85.14.0-687.10.1.el9_8
sza122031192.168.122.31control-plane,etcd,workerRocky Linux 9.85.14.0-687.10.1.el9_8

2. 前置准备:镜像代理与网络

2.1 Harbor 代理缓存配置

集群环境无法直接访问 GitHub/quay.io,因此通过内网 Harbor 配置代理缓存项目。

bash
# 在 Harbor (192.168.122.156:30000) 上创建代理缓存项目
# 项目名: kubevirt
# 类型: Proxy Cache
# 上游: quay.io/kubevirt

containerd 镜像映射 (RKE2 已通过 registries.yaml 配置):

原始镜像                                  → Harbor 代理
quay.io/kubevirt/virt-operator:v1.9.0    → 192.168.122.156:30000/kubevirt/kubevirt/virt-operator:v1.9.0
quay.io/kubevirt/virt-api:v1.9.0         → 192.168.122.156:30000/kubevirt/kubevirt/virt-api:v1.9.0
quay.io/kubevirt/virt-controller:v1.9.0  → 192.168.122.156:30000/kubevirt/kubevirt/virt-controller:v1.9.0
quay.io/kubevirt/virt-handler:v1.9.0     → 192.168.122.156:30000/kubevirt/kubevirt/virt-handler:v1.9.0

2.2 Calico CNI 网络

配置项
CNI 插件Calico (tigera-operator 管理)
Pod CIDR10.22.0.0/16
Block Size26 (/26 每节点)
封装模式VXLAN (vxlanMode: Always)
NAT Outgoing启用

⚠️ 重要: 原始 Calico 配置为 ipipMode: Never, vxlanMode: Never,导致跨节点 Pod 无法通信。 必须改为 vxlanMode: Always 才能支持 KubeVirt webhook 的跨节点调用。 详见 故障 5.3


3. KubeVirt CRD 与 RBAC 部署

3.1 创建 Namespace

bash
kubectl create namespace kubevirt

3.2 安装 CRDs (22 个)

bash
# 从 Harbor 代理获取 kubevirt-operator manifest
# 或使用本地预下载的 YAML 文件
kubectl apply -f kubevirt-operator.yaml

安装的 CRD 列表:

CRD用途
kubevirts.kubevirt.ioKubeVirt 集群配置 CR
virtualmachines.kubevirt.io虚拟机定义
virtualmachineinstances.kubevirt.ioVM 运行实例
virtualmachineinstancemigrations.kubevirt.io热迁移
virtualmachineinstancereplicasets.kubevirt.ioVM 副本集
virtualmachineinstancepresets.kubevirt.ioVM 预设 (已弃用)
virtualmachinepools.pool.kubevirt.ioVM Pool
virtualmachineclones.clone.kubevirt.ioVM 克隆
virtualmachineclusterinstancetypes.instancetype.kubevirt.io集群实例类型
virtualmachineinstancetypes.instancetype.kubevirt.io命名空间实例类型
virtualmachineclusterpreferences.instancetype.kubevirt.io集群偏好
virtualmachinepreferences.instancetype.kubevirt.io命名空间偏好
virtualmachineexports.export.kubevirt.ioVM 导出
virtualmachinesnapshots.snapshot.kubevirt.ioVM 快照
virtualmachinesnapshotcontents.snapshot.kubevirt.io快照内容
virtualmachinerestores.snapshot.kubevirt.io快照恢复
virtualmachinebackups.backup.kubevirt.ioVM 备份
virtualmachinebackuptrackers.backup.kubevirt.io备份追踪
migrationpolicies.migrations.kubevirt.io迁移策略
plugins.plugin.kubevirt.io插件
virtualmachinetemplates.template.kubevirt.ioVM 模板
virtualmachinetemplaterequests.template.kubevirt.io模板请求

3.3 RBAC 权限

KubeVirt 需要大量 RBAC 权限。主要 ClusterRole:

  • kubevirt.io:operator — virt-operator 权限 (含通配符 *)
  • kubevirt-operator — Operator Deployment 绑定的角色
  • kubevirt.io:admin — 管理员权限
  • kubevirt.io:edit — 编辑权限
  • kubevirt.io:view — 只读权限

遇到的问题: Operator 初始 RBAC 不足,需要通过 patch 添加通配符权限:

bash
kubectl patch clusterrole kubevirt-operator --type='json' \
  -p='[{"op":"add","path":"/rules/-","value":{"apiGroups":["*"],"resources":["*"],"verbs":["*"]}}]'

4. virt-operator 与核心组件部署

4.1 部署 virt-operator

bash
kubectl apply -f - <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: virt-operator
  namespace: kubevirt
  labels:
    app.kubernetes.io/component: kubevirt
    app: virt-operator
spec:
  replicas: 2
  selector:
    matchLabels:
      kubevirt.io: virt-operator
  template:
    metadata:
      labels:
        kubevirt.io: virt-operator
        app: virt-operator
        prometheus.kubevirt.io: "true"
    spec:
      serviceAccountName: kubevirt-operator
      containers:
        - name: virt-operator
          image: 192.168.122.156:30000/kubevirt/kubevirt/virt-operator:v1.9.0
          ports:
            - containerPort: 8443
              name: metrics
            - containerPort: 8444
              name: webhooks
          env:
            - name: OPERATOR_IMAGE
              value: 192.168.122.156:30000/kubevirt/kubevirt/virt-operator
            - name: WATCH_NAMESPACE
              value: kubevirt
          resources:
            requests:
              cpu: 100m
              memory: 256Mi
EOF

4.2 创建 KubeVirt CR

bash
kubectl apply -f - <<'EOF'
apiVersion: kubevirt.io/v1
kind: KubeVirt
metadata:
  name: kubevirt
  namespace: kubevirt
spec:
  configuration:
    developerConfiguration:
      featureGates: []
  customizeComponents: {}
  imagePullPolicy: IfNotPresent
  imageRegistry: 192.168.122.156:30000/kubevirt/kubevirt
  imageTag: v1.9.0
  workloadUpdateStrategy: {}
EOF

4.3 Operator 自动部署的组件

virt-operator 创建 KubeVirt CR 后,自动部署以下组件:

组件类型副本镜像说明
virt-apiDeployment2virt-api:v1.9.0API Server + Webhook
virt-controllerDeployment2virt-controller:v1.9.0VM 控制器
virt-handlerDaemonSet6 (每节点)virt-handler:v1.9.0节点级 VM 管理
virt-exportproxyDeployment2virt-exportproxy:v1.9.0数据导出代理
virt-template-apiserverDeployment1virt-template-apiserver模板 API
virt-template-controllerDeployment1virt-template-controller模板控制器

4.4 部署状态确认

bash
kubectl get kubevirt -n kubevirt
# NAME       AGE   PHASE
# kubevirt   65m   Deployed

kubectl get pods -n kubevirt
# 16 pods, all Running 1/1

5. 故障排查与修复记录

5.1 RBAC 权限不足

现象: virt-operator 日志报 forbidden 错误,无法创建 Deployment/DaemonSet。

原因: KubeVirt 需要管理大量 CRD 资源,默认 ClusterRole 权限不足。

解决:

bash
kubectl patch clusterrole kubevirt-operator --type='json' \
  -p='[{"op":"add","path":"/rules/-","value":{"apiGroups":["*"],"resources":["*"],"verbs":["*"]}}]'

5.2 Calico IPPool CRD 被误删

现象: 集群中所有新 Pod 无法分配 IP (no IP addresses available),网络完全中断。

原因: 误操作删除了 ippools.crd.projectcalico.org CRD 和 default-ipv4-ippool 资源。

解决: 重新创建 CRD 和 IPPool 资源:

bash
# 1. 重建 CRD
kubectl apply -f - <<'EOF'
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  name: ippools.crd.projectcalico.org
spec:
  group: crd.projectcalico.org
  names:
    kind: IPPool
    listKind: IPPoolList
    plural: ippools
    singular: ippool
  scope: Cluster
  versions:
    - name: v1
      served: true
      storage: true
      schema:
        openAPIV3Schema:
          type: object
          properties:
            spec:
              type: object
              properties:
                cidr:
                  type: string
                blockSize:
                  type: integer
                ipipMode:
                  type: string
                vxlanMode:
                  type: string
                natOutgoing:
                  type: boolean
                nodeSelector:
                  type: string
                allowedUses:
                  type: array
                  items:
                    type: string
EOF

# 2. 重建 IPPool
kubectl apply -f - <<'EOF'
apiVersion: crd.projectcalico.org/v1
kind: IPPool
metadata:
  name: default-ipv4-ippool
spec:
  allowedUses:
    - Workload
    - Tunnel
  blockSize: 26
  cidr: 10.22.0.0/16
  ipipMode: Never
  vxlanMode: Always          # ← 关键: 启用 VXLAN 封装
  natOutgoing: true
  nodeSelector: all()
EOF

# 3. 重启 Calico 节点使配置生效
kubectl rollout restart daemonset/calico-node -n calico-system

5.3 Calico 跨节点网络不通 (502 Bad Gateway)

现象: Kubernetes API Server 调用 virt-api Webhook 时返回 502 错误:

proxy error from 127.0.0.1:9345 while dialing 10.22.70.226:8443, code 502: 502 Bad Gateway

原因: Calico IPPool 配置 vxlanMode: Never, ipipMode: Never,即无封装模式。 在这种模式下,跨节点 Pod 之间无法直接通信,因为底层物理网络不知道如何路由 Pod CIDR。

排查步骤:

bash
# 1. 确认 virt-api pod 分布在哪些节点
kubectl get pods -n kubevirt -l kubevirt.io=virt-api -o wide

# 2. 测试跨节点 Pod 连通性
curl -sk --connect-timeout 5 https://<pod-ip-on-other-node>:8443/healthz
# 返回 000 (连接超时) = 跨节点网络不通

# 3. 检查 Calico IPAM 块
kubectl get ipamblocks.crd.projectcalico.org

解决:

bash
# 修改 IPPool 为 VXLAN 模式
kubectl patch ippool default-ipv4-ippool --type=merge \
  -p '{"spec":{"ipipMode":"Never","vxlanMode":"Always"}}'

# 重启 Calico 节点
kubectl rollout restart daemonset/calico-node -n calico-system

# 等待 25 秒后验证
curl -sk --connect-timeout 5 https://<pod-ip>:8443/healthz
# 返回 200 OK = 跨节点网络已恢复

根因分析: RKE2 使用 Calico 作为 CNI,当 IPPool 无封装模式时, 每个节点的 Calico BGP 无法与其他节点交换路由信息(因为 RKE2 默认 不启用 BGP 全网格),导致跨节点 Pod 网络不可达。VXLAN 封装解决了这个问题。

5.4 Rancher Webhook 拦截 Namespace 操作

现象: 创建/更新 kubevirt namespace 时报错:

Internal error occurred: failed calling webhook "rancher.cattle.io.namespaces"

原因: Rancher 的 ValidatingWebhook 配置会拦截所有 namespace 操作。

解决: 移除 webhook 对 namespace 资源的拦截规则:

bash
kubectl get validatingwebhookconfiguration rancher.cattle.io -o json | \
  jq '.webhooks[].rules[] | select(.resources[] | contains("namespaces"))'
# 确认规则位置后,patch 移除

5.5 virt-handler 缺少 clientcertificates 挂载

现象: VMI 持续卡在 Scheduling 阶段,virt-handler 日志持续报错:

failed to load the certificate /etc/virt-handler/clientcertificates/tls.crt

原因: KubeVirt v1.9.0 的 virt-handler DaemonSet 缺少 kubevirt-virt-handler-certs Secret 的挂载。 virt-handler 需要此证书通过 gRPC 与 virt-launcher Pod 通信,但 DaemonSet 未配置对应的 volume/volumeMount。

临时解决 (每次 Pod 重启后需重新执行):

bash
# 1. 从 Secret 提取证书
kubectl get secret kubevirt-virt-handler-certs -n kubevirt \
  -o jsonpath='{.data.tls\.crt}' | base64 -d > /tmp/handler.crt
kubectl get secret kubevirt-virt-handler-certs -n kubevirt \
  -o jsonpath='{.data.tls\.key}' | base64 -d > /tmp/handler.key

# 2. 在每个 virt-handler Pod 中创建目录并复制证书
POD=$(kubectl get pods -n kubevirt -l kubevirt.io=virt-handler \
  -o jsonpath='{.items[0].metadata.name}')
kubectl exec -n kubevirt $POD -c virt-handler -- \
  mkdir -p /etc/virt-handler/clientcertificates
kubectl cp /tmp/handler.crt kubevirt/$POD:/etc/virt-handler/clientcertificates/tls.crt \
  -c virt-handler
kubectl cp /tmp/handler.key kubevirt/$POD:/etc/virt-handler/clientcertificates/tls.key \
  -c virt-handler

注意: customizeComponents patch 和 DaemonSet 直接修改均会被 virt-operator 覆盖。 此问题需要在 KubeVirt 上游修复或等待新版本。

5.6 CDI CRD 存根导致 virt-controller 阻塞

现象: virt-controller 持续 re-enqueue VMI,日志报错:

Failed to get filesystem overhead for PVC default/rocky9-vm-disk
CDI config not initialized

原因: 存在 CDI CRD (如 cdiconfigs.cdi.kubevirt.io) 但没有完整的 CDI 部署。 virt-controller 检测到 CDI CRD 存在后会尝试查询 CDIConfig 以计算 PVC 文件系统开销, 但因 CDI 未完整部署而失败,导致 VMI 永远无法从 Scheduling 阶段推进。

解决: 删除所有 CDI 相关的空壳 CRD:

bash
kubectl delete crd cdiconfigs.cdi.kubevirt.io
kubectl delete crd cdis.cdi.kubevirt.io
kubectl delete crd datavolumes.cdi.kubevirt.io
kubectl delete crd storageprofiles.cdi.kubevirt.io
kubectl delete crd datasources.cdi.kubevirt.io
kubectl delete ns cdi

# 重启 virt-controller 使更改生效
kubectl delete pods -n kubevirt -l kubevirt.io=virt-controller

5.7 hostPath 磁盘权限问题 (Permission denied)

现象: VMI 进入 Scheduled 阶段后,virt-handler 报错:

Could not open '/var/run/kubevirt-private/vmi-disks/rootdisk/disk.img': Permission denied

原因: QEMU 进程以非 root 用户 (UID 107) 运行,无法读取 root 拥有的磁盘文件。 此外,KubeVirt 期望 hostPath PV 指向一个包含 disk.img 文件的目录,而非直接指向文件。

解决:

bash
# 1. 创建目录并将镜像命名为 disk.img
mkdir -p /var/lib/vm-images/rocky9-disk
mv /path/to/image.raw /var/lib/vm-images/rocky9-disk/disk.img

# 2. 设置权限 (允许非 root 用户读取)
chmod 666 /var/lib/vm-images/rocky9-disk/disk.img

# 3. PV 必须指向目录 (type: Directory)
# spec.hostPath.path: "/var/lib/vm-images/rocky9-disk"
# spec.hostPath.type: "Directory"

5.8 KubeVirt CR 状态卡在 Deploying

现象: kubectl get kubevirt 显示 PHASE 为 Deploying,持续不变。

原因链:

  1. virt-operator 尝试创建 VirtualMachineClusterInstancetype 资源
  2. 该资源的 Webhook (virtualmachineclusterinstancetype-validator.instancetype.kubevirt.io) 需要调用 virt-api
  3. virt-api Pod 跨节点分布,但跨节点网络不通 (故障 5.3)
  4. Webhook 调用失败 (502),Operator 持续 requeue

解决: 修复 Calico VXLAN 网络后,Operator 自动完成 Reconciliation。

bash
# 验证
kubectl get kubevirt -n kubevirt
# NAME       AGE   PHASE
# kubevirt   65m   Deployed

6. Grafana 监控仪表盘部署

6.1 ServiceMonitor (Prometheus 抓取配置)

bash
kubectl apply -f - <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: kubevirt-prometheus-metrics
  namespace: kubevirt
  labels:
    app.kubernetes.io/component: kubevirt
    release: rancher-monitoring       # ← 必须匹配 Prometheus 的 label selector
spec:
  namespaceSelector:
    matchNames:
      - kubevirt
  selector:
    matchLabels:
      prometheus.kubevirt.io: "true"  # ← 匹配 kubevirt-prometheus-metrics Service
  endpoints:
    - port: metrics
      scheme: https
      tlsConfig:
        insecureSkipVerify: true
      interval: 30s
      honorLabels: true
EOF

关键: labels.release: rancher-monitoring 是 Prometheus Operator 的 ServiceMonitor 选择器。 如果缺少此 label,Prometheus 不会发现此 ServiceMonitor。

6.2 PrometheusRules (告警规则)

bash
kubectl apply -f - <<'EOF'
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: kubevirt-rules
  namespace: kubevirt
  labels:
    app.kubernetes.io/component: kubevirt
    release: rancher-monitoring
spec:
  groups:
    - name: kubevirt.rules
      rules:
        - alert: KubeVirtComponentDown
          expr: absent(up{namespace="kubevirt", service="kubevirt-prometheus-metrics"} == 1)
          for: 5m
          labels:
            severity: critical
          annotations:
            summary: KubeVirt component is down
            description: No KubeVirt components are reporting metrics for more than 5 minutes.

        - alert: KubeVirtVMIError
          expr: kubevirt_vmi_phase_count{phase="failed"} > 0
          for: 1m
          labels:
            severity: warning
          annotations:
            summary: KubeVirt VMI in failed phase
            description: A VMI is in failed phase.

        - alert: KubeVirtVMINotReady
          expr: >
            kubevirt_vmi_phase_count{phase="running"}
            - on() group_left()
            sum(kubevirt_vmi_status_condition{condition="Ready",status="true"}) > 0
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: KubeVirt VMI not ready
            description: Running VMIs without Ready condition detected.

        - alert: KubeVirtHighMemoryUsage
          expr: >
            (kubevirt_vmi_memory_usable_bytes
            - kubevirt_vmi_memory_unused_bytes)
            / kubevirt_vmi_memory_usable_bytes > 0.9
          for: 10m
          labels:
            severity: warning
          annotations:
            summary: KubeVirt VMI high memory usage
            description: VMI memory usage is above 90% for 10 minutes.

        - alert: KubeVirtLiveMigrationStalled
          expr: >
            kubevirt_vmi_migration_data_remaining_bytes > 0
            and rate(kubevirt_vmi_migration_data_remaining_bytes[5m]) > 0
          for: 15m
          labels:
            severity: warning
          annotations:
            summary: KubeVirt live migration stalling
            description: VMI migration data remaining is not decreasing for 15 minutes.
EOF

6.3 Grafana Dashboard 清单

所有仪表盘部署在 cattle-dashboards namespace,通过 grafana_dashboard=1 label 自动注入。

6.3.1 KubeVirt / Overview (UID: kubevirt-overview)

面板指标说明
Total VMssum(kubevirt_vmi_phase_count)VM 总数
Running VMskubevirt_vmi_phase_count{phase="running"}运行中的 VM
Pending/Failed VMskubevirt_vmi_phase_count{phase=...}等待/失败的 VM
Up Endpointscount(up{...} == 1)存活组件数
VMI Memory Usagememory_usable - memory_unused内存使用
VMI CPU Usagerate(vmi_cpu_usage_seconds_total[5m])CPU 使用
VMI Network Trafficrate(vmi_network_*_bytes_total[5m])网络流量
VMI Storage I/Orate(vmi_storage_*_traffic_bytes_total[5m])存储 I/O
VM Phase Distributionsum by (phase)阶段分布饼图
VMs per Nodesum by (node)节点分布
Live Migrationsvmi_migration_data_*_bytes迁移进度

6.3.2 KubeVirt / VM Details (UID: kubevirt-vm-details)

面板: VM Status Table, CPU Usage, Memory Used/Available/Usage%, Network RX/TX/Packets/Errors, Storage Read/Write/IOPS/Flush Latency

6.3.3 KubeVirt / Infrastructure (UID: kubevirt-infrastructure)

面板: 组件状态 (virt-api/controller/handler/operator UP), REST Request Latency (p99), Component Memory/CPU Usage, Workqueue Latency/Depth, VMs per Node

6.3.4 KubeVirt / Live Migration (UID: kubevirt-migration)

面板: Active/Succeeded/Failed/Pending Migrations, Data Remaining/Processed, Transfer Rate, Dirty Memory Rate, Migration Progress %, Network Traffic, CPU Overhead

6.4 部署 Dashboard ConfigMaps

bash
# 通用模式: 创建带 grafana_dashboard=1 label 的 ConfigMap
# Grafana sidecar (grafana-sc-dashboard) 会自动发现并加载

for dashboard in overview vm infra migration; do
  kubectl create configmap "kubevirt-dashboard-${dashboard}" \
    --from-file="${dashboard}.json=/tmp/kubevirt-dashboard-${dashboard}.json" \
    -n cattle-dashboards \
    --dry-run=client -o yaml | \
  kubectl label --local -f - grafana_dashboard=1 --dry-run=client -o yaml | \
  kubectl apply -f -
done

原理: Grafana Pod 中运行 k8s-sidecar 容器 (grafana-sc-dashboard), 它 WATCH cattle-dashboards namespace 中所有带 grafana_dashboard=1 label 的 ConfigMap, 自动将 JSON 文件写入 /tmp/dashboards/ 目录供 Grafana 加载。


7. 部署验证清单

7.1 KubeVirt 核心

bash
# ✅ CR 状态
kubectl get kubevirt -n kubevirt
# NAME       AGE   PHASE
# kubevirt   65m   Deployed

# ✅ 所有 Pod Running
kubectl get pods -n kubevirt
# 16 pods, all 1/1 Running

# ✅ 组件分布
kubectl get deploy,ds -n kubevirt
# 6 Deployments + 1 DaemonSet, all READY

# ✅ CRD 数量
kubectl get crd | grep kubevirt | wc -l
# 22

7.2 监控

bash
# ✅ ServiceMonitor
kubectl get servicemonitor -n kubevirt
# kubevirt-prometheus-metrics

# ✅ PrometheusRules
kubectl get prometheusrule -n kubevirt
# kubevirt-rules

# ✅ Prometheus 抓取目标 (14 个, 全部 UP)
# 通过 Prometheus API 验证:
# curl http://<prometheus-svc>:9090/api/v1/targets | \
#   jq '.data.activeTargets[] | select(.labels.service == "kubevirt-prometheus-metrics")'

# ✅ Grafana Dashboards (4 个)
kubectl get cm -n cattle-dashboards -l grafana_dashboard=1 | grep kubevirt
# kubevirt-dashboard-infra
# kubevirt-dashboard-migration
# kubevirt-dashboard-overview
# kubevirt-dashboard-vm

# ✅ KubeVirt 指标可用 (32 个 kubevirt_* 指标名)

7.3 网络

bash
# ✅ Calico VXLAN 封装
kubectl get ippool default-ipv4-ippool -o jsonpath='{.spec}'
# {"vxlanMode":"Always","ipipMode":"Never","cidr":"10.22.0.0/16",...}

# ✅ 跨节点 Pod 连通
curl -sk https://<virt-api-pod-ip-on-other-node>:8443/healthz
# 200 OK

8. 后续步骤

8.1 部署 CDI (Containerized Data Importer)

CDI 用于管理 VM 磁盘镜像 (导入 qcow2/raw、上传、克隆等)。

bash
# 需要先在 Harbor 添加代理缓存:
# 项目: kubevirt-cdi
# 上游: quay.io/kubevirt

export TAG=$(curl -s https://api.github.com/repos/kubevirt/containerized-data-importer/releases/latest \
  | grep tag_name | cut -d '"' -f 4)
# 或手动指定: export TAG=v1.66.0

# 替换镜像地址为 Harbor 代理
sed -i 's|quay.io/kubevirt|192.168.122.156:30000/kubevirt-cdi/kubevirt|g' cdi-operator.yaml cdi-cr.yaml

kubectl apply -f cdi-operator.yaml
kubectl apply -f cdi-cr.yaml

8.2 安装 virtctl

bash
# 从 Harbor 代理下载或从内网镜像获取
export VERSION=v1.9.0
wget https://github.com/kubevirt/kubevirt/releases/download/${VERSION}/virtctl-${VERSION}-linux-amd64
chmod +x virtctl-${VERSION}-linux-amd64
sudo mv virtctl-${VERSION}-linux-amd64 /usr/local/bin/virtctl

# 验证
virtctl version

8.3 创建测试 VM

yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: test-vm
  namespace: default
spec:
  running: true
  template:
    metadata:
      labels:
        app: test-vm
    spec:
      domain:
        cpu:
          cores: 2
        memory:
          guest: 4Gi
        devices:
          disks:
            - name: rootdisk
              disk:
                bus: virtio
          interfaces:
            - name: default
              masquerade: {}
      networks:
        - name: default
          pod: {}
      volumes:
        - name: rootdisk
          containerDisk:
            image: 192.168.122.156:30000/kubevirt-local/containerdisks/ubuntu:22.04
        - name: cloudinit
          cloudInitNoCloud:
            userData: |
              #cloud-config
              password: xxx
              chpasswd: { expire: False }
              ssh_pwauth: true
bash
kubectl apply -f test-vm.yaml

# 等待 VMI 启动
kubectl get vmi -w

# 通过 virtctl 访问控制台
virtctl console test-vm

# 获取 VMI IP
kubectl get vmi test-vm -o jsonpath='{.status.interfaces[0].ipAddress}'

8.4 VM 磁盘导入 (CDI DataVolume)

yaml
apiVersion: cdi.kubevirt.io/v1beta1
kind: DataVolume
metadata:
  name: rocky9-base
  namespace: default
spec:
  source:
    http:
      url: "http://<file-server>/Rocky-9-GenericCloud.qcow2"
  pvc:
    accessModes: [ReadWriteOnce]
    resources:
      requests:
        storage: 50Gi

8.5 确认 Rancher UI 插件需求

当前已实现 Grafana 监控 Dashboard。若需在 Rancher UI 界面中直接管理 VM (如 Harvester/KubeVirt UI Extension),需进一步调研并安装对应的 Rancher UI 插件。


9. 附录:关键资源清单

9.1 KubeVirt Services

ServiceClusterIPPort说明
virt-api10.23.130.160443API Server + Webhook
virt-exportproxy10.23.25.237443数据导出代理
kubevirt-prometheus-metricsNone (Headless)443Prometheus 指标端点
kubevirt-operator-webhook10.23.98.100443Operator Webhook
virt-template-api-service10.23.19.66443模板 API
virt-template-controller-mgr10.23.220.1258443模板控制器指标
virt-template-webhook-service10.23.213.15443模板 Webhook

9.2 Prometheus 指标 (kubevirt_* 前缀, 共 32 个)

指标名类型说明
kubevirt_infoGaugeKubeVirt 版本信息
kubevirt_configuration_emulation_enabledGauge是否启用软件模拟
kubevirt_vmi_phase_countGaugeVMI 各阶段计数
kubevirt_vmi_memory_usable_bytesGaugeVMI 可用内存
kubevirt_vmi_memory_unused_bytesGaugeVMI 未用内存
kubevirt_vmi_memory_available_bytesGaugeVMI 可用内存总量
kubevirt_vmi_cpu_usage_seconds_totalCounterVMI CPU 使用秒数
kubevirt_vmi_network_receive_bytes_totalCounter网络接收字节
kubevirt_vmi_network_transmit_bytes_totalCounter网络发送字节
kubevirt_vmi_network_receive_packets_totalCounter网络接收包数
kubevirt_vmi_network_transmit_packets_totalCounter网络发送包数
kubevirt_vmi_network_receive_errors_totalCounter网络接收错误
kubevirt_vmi_network_transmit_errors_totalCounter网络发送错误
kubevirt_vmi_storage_read_traffic_bytes_totalCounter存储读字节
kubevirt_vmi_storage_write_traffic_bytes_totalCounter存储写字节
kubevirt_vmi_migration_data_remaining_bytesGauge迁移剩余数据
kubevirt_vmi_migration_data_processed_bytesCounter迁移已处理数据
kubevirt_vmi_migration_dirty_memory_rate_bytesGauge迁移脏内存速率
kubevirt_vmi_migrations_in_pending_phaseGauge等待中迁移数
kubevirt_vmi_migrations_in_running_phaseGauge运行中迁移数
kubevirt_vmi_migrations_in_scheduling_phaseGauge调度中迁移数
kubevirt_vmi_status_conditionGaugeVMI 状态条件
kubevirt_rest_client_requests_totalCounterREST 请求计数
kubevirt_rest_client_request_duration_seconds_bucketHistogramREST 请求延迟
kubevirt_rest_client_request_duration_seconds_countCounterREST 请求延迟计数
kubevirt_rest_client_request_duration_seconds_sumCounterREST 请求延迟总和
kubevirt_rest_client_request_size_bytes_bucketHistogramREST 请求大小
kubevirt_rest_client_response_size_bytes_bucketHistogramREST 响应大小
kubevirt_virt_api_ready_statusGaugevirt-api 就绪状态
kubevirt_virt_controller_ready_statusGaugevirt-controller 就绪状态
kubevirt_virt_handler_ready_statusGaugevirt-handler 就绪状态
kubevirt_virt_operator_ready_statusGaugevirt-operator 就绪状态
kubevirt_virt_controller_leading_statusGaugeLeader 选举状态
kubevirt_node_deprecated_machine_typesGauge弃用的机器类型

9.3 告警规则

告警名严重级别触发条件
KubeVirtComponentDowncritical无 KubeVirt 组件上报指标 >5m
KubeVirtVMIErrorwarning存在 failed 状态的 VMI >1m
KubeVirtVMINotReadywarning运行中 VMI 无 Ready 条件 >5m
KubeVirtHighMemoryUsagewarningVMI 内存使用 >90% >10m
KubeVirtLiveMigrationStalledwarning迁移数据不减少 >15m

9.4 访问地址

服务访问方式
GrafanaRancher UI → 集群 → Monitoring → Grafana
Grafana (内网)http://<grafana-clusterip>:3000
Grafana 密码xxx / (从 Secret rancher-monitoring-grafana 获取)
PrometheusRancher UI → 集群 → Monitoring → Prometheus
KubeVirt DashboardGrafana → Dashboards → 搜索 tag kubevirt
KubeVirt Dashboardhttps://ai-ear.cn:36002 (Basic Auth: admin / xxx)

10. KubeVirt Dashboard 部署 (jimyag/kubevirt-dashboard)

由于 Rancher 官方 Extension 目录中暂无 KubeVirt 专用插件(仅有 Harvester 扩展),我们部署了社区版 jimyag/kubevirt-dashboard 作为 K8s 原生 Dashboard,并通过 Rancher NavLink 和 frpc 隧道集成到外部访问。

双 UI 方案: 同时保留 Python Web UI (systemd + /opt/kubevirt-web-ui/, ai-ear.cn:36003) 作为轻量级管理界面,支持 NodeIP 网段自定义、qcow2 镜像下载等功能。

10.1 架构

External Browser
      │ https://ai-ear.cn:36002 (TLS: ai-ear.cn 证书)

公网机 8.153.84.140
  Traefik entryPoint ep36002 — TLS 终结
      │ frp 隧道 19030 (frps :7000)

192.168.122.9 (frpc, systemd 用户服务 frpc-kubevirt-dashboard)
      │ localIP=192.168.122.31, localPort=8080

192.168.122.31 Auth Proxy (:8080, HTTP Basic Auth)
      │ NodePort :51362

kubevirt-dashboard Pod (Go binary :8080, ServiceAccount ──▶ K8s API)

网络路径: https://ai-ear.cn:36002 → 公网机 Traefik (ep36002, TLS 终结) → frp 隧道端口 19030 (frps 8.153.84.140:7000) → frpc (192.168.122.9) → 192.168.122.31:8080 (Auth Proxy) → :51362 (NodePort) → Pod :8080

10.2 K8s 资源

资源命名空间名称说明
Namespace-kubevirt-dashboard独立命名空间
ServiceAccountkubevirt-dashboardkubevirt-dashboardPod 使用的 SA
ClusterRole-kubevirt-dashboardKubeVirt + Core API 权限
ClusterRoleBinding-kubevirt-dashboardSA → ClusterRole 绑定
Deploymentkubevirt-dashboardkubevirt-dashboard1 副本, Go binary
Servicekubevirt-dashboardkubevirt-dashboardNodePort 51362 → Pod :8080

10.3 镜像

项目
上游镜像ghcr.io/jimyag/kubevirt-dashboard:latest
Harbor 镜像192.168.122.156:30000/kubevirt-local/kubevirt-dashboard:latest
拉取策略IfNotPresent
Harbor 项目kubevirt-local (非代理缓存,手动推送)

注意: 直接拉取 ghcr.io 镜像在国内网络环境下经常超时。解决方案: 通过南京大学镜像 (ghcr.nju.edu.cn) 下载 tarball,再用 skopeo 推送到内部 Harbor。

bash
# 推送镜像到 Harbor 的命令 (参考)
skopeo copy --dest-tls-verify=false \
  --dest-creds admin:xxx \
  docker-archive:/tmp/kubevirt-dashboard.tar \
  docker://192.168.122.156:30000/kubevirt-local/kubevirt-dashboard:latest

10.4 RBAC

bash
kubectl get sa kubevirt-dashboard -n kubevirt-dashboard
kubectl get clusterrole kubevirt-dashboard
kubectl get clusterrolebinding kubevirt-dashboard

权限范围 (ClusterRole):

  • kubevirt.io - VM/VMI 的 CRUD 操作
  • subresources.kubevirt.io - VM 子资源操作 (start/stop/restart/console)
  • cdi.kubevirt.io - DataVolume 管理
  • instancetype.kubevirt.io - 实例类型查询
  • snapshot.kubevirt.io - 快照管理
  • migrations.kubevirt.io - 迁移管理
  • Core API - Namespace/Node/Pod/Event/ConfigMap/Secret 读写
  • storage.k8s.io - StorageClass 查询

10.5 功能清单

功能说明状态
VM 列表多命名空间查看, 搜索过滤
VM 详情CPU/内存/IP/节点/事件
创建 VMYAML 编辑器方式
启动/停止/重启VM 生命周期管理
删除 VM确认操作
控制台VNC/Serial 控制台
多集群支持多 kubeconfig context✅ (当前单集群)
yaml
apiVersion: ui.cattle.io/v1
kind: NavLink
metadata:
  name: kubevirt-dashboard
spec:
  label: KubeVirt VMs
  description: KubeVirt Virtual Machine Dashboard
  group: Virtualization
  target: _blank
  toURL: http://ai-ear.cn:36002

效果: Rancher UI 左侧导航栏出现 Virtualization → KubeVirt VMs 链接,点击在新标签页打开 Dashboard (外网地址)。

10.7 管理命令

bash
# 查看 Pod 状态
kubectl get pods -n kubevirt-dashboard -o wide

# 查看日志
kubectl logs -n kubevirt-dashboard -l app=kubevirt-dashboard -f

# 重启 (滚动更新)
kubectl rollout restart deployment/kubevirt-dashboard -n kubevirt-dashboard

# 查看 Service
kubectl get svc kubevirt-dashboard -n kubevirt-dashboard

# 查看 Deployment YAML
kubectl get deployment kubevirt-dashboard -n kubevirt-dashboard -o yaml

10.8 登录认证 (HTTP Basic Auth Proxy)

kubevirt-dashboard 无内置登录认证,直接使用 Pod ServiceAccount 的 K8s API 权限。我们在节点上部署了一个轻量级 Python 认证代理 (HTTP Basic Auth),拦截所有公网和内网请求。

认证参数

参数
用户名admin
密码xxx
认证方式HTTP Basic Auth (RFC 7617)
RealmKubeVirt Dashboard

架构

Browser

  ├── https://ai-ear.cn:36002/ ──────────────────────────┐
  │                                                       ▼
  │                                          ┌────────────────────────┐
  │                                          │ Auth Proxy (:8080)     │
  │                                          │ Basic Auth Check       │
  │                                          │   401 if no/wrong auth │
  │                                          │   proxy to :51362      │
  │                                          └──────────┬─────────────┘
  │                                                     │
  │                                          ┌──────────▼─────────────┐
  │                                          │ K8s NodePort :51362    │
  │                                          │ → kubevirt-dashboard   │
  │                                          │   Pod :8080            │
  │                                          └────────────────────────┘

部署文件

文件路径主机说明
Auth Proxy 脚本/opt/kubevirt-web-ui/auth_proxy.py192.168.122.31Python HTTP Basic Auth 反向代理
Systemd 服务kubevirt-dashboard-auth-proxy.service192.168.122.31自动启动, 监听 8080

管理命令

bash
# 在 192.168.122.31 上
systemctl status kubevirt-dashboard-auth-proxy
journalctl -u kubevirt-dashboard-auth-proxy -f

# 修改凭据
systemctl edit kubevirt-dashboard-auth-proxy
# [Service]
# Environment="AUTH_USER=newuser"
# Environment="AUTH_PASS=newpass"
systemctl restart kubevirt-dashboard-auth-proxy

# 测试
curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8080/           # → 401
curl -s -o /dev/null -w "%{http_code}" -u admin:xxx http://127.0.0.1:8080/  # → 200

10.9 外网访问 (https://ai-ear.cn:36002)

公网走 公网机 Traefik TLS 终结 + frp 隧道 (沿用 GitLab/Jenkins 惯例),内网通过 iptables DNAT (192.168.122.22)。两条路径均指向 Auth Proxy (:8080)。

完整网络路径

公网: https://ai-ear.cn:36002 → 公网机 8.153.84.140 Traefik (ep36002, TLS 终结, 证书 ai-ear.cn)
      → 127.0.0.1:19030 (frps 隧道端口) → frpc (192.168.122.9) → 192.168.122.31:8080 (Auth Proxy) → :51362
内网: 192.168.122.22:36002 → iptables DNAT → 192.168.122.31:8080 (Auth Proxy) → :51362 (HTTP)

2026-09-03 由「frp TCP 直出 36002 (HTTP)」改为 Traefik TLS 惯例:frpc remotePort 由 36002 改为 19030, 公网端口 36002 让给 Traefik 监听。TLS 在公网机终结,证书 /etc/pki/nginx/ai-ear.cn.crt (Traefik default TLS store),隧道与后端保持明文 HTTP。

frpc 配置 (192.168.122.9)

toml
# /home/xxx/config/frp/frpc-kubevirt-dashboard.toml
serverAddr = "8.153.84.140"
serverPort = 7000
transport.tcpMux = true
auth.method = "token"
auth.token = "xxx"

# 公网 HTTPS 入口 36002 → 公网机 Traefik → 本隧道端口 19030
[[proxies]]
name = "kubevirt-dashboard-36002"
type = "tcp"
localIP = "192.168.122.31"
localPort = 8080
remotePort = 19030
bash
# systemd 用户服务 (xxx 用户)
systemctl --user restart frpc-kubevirt-dashboard
systemctl --user status frpc-kubevirt-dashboard

公网机 Traefik 配置 (8.153.84.140)

静态配置 /etc/traefik/traefik.yaml entryPoint (新增需重启 traefik,秒级中断):

yaml
entryPoints:
  ep36002:
    address: ":36002"

动态配置 /etc/traefik/dynamic.yaml router + service (watch 热加载):

yaml
http:
  routers:
    # KubeVirt Dashboard(K8s 集群,经 frp 隧道 19030,2026-09-02)
    svc-36002:
      entryPoints: [ep36002]
      rule: "Host(`ai-ear.cn`)"
      service: svc-19030
      tls: {}
  services:
    svc-19030:
      loadBalancer:
        servers:
          - url: "http://127.0.0.1:19030"

⚠️ Traefik v3 字段陷阱: HTTP 服务 servers 用 url: "http://host:port"TCP 服务 (passthrough 等) servers 用 address: "host:port"。两者混用会报 field not found, node: url/address,且整个 file provider 配置加载失败 (所有路由失效)。 svc-19030 必须放在 http.services 段,勿追加到文件末尾的 tcp.services 段。

bash
systemctl restart traefik
ss -tlnp | grep -E ':36002|:19030'   # 期望: traefik 持 36002, frps 持 19030

验证

bash
curl -sk -o /dev/null -w "%{http_code}\n" https://ai-ear.cn:36002/                          # → 401 (未带 Basic Auth)
curl -sk -u 'admin:xxx' -o /dev/null -w "%{http_code}\n" https://ai-ear.cn:36002/   # → 200
echo | openssl s_client -connect ai-ear.cn:36002 -servername ai-ear.cn 2>/dev/null \
  | openssl x509 -noout -subject -dates   # subject=CN = ai-ear.cn

iptables DNAT (192.168.122.22)

bash
iptables -t nat -A PREROUTING -p tcp --dport 36002 \
  -j DNAT --to-destination 192.168.122.31:8080
iptables -t nat -A POSTROUTING -p tcp -d 192.168.122.31 --dport 8080 \
  -j MASQUERADE
iptables -t nat -A OUTPUT -p tcp --dport 36002 \
  -j DNAT --to-destination 192.168.122.31:8080

持久化通过 kubevirt-web-ui-forward.service (systemd oneshot) 实现。

10.10 Python Web UI (ai-ear.cn:36003)

Python Web UI 与 kubevirt-dashboard 并行运行,监听在不同端口:

UI公网地址认证方式后端
kubevirt-dashboard (Go)https://ai-ear.cn:36002HTTP Basic AuthK8s Pod + Auth Proxy :8080 (公网经 Traefik TLS)
Python Web UIhttp://ai-ear.cn:36003Session Loginsystemd :8082

功能特性

功能说明
VM 列表/详情多命名空间查看, 搜索过滤, 实时状态
创建 VM表单方式, 支持 containerDisk 和 qcow2 两种镜像类型
NodeIP 网段配置支持 Bridge (Multus) 网络模式, 可自定义 VM IP/子网/网关/DNS
qcow2 镜像下载通过 init container 下载 qcow2 到 PVC, 支持自定义 URL
启动/停止/重启/删除VM 生命周期管理
事件查看VM 关联的 K8s Events

部署文件

文件路径/名称说明
Python 后端/opt/kubevirt-web-ui/server.pyHTTP 服务器, 配置 API, 代理 K8s API
HTML 前端/opt/kubevirt-web-ui/index.htmlSPA, 支持网络/镜像自定义
Systemd 服务kubevirt-web-ui.service监听 :8082
KubeConfig/root/.kube/configK8s API 认证

认证

参数
用户名root
密码xxx
方式Session Cookie (24h 有效)

可配置参数 (systemd Environment)

所有参数均通过 systemd Environment 配置,修改后 systemctl daemon-reload && systemctl restart kubevirt-web-ui:

变量默认值说明
AUTH_USERroot登录用户名
AUTH_PASSxxx登录密码
PORT8082监听端口
NODE_IP_SUBNET192.168.122.0/24NodeIP 网段 (Bridge 模式默认子网)
NODE_GATEWAY192.168.122.1Bridge 模式默认网关
NODE_DNS8.8.8.8,223.5.5.5Bridge 模式默认 DNS
NODE_NET_MODEmasquerade默认网络模式 (masqueradebridge)
DEFAULT_STORAGE_CLASSnfsqcow2 PVC 默认 StorageClass
DEFAULT_DISK_SIZE20Giqcow2 PVC 默认磁盘大小
QCOW2_IMAGESJSON 数组qcow2 镜像列表 (name + url)
CONTAINER_IMAGESJSON 数组containerDisk 镜像列表 (name + image)

配置 API

前端通过 GET /api/config 获取默认配置,无需认证:

json
{
  "network": {
    "subnet": "192.168.122.0/24",
    "gateway": "192.168.122.1",
    "dns": "8.8.8.8,223.5.5.5",
    "defaultMode": "masquerade"
  },
  "images": {
    "qcow2": [
      {"name": "Rocky 9.4 Minimal", "url": "http://192.168.122.31:9001/repo/images/Rocky-9.4-x86_64-minimal.qcow2"},
      {"name": "Ubuntu 22.04 Cloud", "url": "http://192.168.122.31:9001/repo/images/ubuntu-22.04-server-cloudimg-amd64.qcow2"},
      {"name": "CentOS Stream 9", "url": "http://192.168.122.31:9001/repo/images/CentOS-Stream-GenericCloud-9-latest.x86_64.qcow2"}
    ],
    "containerDisks": [...]
  },
  "storage": {"defaultClass": "nfs", "defaultDiskSize": "20Gi"}
}

创建 VM 网络模式说明

Pod Network (Masquerade): VM 获得 Pod 内部 IP (10.22.x.x),通过 NAT 访问外部网络。适用于无需独立 IP 的场景。

Bridge Network (Multus): VM 获得 NodeIP 网段 IP (如 192.168.122.x),与物理网络直接通信。需要:

  1. 在目标节点创建 NetworkAttachmentDefinition (bridge CNI)
  2. cloud-init 中自动注入静态 IP/网关/DNS 网络配置

管理命令

bash
# 在 192.168.122.31 上
systemctl status kubevirt-web-ui
journalctl -u kubevirt-web-ui -f
systemctl restart kubevirt-web-ui

# 修改配置 (示例: 更改 NodeIP 网段)
systemctl edit kubevirt-web-ui
# [Service]
# Environment="NODE_IP_SUBNET=10.0.0.0/24"
# Environment="NODE_GATEWAY=10.0.0.1"
systemctl daemon-reload && systemctl restart kubevirt-web-ui

# 查看当前配置
curl -s http://127.0.0.1:8082/api/config | python3 -m json.tool

frpc 配置 (192.168.122.9)

toml
# /home/xxx/config/frp/frpc-kubevirt-webui.toml
serverAddr = "8.153.84.140"
serverPort = 7000
transport.tcpMux = true
auth.method = "token"
auth.token = "xxx"

[[proxies]]
name = "kubevirt-webui-36003"
type = "tcp"
localIP = "192.168.122.31"
localPort = 8082
remotePort = 36003

iptables DNAT (192.168.122.22)

bash
iptables -t nat -A PREROUTING -p tcp --dport 36003 \
  -j DNAT --to-destination 192.168.122.31:8082
iptables -t nat -A POSTROUTING -p tcp -d 192.168.122.31 --dport 8082 \
  -j MASQUERADE
iptables -t nat -A OUTPUT -p tcp --dport 36003 \
  -j DNAT --to-destination 192.168.122.31:8082

10.11 Rocky 9 VM 部署 (hostPath PV 方式)

由于 CDI 尚未完整部署,我们使用 hostPath PV 方式直接挂载 qcow2 磁盘镜像。

10.11.1 准备磁盘镜像

bash
# 从内网 HTTP 服务器下载 qcow2 镜像
curl -Lo /tmp/Rocky-9-GenericCloud.qcow2 \
  http://192.168.122.9:9001/repo/osiso/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2

# 安装 qemu-img (如果没有)
dnf install -y qemu-img

# 转换为 raw 格式
qemu-img convert -f qcow2 -O raw /tmp/Rocky-9-GenericCloud.qcow2 /tmp/Rocky-9-GenericCloud.raw

# 创建目录并移动 (KubeVirt 要求 PV 指向包含 disk.img 的目录)
mkdir -p /var/lib/vm-images/rocky9-disk
mv /tmp/Rocky-9-GenericCloud.raw /var/lib/vm-images/rocky9-disk/disk.img

# 设置权限 (QEMU 以 UID 107 运行)
chmod 666 /var/lib/vm-images/rocky9-disk/disk.img

10.11.2 创建 PV/PVC

bash
kubectl apply -f - <<'EOF'
---
apiVersion: v1
kind: PersistentVolume
metadata:
  name: rocky9-vm-disk
  labels:
    type: rocky9-disk
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/var/lib/vm-images/rocky9-disk"
    type: Directory
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - sza122031.local
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rocky9-vm-disk
  namespace: default
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  selector:
    matchLabels:
      type: rocky9-disk
EOF

10.11.3 创建 VM

yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: rocky9-test
  namespace: default
  labels:
    app: rocky9-test
    kubevirt.io/vm: rocky9-test
spec:
  running: true
  template:
    metadata:
      labels:
        app: rocky9-test
        kubevirt.io/vm: rocky9-test
    spec:
      nodeSelector:
        kubernetes.io/hostname: sza122031.local
      evictionStrategy: None   # ← 必须设为 None (hostPath 不支持 LiveMigrate)
      domain:
        cpu:
          cores: 2
        memory:
          guest: 2Gi
        devices:
          disks:
          - name: rootdisk
            disk:
              bus: virtio
          - name: cloudinit
            disk:
              bus: virtio
          interfaces:
          - name: default
            masquerade: {}
      networks:
      - name: default
        pod: {}
      volumes:
      - name: rootdisk
        persistentVolumeClaim:
          claimName: rocky9-vm-disk
      - name: cloudinit
        cloudInitNoCloud:
          userData: |
            #cloud-config
            hostname: rocky9-test
            password: xxx
            chpasswd:
              expire: false
            ssh_pwauth: true
            disable_root: false

10.11.4 验证

bash
# 检查 VM 状态
kubectl get vm,vmi -n default
# NAME          STATUS    READY
# rocky9-test   Running   True

# 获取 VMI IP
kubectl get vmi rocky9-test -n default -o jsonpath='{.status.interfaces[0].ipAddress}'
# 10.22.183.219

# SSH 到 VM
sshpass -p rocky ssh -o StrictHostKeyChecking=no rocky@<VMI_IP>
# rocky9-test$ hostname
# rocky9-test

# 停止 VM
kubectl patch vm rocky9-test -n default --type merge -p '{"spec":{"running":false}}'

# 启动 VM
kubectl patch vm rocky9-test -n default --type merge -p '{"spec":{"running":true}}'

已知限制:

  • VM 只能在指定节点 (sza122031.local) 上运行 (hostPath 限制)
  • 不支持热迁移 (evictionStrategy 必须设为 None)
  • 磁盘是共享的,多个 VM 实例会写入同一文件
  • virt-handler Pod 重启后需重新手动复制证书 (见故障 5.5)

10.12 containerDisk 镜像 (已解决 ✅)

已将所有 containerDisk 镜像推送到 Harbor 私有仓库 kubevirt-local 项目:

镜像私有仓库地址
Ubuntu 22.04192.168.122.156:30000/kubevirt-local/containerdisks/ubuntu:22.04
Ubuntu 24.04192.168.122.156:30000/kubevirt-local/containerdisks/ubuntu:24.04
Fedora Latest192.168.122.156:30000/kubevirt-local/containerdisks/fedora:latest
CentOS Stream 9192.168.122.156:30000/kubevirt-local/containerdisks/centos-stream:9

推送命令参考:

bash
skopeo copy \
  --src-tls-verify=false --dest-tls-verify=false \
  --dest-creds admin:PASSWORD \
  --override-os linux --override-arch amd64 \
  --image-parallel-copies 2 \
  docker://quay.io/containerdisks/ubuntu:22.04 \
  docker://192.168.122.156:30000/kubevirt-local/containerdisks/ubuntu:22.04

Web UI 已更新为使用私有仓库地址,创建 VM 时将自动从内网拉取镜像。


变更记录

日期操作说明
2026-08-27环境评估确认 RKE2 v1.35.6, 6 节点, Rocky Linux 9.8
2026-08-27Harbor 代理缓存创建 kubevirt 项目代理 quay.io
2026-08-27部署 22 个 CRDKubeVirt 所有自定义资源定义
2026-08-27部署 RBACClusterRole + ClusterRoleBinding
2026-08-27修复 RBAC 权限Patch 添加通配符权限
2026-08-27部署 virt-operator2 副本, 从 Harbor 拉取镜像
2026-08-27创建 KubeVirt CRimageRegistry 指向 Harbor
2026-08-27恢复 Calico CRD/IPPool修复误删导致的网络中断
2026-08-27修复 Rancher Webhook移除对 namespace 操作的拦截
2026-08-27修复 Calico VXLAN从 Never → Always, 解决跨节点 502
2026-08-27KubeVirt Deployed ✅16 pods 全部 Running
2026-08-27部署 ServiceMonitor14 个 scrape targets, 全部 UP
2026-08-27部署 PrometheusRules5 条告警规则
2026-08-27部署 Grafana Dashboards4 个仪表盘 (Overview/VM/Infra/Migration)
2026-08-27调研 Rancher Extension确认官方目录无 KubeVirt 插件 (仅 Harvester)
2026-08-27部署 KubeVirt Web UIPython 后端 + SPA 前端, systemd 服务
2026-08-27创建 Rancher NavLinkVirtualization → KubeVirt VMs 侧边栏集成
2026-08-27验证 Web UI API创建/启动/停止/删除 VM 全流程通过
2026-08-27下载 Rocky 9 qcow2从 192.168.122.9:9001 下载并转换为 raw (10GB)
2026-08-27排查 virt-handler 证书问题手动复制 clientcertificates 到 Pod (临时方案)
2026-08-27排查 CDI CRD 干扰删除空壳 CDI CRD,解除 virt-controller 阻塞
2026-08-27修复磁盘权限chmod 666 + PV 指向 Directory (非 File)
2026-08-27Rocky 9 VM 启动成功 ✅VM Running, SSH 可达, Rocky Linux 9.8
2026-08-27Web UI 登录认证session-based 认证 (admin), 登录页 + 401 拦截 + Logout
2026-08-27外网端口转发iptables DNAT: ai-ear.cn:36002 → 192.168.122.31:8080
2026-08-27NavLink 更新Rancher 侧边栏链接指向 ai-ear.cn:36002
2026-08-27替换 Web UI 为 kubevirt-dashboardjimyag/kubevirt-dashboard (Go), Harbor 镜像部署
2026-08-27停用旧 Python Web UIsystemctl stop/disable kubevirt-web-ui
2026-08-27部署 Auth ProxyPython Basic Auth 代理 :8080 → NodePort :51362
2026-08-27配置 frpc 隧道192.168.122.9 → ai-ear.cn:36002 → 192.168.122.31:8080
2026-08-27恢复 Python Web UI重新启用自建 Web UI, 支持 Bridge 网络 + qcow2 PVC
2026-08-27更新默认凭据root/ai-ear.cn, systemd 环境变量驱动
2026-08-27修复 JS 语法错误renderVMs 使用 ' 替代 ' 转义, 解决 Unexpected identifier 报错
2026-08-27containerDisk 镜像推送 ✅4 个镜像推送至 Harbor kubevirt-local 项目 (skopeo)
2026-09-03公网访问改 HTTPSai-ear.cn:36002 由 frp TCP 直出改为公网机 Traefik TLS 终结 (ep36002 + frp 隧道 19030),沿用 GitLab/Jenkins 惯例;frpc remotePort 36002→19030;NavLink toURL 改 https;修正文档中 Basic Auth 密码笔误 (xxx)