Skip to content

KubeVirt 网络固定 IP、SSH Root 密码登录与密码管理指南

日期: 2026-09-02 环境: RKE2 v1.35.6 (6 节点) + KubeVirt v1.9.0,Rocky Linux 9.8,Calico VXLAN (Pod CIDR 10.22.0.0/16) 入口节点: root@192.168.122.31 (sza122031.local) 适用 VM: rocky9-test / rocky9-vm2 / rocky9-vm3(当前均为 masquerade Pod 网络) 关联文档: rke2-kubevirt-deployment-guide.mdrocky9-vm-ops-log-20260827.md


目录

  1. 现状与方案选型
  2. 方案 A: NodePort 固定访问入口(零节点改造)
  3. 方案 B: Multus bridge/macvlan 获得物理网段固定 IP
  4. SSH root 密码登录配置
  5. 账号密码修改与管理
  6. 完整示例: rocky9-vm2 固定 IP + root 登录
  7. 验证清单
  8. FAQ 与常见问题
  9. 重启后 IP 保留机制

1. 现状与方案选型

1.1 当前网络现状

三台 VM 均使用默认 Pod 网络:

yaml
interfaces:
- name: default
  masquerade: {}      # VM 通过 NAT 借用 Pod IP 出网
networks:
- name: default
  pod: {}
项目现状问题
VM IP10.22.x.x (Calico Pod IP)每次重建/重启可能变化,无法固定
可达性仅集群内部直连,外部需经 NAT/NodePort外部系统无法按固定 IP 直连

1.2 方案对比

方案原理固定效果适用场景改造范围
A. NodePort ServiceK8s Service 作为稳定入口节点IP:端口 永远不变只需稳定 SSH/服务访问零节点改造,10 分钟
B. Multus bridgeVM 第二块网卡挂到宿主机物理网桥VM 拥有固定 192.168.122.x外部系统需按物理网段 IP 直连节点建桥 + NAD + cloud-init
B'. macvlanVM 网卡虚拟成物理网卡的子 MAC同方案 B不想改动节点网卡配置仅 NAD + cloud-init

⚠️ 重要结论: KubeVirt 的 Multus 辅助网络不会把 NAD 中 IPAM(whereabouts/host-local 等)分配的 IP 注入客户机, 客户机内必须由 cloud-init 静态配置或内部 DHCP 获取 IP。因此固定 IP 的标准做法是 cloud-init networkData 写死静态 IP


2. 方案 A: NodePort 固定访问入口(零节点改造)

Pod IP 会变,但 Service 入口稳定。适合"只要 SSH 能稳定连上"的场景。

2.1 命令行方式(virtctl)

bash
virtctl expose vm rocky9-vm2 -n default \
  --port 22 --target-port 22 \
  --type NodePort --name rocky9-vm2-ssh

kubectl get svc rocky9-vm2-ssh
# NAME             TYPE       CLUSTER-IP      PORT(S)
# rocky9-vm2-ssh   NodePort   10.43.xxx.xxx   22:31222/TCP

# 通过任意节点 IP + 该端口访问,VM 重启/换节点均不受影响
ssh root@192.168.122.31 -p 31222

2.2 声明式 YAML 方式(推荐,可进 Git)

yaml
apiVersion: v1
kind: Service
metadata:
  name: rocky9-vm2-ssh
  namespace: default
spec:
  type: NodePort
  selector:
    app: rocky9-vm2          # 必须与 VM template.metadata.labels 一致
  ports:
    - name: ssh
      port: 22
      targetPort: 22
      nodePort: 31022        # 固定端口号 (30000-32767),双固定: 节点IP+端口

说明: virtctl expose 的端口由系统随机分配;要完全固定用上面的 YAML 指定 nodePort。 局限: 外部看到的源地址是节点(NAT),VM 内无法感知真实客户端 IP。


3. 方案 B: Multus bridge/macvlan 获得物理网段固定 IP

