主题
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.md、rocky9-vm-ops-log-20260827.md
目录
- 现状与方案选型
- 方案 A: NodePort 固定访问入口(零节点改造)
- 方案 B: Multus bridge/macvlan 获得物理网段固定 IP
- SSH root 密码登录配置
- 账号密码修改与管理
- 完整示例: rocky9-vm2 固定 IP + root 登录
- 验证清单
- FAQ 与常见问题
- 重启后 IP 保留机制
1. 现状与方案选型
1.1 当前网络现状
三台 VM 均使用默认 Pod 网络:
yaml
interfaces:
- name: default
masquerade: {} # VM 通过 NAT 借用 Pod IP 出网
networks:
- name: default
pod: {}| 项目 | 现状 | 问题 |
|---|---|---|
| VM IP | 10.22.x.x (Calico Pod IP) | 每次重建/重启可能变化,无法固定 |
| 可达性 | 仅集群内部直连,外部需经 NAT/NodePort | 外部系统无法按固定 IP 直连 |
1.2 方案对比
| 方案 | 原理 | 固定效果 | 适用场景 | 改造范围 |
|---|---|---|---|---|
| A. NodePort Service | K8s Service 作为稳定入口 | 节点IP:端口 永远不变 | 只需稳定 SSH/服务访问 | 零节点改造,10 分钟 |
| B. Multus bridge | VM 第二块网卡挂到宿主机物理网桥 | VM 拥有固定 192.168.122.x | 外部系统需按物理网段 IP 直连 | 节点建桥 + NAD + cloud-init |
| B'. macvlan | VM 网卡虚拟成物理网卡的子 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 312222.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-system3.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注意事项:
- 保留默认
pod: {}网络——KubeVirt 热迁移的数据通道走 Pod 网络,去掉后无法热迁移;metric: 100避免物理默认路由与 Pod 网络默认路由抢路由;- 多台 VM 依次分配 192.168.122.102/103/104…,避免与现有设备冲突(先
ping+arping确认);- 该模式与 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已获得物理网段静态 IP192.168.122.200,重启自动恢复, 工作站/跨节点/SSH 全部验证通过。 全部清单文件:/u02/repo/kubevirt/multus/(physnet-devices.yaml、nads.yaml、 multus-whereabouts.yaml、rocky9-vm3-static-ip.yaml)。
3B.1 已部署组件
| 组件 | 版本 | 部署方式 |
|---|---|---|
| Multus | v4.2.4(rancher/hardened-multus-cni) | RKE2 内置 rke2-multus chart(从 rke2-runtime 镜像解出),helm template 后 kubectl apply |
| Whereabouts | v0.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 踩坑链(每一步都有实测证据)
- macvlan CNI 的 master 落在 pod 内的 calico veth 上:NAD 里写
"type":"macvlan","master":"eth0"时,插件在 pod netns 里找 "eth0", 命中的是 calico 的 veth——"物理网卡"实际挂在 overlay 上,出不了物理网段。 - busybox ip 不认
mode bridge:hardened-cni-plugins 镜像里的ip是 busybox 版,ip link add ... type macvlan mode bridge会静默建成 vepa 模式。必须chroot /hostroot /usr/sbin/ip用宿主机原生 iproute2。 - KubeVirt bridge 绑定会改写/随机化 pod 网卡 MAC,multus 注解里请求的
mac不生效。 - macvlan 作桥端口收不到"他人 MAC"的单播:内核 macvlan 的 rx 过滤只认 自身 MAC + 广播/组播,promisc 也救不了。VM 的 MAC 在网桥"后面"时, 发往 VM MAC 的单播到不了 VM。
- 桥自身 MAC 遮蔽:桥会继承最小成员 MAC。若 macvlan 端口 MAC = VM MAC, 桥把 VM MAC 当成自己的,发往该 MAC 的单播被桥本地吸收;即使显式改桥 MAC, 端口的 permanent FDB 条目仍会把单播弹回原端口。
bridge fdb del报Operation not supported,无解。 - 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>)。 - tc u32 偏移以 L3 头为基准:ingress 上按以太网偏移写的规则永不命中 (
Illegal "match"/success 0);u32 值还需 4 字节对齐。正确写法见 3B.5。 - mirred 按安装时刻的 ifindex 绑定设备:veth 对重建后,指向旧设备的 过滤器全部失效。看门狗用
/run/physnet-vh200.ifindexmarker 检测 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 秒巡检,全部幂等):
- 维护
mv-131(动态池 macvlan,缺失即建); - 清理历史桥接拓扑(
br-v200/mv-200); - 维护 veth 对
vh-200↔vp-200:vp-200不在宿主侧时, 先用crictl --runtime-endpoint unix:///run/k3s/containerd/containerd.sock pods检查virt-launcher-rocky9-vm3是否存在——存在则为 pod 持有,绝不重建 (删 veth 一端会连带销毁 pod 内的另一端!);不存在则重建; - 维护 tc 规则(marker
/run/physnet-vh200.ifindex变化则全量重建):
| 设备 | 协议 | 匹配(偏移以 L3 头为基准) | 动作 |
|---|---|---|---|
| eth0 ingress | arp | tpa=c0a8 7ac8 @24/@26 | redirect vh-200 |
| eth0 ingress | ip | dst=192.168.122.200 @16 | redirect vh-200 |
| vh-200 ingress | ip | src=192.168.122.200 @12 | redirect eth0 |
| vh-200 ingress | arp | spa=c0a8 7ac8 @14/@16 | redirect eth0 |
| vh-200 ingress | all | 全匹配 | 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(操作手册)
- 选定
<IP>与<MAC>(非本地管理位,LAN 内唯一); physnet-devices.yaml:仿照ensure_vm200增加一套vh-<尾段>/vp-<尾段>+ 对应 tc 规则(0xc0a87ac8换成新 IP 的十六进制, MAC 过滤按需添加——仅靠 IP 匹配也能工作);- 新建 NAD
physnet-static-<尾段>(host-device,device=vp-<尾段>, static ipam=<IP>/24); - VM spec 增加第二网卡(multus: networkName 指向该 NAD,macAddress=
<MAC>); - 应用后重启 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: true | sshd_config → PasswordAuthentication yes |
disable_root: false | sshd_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 # 确认列表里有 sshVM 内手动修复(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 --reload4.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:NewRockyPassbash
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$xxxxxxxxxxxxxxxx5.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 与常见问题
| # | 现象 | 原因与处理 |
|---|---|---|
| 1 | eth1 没有拿到 IP | networkData 渲染失败。查 /var/log/cloud-init-output.log,改用 3.5 的 runcmd 兜底 |
| 2 | VM 能 ping 通网关,外部 ping 不通 VM | bridge: 检查 br0 成员是否含物理网卡;macvlan: 检查 master 网卡名;排查交换机端口隔离 |
| 3 | ssh: Connection refused | VM 内 sshd 未启动或 firewalld 拦截,见 4.2 |
| 4 | root 登录提示 Permission denied (publickey) | PermitRootLogin prohibit-password 未改,见 4.2 |
| 5 | 重启后密码被重置 | cloud-init 每次开机重跑(见 5.1),把新密码同步进 YAML |
| 6 | 配置双网卡后无法热迁移 | 必须保留默认 pod: {} 网络;迁移通道依赖它 |
| 7 | virt-launcher 报找不到网络 br-physnet | NAD 没建在 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 IPPod 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 Service | 2 |
| VM 拥有物理网段固定 IP(可动节点网卡) | Multus + bridge | 3 |
| VM 拥有物理网段固定 IP(不想动节点网卡) | Multus + macvlan | 3.4 |
| root 密码 SSH 登录 | cloud-init ssh_pwauth + disable_root: false | 4 |
| 永久改密码 | 改 YAML chpasswd.list + virtctl restart | 5 |