原理:给 VM 增加第二块网卡,经 Multus 挂到宿主机物理网桥(或 macvlan),cloud-init 在客户机内写死静态 IP。 共 5 步:验证 Multus → 装 CNI 插件 → (bridge 模式)节点建桥 → 建 NAD → 改 VM YAML。

3.1 步骤 ①: 确认 Multus 已部署(RKE2 默认自带,需验证)

bash
kubectl get pods -n kube-system | grep multus
kubectl get crd network-attachment-definitions.k8s.cni.cncf.io

若不存在,用 Helm 安装:

bash
helm repo add rke2-charts https://rancher.github.io/rke2-charts
helm install multus rke2-charts/rke2-multus -n kube-system

3.2 步骤 ②: 节点安装 bridge CNI 插件

Calico 不自带 bridge 插件,需要在每台会跑该 VM 的节点上手动安装:

bash
dnf install -y containernetworking-plugins
ls /opt/cni/bin/                               # 确认 kubelet 的 CNI bin 目录
cp /usr/libexec/cni/bridge /opt/cni/bin/       # 若该路径不存在,试 /usr/lib/cni
ls /opt/cni/bin/bridge && echo "bridge 插件 OK"

macvlan 变体(B')同样需要 cp /usr/libexec/cni/macvlan /opt/cni/bin/

3.3 步骤 ③: 节点创建网桥(macvlan 变体可跳过本步)

⚠️ 把物理网卡挂进网桥会有几秒断网,务必通过控制台 / IPMI 操作,不要只依赖 SSH

bash
# 创建网桥并把物理网卡作为桥成员,节点 IP 迁移到网桥上
nmcli con add type bridge con-name br0 ifname br0 stp no
nmcli con add type bridge-slave con-name br0-slave ifname ens192 master br0   # 换成实际网卡名
nmcli con modify br0 ipv4.addresses 192.168.122.31/24 ipv4.gateway 192.168.122.1 \
  ipv4.dns 192.168.122.1 ipv4.method manual connection.autoconnect yes
nmcli con up br0 && nmcli con up br0-slave && nmcli con down "<原物理网卡连接名>"

3.4 步骤 ④: 创建 NetworkAttachmentDefinition(放在 VM 所在命名空间)

bridge 版:

yaml
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: br-physnet
  namespace: default
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "bridge",
    "bridge": "br0",
    "ipam": {}
  }'

macvlan 版(B',无需改动节点网卡,配置零风险):

yaml
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "macvlan",
    "master": "ens192",
    "mode": "bridge",
    "ipam": {}
  }'
  • ipam: {} 表示不自动分配 IP,静态 IP 由 cloud-init 在客户机内写死(见 3.5)。
  • macvlan 局限:VM 与自己所在的宿主机互访不通;bridge 无此问题。
  • 若物理交换机有端口隔离(port isolation),macvlan 可能不通,用 bridge。

3.5 步骤 ⑤: VM 增加第二网卡并写死静态 IP

修改 VM YAML 两处——interfaces/networks 增加 br-net,cloud-init 增加 networkData

yaml
      domain:
        devices:
          interfaces:
          - name: default
            masquerade: {}          # 保留 Pod 网络:控制面、热迁移通道需要
          - name: br-net
            bridge: {}              # 新增:物理网桥网卡,在 VM 内表现为 eth1
      networks:
      - name: default
        pod: {}
      - name: br-net                # 新增
        multus:
          networkName: br-physnet
      volumes:
      - name: cloudinit
        cloudInitNoCloud:
          userData: |-
            # ... 见第 4 章 ...
          networkData: |-           # 新增:固定 IP 静态配置
            version: 2
            ethernets:
              eth1:
                addresses:
                  - 192.168.122.102/24
                routes:
                  - to: default
                    via: 192.168.122.1
                    metric: 100
                nameservers:
                  addresses: [192.168.122.1]

networkData(netplan v2)在镜像上渲染不生效,用 runcmd 兜底,效果等价且跨发行版稳定:

yaml
          userData: |-
            #cloud-config
            runcmd:
              - nmcli con add type ethernet con-name eth1-static ifname eth1
                  ipv4.method manual ipv4.addresses 192.168.122.102/24
                  ipv4.gateway 192.168.122.1 ipv4.dns 192.168.122.1
                  connection.autoconnect yes
              - nmcli con up eth1-static

应用并重启:

bash
kubectl apply -f rocky9-vm2.yaml
virtctl restart rocky9-vm2
# 之后该 VM 永远是 192.168.122.102

注意事项:

  1. 保留默认 pod: {} 网络——KubeVirt 热迁移的数据通道走 Pod 网络,去掉后无法热迁移;
  2. metric: 100 避免物理默认路由与 Pod 网络默认路由抢路由;
  3. 多台 VM 依次分配 192.168.122.102/103/104…,避免与现有设备冲突(先 ping + arping 确认);
  4. 该模式与 hostPath PV 组合时,VM 仍被 nodeAffinity 钉在单节点;换共享存储后才能自由调度。

3B. 2026-09 实施记录:最终架构与踩坑全集(取代 §3 的 bridge/macvlan 步骤)

本节是 2026-09-03 在 122.31 集群(RKE2 v1.35.6 + Calico + KubeVirt v1.9.0, 节点为工作站 libvirt VM)上的真实落地结果。§3 的 bridge/macvlan 步骤在 本环境验证不可行(原因见下),最终采用 host-device + tc mirred 方案, rocky9-vm3 已获得物理网段静态 IP 192.168.122.200,重启自动恢复, 工作站/跨节点/SSH 全部验证通过。 全部清单文件:/u02/repo/kubevirt/multus/(physnet-devices.yaml、nads.yaml、 multus-whereabouts.yaml、rocky9-vm3-static-ip.yaml)。

3B.1 已部署组件

组件版本部署方式
Multusv4.2.4(rancher/hardened-multus-cni)RKE2 内置 rke2-multus chart(从 rke2-runtime 镜像解出),helm template 后 kubectl apply
Whereaboutsv0.9.4(rancher/hardened-whereabouts)同上(rke2-multus 的子 chart,rke2-whereabouts.enabled=true
CNI 插件包v1.9.1(rancher/hardened-cni-plugins,含 bridge/macvlan/host-device/static 等)chart 自带 initContainer 安装到 /opt/cni/bin
宿主设备守护自研 physnet-host-devices DaemonSet特权 + hostPID + chroot,见 3B.5

离线要点:节点 registries.yaml 已把 * 镜像源 mirror 到 http://192.168.122.156:30000(带认证),chart 渲染时用 --set global.systemDefaultRegistry=192.168.122.156:30000。 因节点间无 SSH,Multus 不能用 multus: true 改 RKE2 配置的方式(需逐台重启 server),改用 helm template + kubectl apply。

3B.2 踩坑链(每一步都有实测证据)

  1. macvlan CNI 的 master 落在 pod 内的 calico veth 上:NAD 里写 "type":"macvlan","master":"eth0" 时,插件在 pod netns 里找 "eth0", 命中的是 calico 的 veth——"物理网卡"实际挂在 overlay 上,出不了物理网段。
  2. busybox ip 不认 mode bridge:hardened-cni-plugins 镜像里的 ip 是 busybox 版,ip link add ... type macvlan mode bridge 会静默建成 vepa 模式。必须 chroot /hostroot /usr/sbin/ip 用宿主机原生 iproute2。
  3. KubeVirt bridge 绑定会改写/随机化 pod 网卡 MAC,multus 注解里请求的 mac 不生效。
  4. macvlan 作桥端口收不到"他人 MAC"的单播:内核 macvlan 的 rx 过滤只认 自身 MAC + 广播/组播,promisc 也救不了。VM 的 MAC 在网桥"后面"时, 发往 VM MAC 的单播到不了 VM。
  5. 桥自身 MAC 遮蔽:桥会继承最小成员 MAC。若 macvlan 端口 MAC = VM MAC, 桥把 VM MAC 当成自己的,发往该 MAC 的单播被桥本地吸收;即使显式改桥 MAC, 端口的 permanent FDB 条目仍会把单播弹回原端口。bridge fdb delOperation not supported,无解。
  6. host-device CNI 的 teardown bug:pod 删除时偶发 failed to find original ifname ... (alias is not set),KillPodSandbox 无限重试,launcher pod 卡 Terminating。处置: kubectl delete pod <launcher> --force --grace-period=0 (必要时先 crictl stopp/rmp <sandbox-id>)。
  7. tc u32 偏移以 L3 头为基准:ingress 上按以太网偏移写的规则永不命中 (Illegal "match"/success 0);u32 值还需 4 字节对齐。正确写法见 3B.5。
  8. mirred 按安装时刻的 ifindex 绑定设备:veth 对重建后,指向旧设备的 过滤器全部失效。看门狗用 /run/physnet-vh200.ifindex marker 检测 ifindex 变化并全量重建规则。

3B.3 最终架构(tc mirred 版,零停机、不动节点 IP)

                        节点宿主 netns
 物理线路 ── eth0 ─┬─(tc ingress)─ 命中: ARP请求.200 / 目的IP.200 ─redirect─┐
                   │                                                          ▼
                   │                                                     vh-200 (veth)
                   └─(tc ingress: 源IP.200 / ARP spa.200 ─redirect→ eth0)     │

                        ┌────────────── launcher pod netns ──────────────┐
                        │  vp-200(host-device 移入, KubeVirt 改名 *-nic) │
                        │      │ bridge k6t-<cid>                        │
                        │      └── tap<cid> ── VM 客人机 eth1 (.200)     │
                        └────────────────────────────────────────────────┘
  vh-200 ingress 其余帧(回弹广播等)一律 drop,防环。

要点:

  • macvlan 从数据路径中彻底移除——eth0 保持原样(节点网络零影响);
  • 入向靠 eth0 ingress tc 精确匹配(ARP tpa=.200 / IP dst=.200);
  • 出向只放行源为 .200 的帧,其余丢弃;
  • vp-200 由 NAD(host-device)在 VM pod 创建时移入,pod 销毁时归还 (归还失败时看门狗重建整对)。

3B.4 NetworkAttachmentDefinition

yaml
# 静态 200(rocky9-vm3 专用;设备名必须与 DaemonSet 中一致)
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: physnet-static-200
  namespace: default
spec:
  config: '{
    "cniVersion": "0.3.1",
    "name": "physnet-static-200",
    "type": "host-device",
    "device": "vp-200",
    "ipam": { "type": "static",
              "addresses": [ { "address": "192.168.122.200/24" } ] }
  }'

动态池(whereabouts):physnet-pool-1 使用宿主预创建的 mv-131 (macvlan,供普通测试 pod 直接使用——普通 pod 无 KubeVirt 内部网桥, macvlan 用自身 MAC 收单播没有问题):

yaml
spec:
  config: '{
    "cniVersion": "0.3.1", "name": "physnet-pool-1",
    "type": "host-device", "device": "mv-131",
    "ipam": { "type": "whereabouts",
              "range": "192.168.122.128/26",
              "exclude": ["192.168.122.128/32","192.168.122.191/32"] }
  }'

注意:whereabouts 池给 VM 用时同样受 3B.2 第 4/5 条限制—— 每个需要物理网段的 VM 都应走"专属设备 + 静态绑定"模式(见 3B.7)。

3B.5 DaemonSet:physnet-host-devices(看门狗)

职责(每 5 秒巡检,全部幂等):

  1. 维护 mv-131(动态池 macvlan,缺失即建);
  2. 清理历史桥接拓扑(br-v200/mv-200);
  3. 维护 veth 对 vh-200↔vp-200vp-200 不在宿主侧时, 先用 crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock pods 检查 virt-launcher-rocky9-vm3 是否存在——存在则为 pod 持有,绝不重建 (删 veth 一端会连带销毁 pod 内的另一端!);不存在则重建;
  4. 维护 tc 规则(marker /run/physnet-vh200.ifindex 变化则全量重建):
设备协议匹配(偏移以 L3 头为基准)动作
eth0 ingressarptpa=c0a8 7ac8 @24/@26redirect vh-200
eth0 ingressipdst=192.168.122.200 @16redirect vh-200
vh-200 ingressipsrc=192.168.122.200 @12redirect eth0
vh-200 ingressarpspa=c0a8 7ac8 @14/@16redirect eth0
vh-200 ingressall全匹配drop(防环)

镜像:192.168.122.156:30000/rancher/hardened-cni-plugins:v1.9.1-build20260608; 镜像自带的 ip 是 busybox 版,真正的 ip/tc 一律 chroot /hostroot 用宿主二进制。

3B.6 VM 侧配置(rocky9-vm3 实例)

yaml
spec:
  template:
    spec:
      domain:
        devices:
          interfaces:
          - masquerade: {}
            name: default
            macAddress: 00:16:3e:7a:c8:00
          - bridge: {}
            name: physnet
            macAddress: 00:16:3e:7a:c8:01   # 须与 DaemonSet 中 MAC200 一致
      networks:
      - name: default
        pod: {}
      - name: physnet
        multus:
          networkName: physnet-static-200
      volumes:
      - cloudInitNoCloud:
          userData: |
            #cloud-config
            hostname: rocky9-vm3
            password: xxx
            chpasswd: { expire: false }
            ssh_pwauth: true
            disable_root: false

要点:

  • 客人机第二网卡的 IP 来自 KubeVirt 内部 DHCP(把 static IPAM 的 .200 发给 VM),cloud-init 默认"所有网卡 DHCP"即可,不需要 networkData (Rocky9 的 NM renderer 对 networkData 支持不稳,实测曾导致主网卡失联);
  • containerDisk 每次启动是全新文件系统,cloud-init 每次都会执行;
  • macAddress 校验只接受非本地管理位 MAC(02:xx/52:54:00 等被拒, 实测 00:16:3e:* 可用)。

3B.7 新增一台静态物理 IP 的 VM(操作手册)

  1. 选定 <IP><MAC>(非本地管理位,LAN 内唯一);
  2. physnet-devices.yaml:仿照 ensure_vm200 增加一套 vh-<尾段>/vp-<尾段> + 对应 tc 规则(0xc0a87ac8 换成新 IP 的十六进制, MAC 过滤按需添加——仅靠 IP 匹配也能工作);
  3. 新建 NAD physnet-static-<尾段>(host-device,device=vp-<尾段>, static ipam=<IP>/24);
  4. VM spec 增加第二网卡(multus: networkName 指向该 NAD,macAddress=<MAC>);
  5. 应用后重启 VM;从工作站 ping <IP> 验证。

3B.8 已知问题与运维处置

问题处置
launcher 卡 Terminating(host-device DEL alias bug,偶发)kubectl delete pod <launcher> --force --grace-period=0;仍卡则 crictl stopp <sandbox-id> && crictl rmp <sandbox-id>
VM 删除/重建窗口内物理 IP 短暂不可用正常(看门狗 ≤5s 重建设备,新 pod 起来后自动恢复,已实测)
crictl 在 chroot 里找不到 socket必须带 --runtime-endpoint unix:///run/k3s/containerd/containerd.sock(RKE2 socket 不在默认路径)
节点宿主排障入口(节点无 SSH 时)kubectl exec -n kube-system <physnet-host-devices-pod> -- chroot /hostroot ...

4. SSH root 密码登录配置

4.1 cloud-init 声明(现有 VM 已具备)

三台测试机的 cloud-init 里已有以下配置,作用就是开启 root 密码登录:

yaml
      - name: cloudinit
        cloudInitNoCloud:
          userData: |-
            #cloud-config
            hostname: rocky9-vm2
            ssh_pwauth: true        # sshd 开启密码认证 (PasswordAuthentication yes)
            disable_root: false     # 允许 root 登录 (cloud-init 设 PermitRootLogin yes)
            chpasswd:
              expire: false
              list: |
                root:rocky          # root 密码
字段作用
chpasswd.list / `password:xxx设置 root 密码
ssh_pwauth: truesshd_config → PasswordAuthentication yes
disable_root: falsesshd_config → PermitRootLogin yes

4.2 验证

bash
kubectl get vmi -o wide             # 拿 VM 的 IP
ssh root@10.22.x.x                  # 密码 rocky

连不上时,用串口控制台 virtctl console rocky9-vm2 进去检查:

bash
sshd -T | grep -E 'permitrootlogin|passwordauthentication'   # 实际生效值
systemctl status sshd
firewall-cmd --list-services        # 确认列表里有 ssh

VM 内手动修复(Rocky 9 默认 PermitRootLogin prohibit-password):

bash
sed -ri 's/^\s*#?\s*PermitRootLogin.*/PermitRootLogin yes/' /etc/ssh/sshd_config
echo 'PasswordAuthentication yes' > /etc/ssh/sshd_config.d/01-pwauth.conf
systemctl restart sshd
firewall-cmd --add-service=ssh --permanent && firewall-cmd --reload

4.3 安全建议

  • 生产环境优先用密钥登录ssh_authorized_keys 写进 cloud-init(见 5.4);
  • root 密码登录仅限内网测试环境,并用 NetworkPolicy / 防火墙限制 22 端口来源;
  • 固定 IP 生效后,外部直连面扩大,务必同步改强密码。

5. 账号密码修改与管理

5.1 ⚠️ 最大的坑:cloud-init 每次开机都会重新执行

KubeVirt 每次启动 VMI 都会生成新的 instance-id,cloud-init 视为全新实例并重跑所有模块。后果:

在 VM 内用 passwd 改的密码,下次 xxx 后会被 YAML 里 userData 中的密码覆盖回去

因此改密码必须"改 YAML"才算永久生效。

5.2 永久修改方式(推荐)

bash
kubectl edit vm rocky9-vm2

把 cloud-init 的密码段改成:

yaml
            chpasswd:
              expire: false
              list: |
                root:NewRootPass!2026
                rocky:NewRockyPass
bash
virtctl restart rocky9-vm2    # 必须重启:cloud-init 卷在 VMI 创建时生成,重启才生效

5.3 使用密码哈希(YAML 不留明文)

bash
openssl passwd -6 'YourPass'        # 输出 $6$xxxx 形式的 SHA-512 哈希
yaml
            chpasswd:
              expire: false
              list: |
                root:$6$xxxxxxxxxxxxxxxx

5.4 普通用户的创建与管理

Rocky GenericCloud 镜像默认只有 root;rocky 用户建议在 YAML 里显式声明,避免环境漂移:

yaml
            users:
              - name: rocky
                plain_text_passwd: 'xxx'   # 或 passwd: 'xxx'
                lock_passwd: xxx
                shell: /bin/bash
                sudo: ALL=(ALL) NOPASSWD:xxx
                ssh_authorized_keys:
                  - ssh-ed25519 AAAA... your-key

提示: users 中已存在的用户只会被"补齐"字段,不会重复创建;改密码仍以 xxx 为准。


6. 完整示例: rocky9-vm2 固定 IP + root 登录

在现有 rocky9-vm2.yaml 基础上,整合"方案 B + root 密码登录"后的完整文件:

yaml
---
# === PersistentVolume (hostPath,维持现状) ===
apiVersion: v1
kind: PersistentVolume
metadata:
  name: rocky9-vm2-disk
  labels:
    type: rocky9-vm2-disk
spec:
  storageClassName: manual
  capacity:
    storage: 10Gi
  accessModes:
    - ReadWriteOnce
  hostPath:
    path: "/var/lib/vm-images/rocky9-vm2-disk"
    type: Directory
  nodeAffinity:
    required:
      nodeSelectorTerms:
      - matchExpressions:
        - key: kubernetes.io/hostname
          operator: In
          values:
          - sza122031.local
---
# === PersistentVolumeClaim ===
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: rocky9-vm2-disk
  namespace: default
spec:
  storageClassName: manual
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 10Gi
  selector:
    matchLabels:
      type: rocky9-vm2-disk
---
# === 物理网桥 NAD (bridge 需先在节点上按 3.3 建好 br0) ===
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: br-physnet
  namespace: default
spec:
  config: '{
    "cniVersion": "0.3.1",
    "type": "bridge",
    "bridge": "br0",
    "ipam": {}
  }'
---
# === VirtualMachine: 双网卡 + 固定 IP + root 密码登录 ===
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
  name: rocky9-vm2
  namespace: default
  labels:
    app: rocky9-vm2
    kubevirt.io/vm: rocky9-vm2
spec:
  running: true
  template:
    metadata:
      labels:
        app: rocky9-vm2
        kubevirt.io/vm: rocky9-vm2
    spec:
      evictionStrategy: None
      nodeSelector:
        kubernetes.io/hostname: sza122031.local
      domain:
        cpu:
          cores: 2
        memory:
          guest: 2Gi
        devices:
          disks:
          - name: rootdisk
            disk:
              bus: virtio
          - name: cloudinit
            disk:
              bus: virtio
          interfaces:
          - name: default
            masquerade: {}          # eth0: Pod 网络 (控制面/迁移通道)
          - name: br-net
            bridge: {}              # eth1: 物理网桥,固定 192.168.122.102
      networks:
      - name: default
        pod: {}
      - name: br-net
        multus:
          networkName: br-physnet
      volumes:
      - name: rootdisk
        persistentVolumeClaim:
          claimName: rocky9-vm2-disk
      - name: cloudinit
        cloudInitNoCloud:
          userData: |-
            #cloud-config
            hostname: rocky9-vm2
            ssh_pwauth: true
            disable_root: false
            chpasswd:
              expire: false
              list: |
                root:rocky
          networkData: |-
            version: 2
            ethernets:
              eth1:
                addresses:
                  - 192.168.122.102/24
                routes:
                  - to: default
                    via: 192.168.122.1
                    metric: 100
                nameservers:
                  addresses: [192.168.122.1]
bash
kubectl apply -f rocky9-vm2.yaml && virtctl restart rocky9-vm2
ssh root@192.168.122.102       # 密码 rocky,IP 永久固定

7. 验证清单

bash
# ① Multus / CNI 插件
kubectl get pods -n kube-system | grep multus          # Running
ls /opt/cni/bin/bridge                                  # 存在

# ② NAD
kubectl get nad -n default                              # br-physnet

# ③ VMI 双网卡
kubectl get vmi rocky9-vm2 -o jsonpath='{.status.interfaces}' | jq
# 期望看到 2 个接口 (default + br-net)

# ④ VM 内部 (virtctl console rocky9-vm2)
ip a                     # eth1 有 192.168.122.102/24
ip route                 # 默认路由存在
ping -c2 192.168.122.1   # 网关通
cloud-init status --long # 确认无报错

# ⑤ 外部直连
ssh root@192.168.122.102                    # 从办公网/其他主机
virtctl restart rocky9-vm2 && sleep 60 && ssh root@192.168.122.102   # 重启后 IP/密码依旧

8. FAQ 与常见问题

#现象原因与处理
1eth1 没有拿到 IPnetworkData 渲染失败。查 /var/log/cloud-init-output.log,改用 3.5 的 runcmd 兜底
2VM 能 ping 通网关,外部 ping 不通 VMbridge: 检查 br0 成员是否含物理网卡;macvlan: 检查 master 网卡名;排查交换机端口隔离
3ssh: Connection refusedVM 内 sshd 未启动或 firewalld 拦截,见 4.2
4root 登录提示 Permission denied (publickey)PermitRootLogin prohibit-password 未改,见 4.2
5重启后密码被重置cloud-init 每次开机重跑(见 5.1),把新密码同步进 YAML
6配置双网卡后无法热迁移必须保留默认 pod: {} 网络;迁移通道依赖它
7virt-launcher 报找不到网络 br-physnetNAD 没建在 VM 所在命名空间(default),或名称拼写不一致
8节点建桥后失联通过 IPMI/控制台恢复;参考 3.3 先建桥、后切连接的顺序
9重启后 VM 的 IP 变了Pod IP 是动态资源,无保留保证;见第 9 章,推荐方案 A/B

9. 重启后 IP 保留机制

9.1 为什么 masquerade 模式重启后 IP 会变

virtctl restart / VMI 删除重建 / 节点驱逐的实际过程:

删除旧 VMI → 创建新 VMI → 新的 virt-launcher Pod → Calico 重新分配 Pod IP

Pod IP 在 Kubernetes 设计中是临时资源,不存在重启保留的保证——这不是配置问题,是设计如此。

9.2 三种"重启保留"途径对比

途径重启后重建/换节点后建议
方案 B: Multus + networkData 静态 IP✅ 保留✅ 保留(目标节点需建好桥)需要固定物理网段 IP 时推荐
方案 A: NodePort Service入口不变入口不变只需稳定访问时推荐
Calico ipAddrs 注解钉死 Pod IP✅ 保留IPPool 内可用即保留⚠️ 实验性,不推荐生产

方案 B 天然"重启保留"的原理:静态 IP 写在 networkData 中,而 KubeVirt 的 cloud-init 每次开机都会重跑 (即 5.1 的机制,此处反而成为优点)——静态网络配置每次开机被重新施加, 因此无论重启多少次、VMI 重建多少次,eth1 永远是 192.168.122.102

9.3 偏门技巧:Calico 注解钉死 Pod IP(仅应急)

Calico 支持 Pod 级别申请指定 IP,可通过 VMI 模板注解尝试透传给 virt-launcher Pod:

yaml
  template:
    metadata:
      annotations:
        cni.projectcalico.org/ipAddrs: '["10.22.100.102"]'

限制与风险:

  • IP 必须在 Calico IPPool(10.22.0.0/16)范围内且未被占用;
  • 注解能否透传需先验证: kubectl get pod virt-launcher-rocky9-vm2-xxxx -o jsonpath='{.metadata.annotations}' | grep ipAddrs
  • 非 KubeVirt 官方支持用法,多台 VM 使用时有 IP 冲突风险;正路仍是方案 A / B。

9.4 两个补充细节

  • 客户机内部 sudo reboot:QEMU 执行硬件复位,Pod 不重建,Pod IP 通常不变; 但经 virtctl stop/restart(guest-agent 优雅关机)会重建 VMI → 新 Pod → 新 IP。不要依赖内部重启作为保留策略;
  • virtctl pause/unpause:只是冻结/恢复 vCPU,Pod 不重建,IP 不变。

附:方案选型速查

需求推荐做法章节
只要 SSH 入口稳定NodePort Service2
VM 拥有物理网段固定 IP(可动节点网卡)Multus + bridge3
VM 拥有物理网段固定 IP(不想动节点网卡)Multus + macvlan3.4
root 密码 SSH 登录cloud-init ssh_pwauth + disable_root: false4
永久改密码改 YAML chpasswd.list + virtctl restart5