主题
05 创建带双网卡的 VM:管理网 + 10.181.91.x 直通
本章目标:创建一台带两块网卡的 VM——
eth0走 Harvester 管理网(集群内访问/控制台),eth1走net-91直接获得10.181.91.x,并能从外部 SSH 进去。这也是后续模板化批量创建(第 6 章)的原型。
5.1 先接受五个事实(官方文档明确写出,与直觉相反)
| # | 事实 | 影响 |
|---|---|---|
| 1 | Management Network(mgmt)上的 VM IP 只能在集群节点内部访问 | 外部机器不能直接 SSH 到 VM 的 mgmt IP,必须走 VLAN 网络或 LoadBalancer |
| 2 | VM 重启后 mgmt IP 会变(非典型行为,官方明确提示) | 不要把 mgmt IP 写进任何配置/监控/DNS;要稳定入口就用 K8s Service 或 VLAN 网络 |
| 3 | 连到 mgmt 的 VM 网卡 MTU = 1450(Calico/Flannel 每包 50 字节开销) | VM 内 eth0 是 1450 而不是 1500;跨网卡传大包时要注意 |
| 4 | VLAN 网络下IPv4 地址靠 DHCPv4 下发,VM 应配置为 DHCP 获取 | 没有外部 DHCP 时必须用静态 cloud-init 或 Managed DHCP(4.8) |
| 5 | 一块网卡在 mgmt、另一块在 VLAN 时,VLAN 网关会覆盖 VM 的默认路由,导致节点访问不到 VM 的 mgmt IP | 官方 note 原话;必须按 5.4 处理路由,否则「VM 起来了但控制台连不上」 |
这五条解释了为什么"直连外部网络"必须用 VLAN 网络,也解释了双网卡最常见的坑。
5.1.1 三种取地址方式怎么选
| 方式 | 前提 | 优点 | 缺点 | 本环境 |
|---|---|---|---|---|
| 外部 DHCP | VLAN 91 上有 DHCP 服务器 | VM 侧零配置;迁移/重启自动续租 | 地址不可预测,需 DHCP 保留才能固定 | 若有 DHCP 则首选 |
| 静态 IP(cloud-init networkData) | 无 DHCP,或要求地址固定 | 地址完全可控,便于登记与防火墙 | 每台 VM 一份 networkData;改 IP 要重建 cloud-init | 本书默认 |
| Managed DHCP | 装了 4.8 的 addon | 集中管理租约,UI 可见 | 实验特性;不支持 RELEASE;改 IPPool 要重启 agent | 规模化后可选 |
⚠️ 无论哪种方式,VM 的 MAC 地址是 Harvester 自动生成的(除非显式指定),因此外部 DHCP 的「MAC 保留」策略在 VM 重建后会失效。需要固定 IP 就用静态方式或 DHCP 池 + 记录映射。
5.1.2 接口类型(Type)只有两个选项
| Type | 含义 | 何时用 |
|---|---|---|
bridge | 通过 Linux bridge 接入(VLAN 网络必须用它) | eth1(net-91) |
masquerade | 通过 iptables NAT 出去(Pod 网络) | eth0(Management Network) |
5.1.3 两个「反直觉」的地址现象,先知道就不慌
eth0(masquerade)的地址为什么长这样? VM 内 ip -br addr 你会看到:
text
eth0 UP 10.52.7.31/32 ... ← /32!掩码不是网段
ip route: default via 169.254.1.1 dev eth0 ← 网关不在任何网段里这是 KubeVirt masquerade 的设计:VM 拿到的是 Pod 网段(10.52.0.0/16)里的一个地址,但掩码是 /32,网关是链路本地地址 169.254.1.1(由 virt-launcher Pod 应答)。所有出向包都发给这个「假网关」,由宿主机 NAT 改写源地址后从 mgmt 网出去。所以「网关不在网段里」不是配错了,是正常现象(原理图见 1.3.0 ④)。
Guest 里的网卡名不一定是 eth0/eth1。 本书示例统一用 eth0/eth1,但部分镜像启用了 systemd 可预测命名,你会看到 ens3/ens4、enp1s0 等。以 VM 内 ip -br link 的实际输出为准:网卡顺序由 PCI 枚举顺序决定,第一块网卡(Management Network)总是排前面,第二块(net-91)排后面;不确定时用 MAC 地址与 kubectl get vmi <vm> -o jsonpath='{.status.interfaces[*].mac}' 对照确认。
5.2 UI 创建双网卡 VM(推荐第一次这样做,便于理解字段)
前置:已上传镜像(Advanced → Images → Create,填 URL 或直接上传),记下镜像名。
- Virtual Machines → Create。
- 基本信息:
- Name:
vm-web-01 - Namespace:
default - (可选)VM Template:先不选,第 6 章再模板化
- Name:
- Basics 标签页:
- CPU:
2,Memory:4 Gi - SSHKey:选择或上传公钥(对应
KeyPair对象,会注入 cloud-init)
- CPU:
- Volumes 标签页:
- 根盘:Image Volume → 选镜像 → StorageClass
longhorn(默认)→ 大小 ≥ 镜像要求 - cloud-init 盘会自动出现(
cloudinitdisk)
- 根盘:Image Volume → 选镜像 → StorageClass
- Networks 标签页(本章重点):
- 默认已有一行 Management Network,Type =
masquerade,保留它作为eth0; - 点 Add Network:
- Network Name:选
default/net-91(第 4 章创建的 VM 网络) - Type:
bridge - Model:
virtio
- Network Name:选
- 顺序决定 Guest 内网卡顺序:Management Network 在上 →
eth0;net-91在下 →eth1。
- 默认已有一行 Management Network,Type =
- Node Scheduling / VM Scheduling:初期保持 Any available node,便于验证自动平衡与迁移。
- Advanced Options → Cloud Config:填入
userdata与networkData(可复制内容见 5.3.2)。UI 会把它们存成一个 Secret(名为<vm>-cloudinit)。 - 提交,观察 Virtual Machines 列表状态从
Starting→Running。
bash
# UI 之外随时可用命令核对
kubectl get vm,vmi -n default
kubectl get vmi vm-web-01 -n default -o jsonpath='{.status.interfaces[*]}' | python3 -m json.tool 2>/dev/null || \
kubectl get vmi vm-web-01 -n default -o yaml | sed -n '/interfaces:/,/^ [a-z]/p'vmi.status.interfaces 里能看到每块网卡的 name、mac、ipAddress(mgmt 那块会有 IP,VLAN 那块通常为空,因为地址由 Guest 内部配置)。
5.3 用 YAML 创建(可复制、可进 Git、可批量)
强烈建议:先用 5.2 的 UI 建一台,然后
kubectl get vm vm-web-01 -n default -o yaml导出,把dataVolumeTemplates(镜像 PVC 名、StorageClass、容量)等环境相关字段抄进你的模板。网络与 cloud-init 部分则完全照抄下面的内容——这两部分才是本章的重点,也是版本间最稳定的部分。
5.3.1 查镜像与 PVC 名
bash
kubectl get vmimages -A
# NAMESPACE NAME DISPLAY-NAME SIZE AGE
# default image-79hdq focal-server-cloudimg-amd64.img 566886400 5h
kubectl get vmimage image-79hdq -n default -o jsonpath='{.status.pvcName}{"\n"}'
kubectl get pvc -n default | grep image-5.3.2 cloud-init Secret(静态 IP 版)
yaml
# appendix/vm/01-secret-vm-web-01-cloudinit.yaml
apiVersion: v1
kind: Secret
metadata:
name: vm-web-01-cloudinit
namespace: default
type: Opaque
stringData:
userdata: |
#cloud-config
hostname: vm-web-01
timezone: Asia/Shanghai
manage_etc_hosts: true
users:
- name: ops
groups: [sudo, docker]
shell: /bin/bash
sudo: ALL=(ALL) NOPASSWD:xxx
ssh_authorized_keys:
- ssh-ed25519 AAAA...REPLACE_ME... ops@workstation
packages:
- qemu-guest-agent
- curl
runcmd:
- systemctl enable --now qemu-guest-agent
final_message: "cloud-init done after $UPTIME seconds"
networkdata: |
version: 2
ethernets:
eth0: # Management Network(masquerade / Pod 网络)
dhcp4: true
dhcp4-overrides:
use-dns: false # DNS 统一由 eth1 侧提供,避免两份 resolv.conf 打架
route-metric: 900 # 关键:mgmt 默认路由降级,确保 eth1 的 metric 100 永远生效
eth1: # net-91(VLAN 91,bridge)
dhcp4: false
addresses:
- 10.181.91.101/24
nameservers:
addresses:
- 10.181.0.2
search:
- example.local
routes:
- to: default
via: 10.181.91.1
metric: 100 # 关键:显式低 metric,确保默认路由走 eth1⚠️ Secret 的 key 名是
userdata/networkdata(小写、无连字符)。不同版本/来源的示例可能写成userData,以你环境导出的为准:kubectl get secret vm-web-01-cloudinit -o jsonpath='{.data}' | python3 -c 'import sys,json;print(list(json.load(sys.stdin)))'
5.3.3 VirtualMachine(双网卡)
yaml
# appendix/vm/02-vm-web-01.yaml
apiVersion: kubevirt.io/v1
kind: VirtualMachine
metadata:
name: vm-web-01
namespace: default
labels:
app: vm-web
tier: web
annotations:
harvesterhci.io/volumeClaimClass: longhorn
spec:
running: true
template:
metadata:
labels:
app: vm-web
tier: web
harvesterhci.io/creator: harvester
spec:
domain:
machine:
type: q35
cpu:
cores: 2
sockets: 1
threads: 1
resources:
requests:
memory: 4Gi
devices:
disks:
- name: disk-0
disk:
bus: virtio
- name: cloudinitdisk
disk:
bus: virtio
interfaces:
- name: default # → Guest 内 eth0
masquerade: {}
model: virtio
- name: nic-1 # → Guest 内 eth1
bridge: {}
model: virtio
networks:
- name: default
pod: {} # Management Network
- name: nic-1
multus:
networkName: default/net-91 # 第 4 章创建的 NAD
volumes:
- name: disk-0
dataVolume:
name: vm-web-01-disk-0
- name: cloudinitdisk
cloudInitNoCloud:
secretRef:
name: vm-web-01-cloudinit
networkDataSecretRef:
name: vm-web-01-cloudinit
dataVolumeTemplates:
- metadata:
name: vm-web-01-disk-0
namespace: default
spec:
pvc:
accessModes:
- ReadWriteMany
volumeMode: Block
storageClassName: longhorn
resources:
requests:
storage: 40Gi
source:
pvc:
name: image-79hdq # ← 换成 5.3.1 查到的镜像 PVC 名
namespace: defaultbash
kubectl apply -f appendix/vm/01-secret-vm-web-01-cloudinit.yaml
kubectl apply -f appendix/vm/02-vm-web-01.yaml
kubectl get vm vm-web-01 -n default -w # Starting → Running
kubectl get vmi vm-web-01 -n default -o wide
kubectl get pvc -n default | grep vm-web-015.3.4 三个变体
变体 A:VLAN 侧用 DHCP(外部有 DHCP 服务器)
yaml
networkdata: |
version: 2
ethernets:
eth0:
dhcp4: true
eth1:
dhcp4: true
dhcp-identifier: mac # ← 必写,见下方警告
dhcp4-overrides:
route-metric: 100 # 让 eth1 的默认路由优先⚠️
dhcp-identifier: mac不是可选项。官方明确说明:Ubuntu 16.04 之后的镜像默认由 netplan 管理网络,而 netplan 默认用 machine-id 作为 DHCP client identifier。若省略该字段,从快照/备份恢复或克隆出来的 VM 会拿到与原机相同的 IP,造成地址冲突。批量创建(第 6 章)时尤其致命。
变体 B:使用 Managed DHCP(4.8)——networkdata 与变体 A 相同(同样要写 dhcp-identifier: mac),地址由 Harvester 的 DHCP agent 下发;可另用 field.cattle.io/ports 注解在 UI 上展示端口(见 5.6.2)。
变体 C:只要 VLAN 网卡,彻底去掉 Management Network(官方明确:配了 VLAN 网络时 mgmt 是可选的)
yaml
devices:
interfaces:
- name: nic-1
bridge: {}
model: virtio
networks:
- name: nic-1
multus:
networkName: default/net-91此时 Guest 内只有一块网卡(通常仍是 eth0),没有默认路由冲突,最简单;代价是失去集群内的 mgmt IP(UI 上不再显示 mgmt 地址)。对外提供服务、且完全由 10.181.91.x 访问的 VM,推荐这个变体。
⚠️ 接口名(
default/nic-1)在devices.interfaces与networks两处必须一一对应,写错会导致 VM 卡在Starting且事件里报网络找不到。Guest 内的网卡名(eth0/eth1)由内核按 PCI 顺序分配,与这里的 name 无关。
5.4 双网卡最容易踩的坑:默认路由冲突
5.4.1 现象与原理
现象:VM 已 Running,从外部 ping 10.181.91.101 通,但在 Harvester 节点上 ping VM 的 mgmt IP 不通,或 UI 里 VM 网络状态异常、控制台打不开。
原理(官方 note 描述的行为):
text
eth0(mgmt/masquerade):10.52.7.31/32,默认路由由 Pod 网络 DHCP 下发
eth1(net-91/bridge) :10.181.91.101/24,cloud-init 又下发一条默认路由 via 10.181.91.1
后配置的默认路由覆盖了前一条 →
节点访问 10.52.7.31 时,VM 的回包从 eth1 发给 10.181.91.1
而 10.181.91.1(业务网关)不知道 10.52.0.0/16 怎么走 → 回包被丢
→ 非对称路由,连接失败一句话:入向走 eth0,出向走 eth1,回不来。
5.4.2 诊断(在 VM 内执行)
bash
ip -br addr # 两块网卡的地址
ip route show default # 有几条默认路由?metric 各是多少?
ip route get 10.52.0.1 # 去 Pod 网段走哪个接口?← 关键
ip route get 8.8.8.8 # 去外网走哪个接口?
ip rule show # 是否有策略路由判断标准:
| 检查 | 期望 |
|---|---|
ip route get 8.8.8.8 | dev eth1(外部流量走业务网) |
ip route get 10.52.0.1 | dev eth0(集群内流量走 mgmt,靠直连路由即可) |
ip route show default | 只有 一条默认路由,或 eth1 的 metric 明显更小 |
5.4.3 三种修法(按推荐度排序)
修法一:让 eth1 的默认路由 metric 更小(本书默认)
只要 eth0 保留直连路由(10.52.x.x/32 dev eth0 或对应网段),回程就不依赖默认路由,冲突自然消失。cloud-init 里显式给 eth1 的默认路由加 metric: 100(5.3.2 已这么写):
bash
# VM 内立即验证/临时修复
sudo ip route del default dev eth0 2>/dev/null || true
sudo ip route add default via 10.181.91.1 dev eth1 metric 100修法二:不要 mgmt 的默认路由,只保留直连
yaml
networkdata: |
version: 2
ethernets:
eth0:
dhcp4: true
dhcp4-overrides:
use-routes: false # 不接受 mgmt 下发的默认路由
eth1:
addresses: [10.181.91.101/24]
routes:
- to: default
via: 10.181.91.1⚠️ 用
use-routes: false后,若你的 Guest 镜像把 mgmt 侧的直连路由也一并丢弃,节点将无法访问 VM 的 mgmt IP。改完必须用 5.4.2 的ip route get 10.52.0.1复核。
修法三:干脆去掉 Management Network(变体 C)
只留 VLAN 网卡,没有第二条默认路由,问题从根上消失。对外服务的 VM 首选。
5.4.4 需要 VM 同时访问管理网段和业务网段时(策略路由)
如果 VM 既要访问 10.181.0.0/24(管理网)又要走 10.181.91.0/24 出网,且两块网卡在同一物理网络的不同 VLAN,最稳的是加静态路由而非策略路由:
yaml
eth1:
addresses: [10.181.91.101/24]
routes:
- to: default
via: 10.181.91.1
metric: 100
- to: 10.181.0.0/24 # 明确管理网段的走法(若需经业务网关绕行)
via: 10.181.91.1策略路由(ip rule + 多张路由表)留给真正需要源地址选路的场景,cloud-init 里可用 routing-policy 字段声明,但会让排障复杂度翻倍——能用静态路由解决就不要上策略路由。
5.5 VM 内部验证清单
进入 VM 的方式(按可用性排序):
bash
# ① UI 控制台:Virtual Machines → vm-web-01 → Console(VNC,不依赖网络,永远可用)
# ② 集群内 SSH(走 mgmt IP,只能在节点上执行)
ssh rancher@10.181.0.11
kubectl get vmi vm-web-01 -n default -o jsonpath='{.status.interfaces[?(@.name=="default")].ipAddress}{"\n"}'
ssh ops@<上面得到的 IP>
# ③ 外部 SSH(走 VLAN 地址,本章最终目标)
ssh ops@10.181.91.101VM 内逐项检查:
bash
# 网卡与地址
ip -br link # 期望 eth0、eth1 均 UP,driver=virtio_net
ethtool -i eth1 | head -3 # driver: virtio_net
ip -br addr show eth1 # 10.181.91.101/24
# 路由(见 5.4.2)
ip route show
ip route get 8.8.8.8
# L2/L3 连通
ping -c3 10.181.91.1 # 网关
arping -c2 -I eth1 10.181.91.1 # 二层可达(能拿到网关 MAC 说明 VLAN 通了)
ping -c3 10.181.91.11 # 同网段其他机器
ping -c3 10.181.0.2 # 跨网段(DNS 服务器)
# DNS
resolvectl status 2>/dev/null | head -20 || cat /etc/resolv.conf
getent hosts harvester.example.local
dig +short @10.181.0.2 harvester.example.local
# MTU(mgmt 侧应为 1450,VLAN 侧 1500)
ip link show eth0 | grep -o 'mtu [0-9]*'
ip link show eth1 | grep -o 'mtu [0-9]*'
ping -M do -s 1472 -c2 10.181.91.1 # 1500 MTU 不分片测试
# cloud-init 是否真的跑完
cloud-init status --long
sudo tail -30 /var/log/cloud-init-output.log
# qemu-guest-agent(UI 里能否看到 Guest 信息、能否优雅关机都靠它)
systemctl status qemu-guest-agent --no-pager | head -5⚠️ qemu-guest-agent 决定 Harvester 能否看到「第二块网卡」的信息。官方说明:guest agent 负责把 VM、用户、文件系统以及 secondary networks 的信息回传给宿主机。没装/没启动 qga 时,UI 上 VLAN 网卡的 IP 一栏会一直是空的——这不是网络不通,只是没人上报。新建 VM 时 UI 的 "Install guest agent" 默认勾选;用 YAML 建 VM 时必须自己在 cloud-init 里装(5.3.2 已包含)。
openSUSE < 15.3 的服务名是
qemu-ga.service而不是qemu-guest-agent.service。
| 检查项 | 期望 |
|---|---|
eth1 地址 | 10.181.91.101/24 |
ping 10.181.91.1 | 通 |
arping 网关 | 能收到应答(证明 VLAN 标签正确) |
| 默认路由 | 走 eth1 |
| 到 Pod 网段路由 | 走 eth0(若保留 mgmt 网卡) |
cloud-init status | done |
qemu-guest-agent | active (running) |
5.6 从外部访问 VM
5.6.1 直连(本环境主路径)
VM 在 10.181.91.0/24 里有地址后,同网段/可路由到该网段的机器可直接访问:
bash
ping 10.181.91.101
ssh ops@10.181.91.101
curl -I http://10.181.91.101:8080
nmap -p22,80,443,8080 10.181.91.1015.6.2 让 UI 显示 VM 的端口(field.cattle.io/ports)
如果用了 Managed DHCP(4.8),可在 VM 上声明端口,UI 的 Virtual Machines 页面会显示对应地址与端口:
bash
kubectl annotate vm vm-web-01 -n default --overwrite \
'field.cattle.io/ports=[[{"port":22,"protocol":"TCP"},{"port":8080,"protocol":"TCP"}]]'⚠️ 这个注解只影响 UI 展示,不会真的去交换机或防火墙上开端口,也不做端口转发。
5.6.3 不想给 VM 配 VLAN 地址时:LoadBalancer Service
若 VM 只有 mgmt 网卡(或想让外部通过一个稳定 VIP 访问),可以创建 LoadBalancer 类型的 Service,从 LB 地址池(本环境规划 10.181.91.50-.99)分配 VIP。
先建 LB 的 IPPool(注意是 networking.harvesterhci.io 这个 group,与给 VM 发 DHCP 地址的 network.harvesterhci.io 完全不同):
yaml
# appendix/network/07-ippool-lb-net-91.yaml
apiVersion: networking.harvesterhci.io/v1beta1
kind: IPPool
metadata:
name: pool-lb-net-91
spec:
subnet: 10.181.91.0/24
gateway: 10.181.91.1
ranges:
- rangeStart: 10.181.91.50
rangeEnd: 10.181.91.99bash
# 字段名以集群实际 CRD 为准
kubectl api-resources --api-group=networking.harvesterhci.io
kubectl explain ippool.networking.harvesterhci.io.spec --recursive
kubectl apply -f appendix/network/07-ippool-lb-net-91.yaml
kubectl get ippools.networking.harvesterhci.io -A # 必须带全限定名,否则会列到另一个 IPPool再建 Service:
yaml
# appendix/network/08-svc-lb-vm-web-01.yaml
apiVersion: v1
kind: Service
metadata:
name: vm-web-01-lb
namespace: default
annotations:
cloudprovider.harvesterhci.io/vip: "10.181.91.60" # 或留空由 IPPool 自动分配
cloudprovider.harvesterhci.io/vip-type: "fixed"
spec:
type: LoadBalancer
selector:
kubevirt.io/vmiName: vm-web-01 # 注意:selector 指向 VMI 标签
ports:
- name: ssh
port: 22
targetPort: 22
- name: http
port: 8080
targetPort: 8080LB 侧的 IPPool(networking.harvesterhci.io group)与完整用法见 6.5 与附录 appendix/network/。
5.6.4 外部访问不通时的排查顺序(严格自下而上)
text
① 交换机:端口 trunk 是否放行 VLAN 91、是否 LACP 协商成功
② 宿主机:bridge vlan show dev cn-vm-bo | grep 91
③ 宿主机:bridge fdb show | grep <VM 的 MAC> ← MAC 有没有学到?在哪个口?
④ 宿主机:tcpdump -eni cn-vm-br -e 'vlan 91' ← 有没有带 tag 的包?
⑤ VM 内:ip addr / ip route / arping 网关
⑥ VM 内:防火墙(ufw/firewalld)是否放行
⑦ 客户端:是否与 10.181.91.0/24 路由可达、有无 ACL 拦截拿 VM 的 MAC:
bash
kubectl get vmi vm-web-01 -n default \
-o jsonpath='{range .status.interfaces[*]}{.name}{"\t"}{.mac}{"\n"}{end}'第 ③ 步是性价比最高的判断点:MAC 学到了但 ping 不通 → 问题在 VM 内部或 L3;MAC 学不到 → 问题在 VLAN/bridge/交换机。
5.7 最佳实践:内部互通 + 固定 IP 的标准形态
把需求拆开:「内部网络通」= VM 能被集群节点访问(mgmt 通道,用于集群内互访、控制台兜底、LB 后端探测);「固定 IP」= 外部系统能用不变的地址直连 VM。两个都要,答案只有一个:双网卡 + mgmt 侧 DHCP(高 metric)+ VLAN 侧静态 IP(低 metric)。
5.7.1 为什么是这套组合(与其他方案对比)
| 方案 | 集群内可达 | 固定 IP | 外部直连 | 结论 |
|---|---|---|---|---|
| 只接 mgmt(masquerade) | ✅ | ❌ 重启会变 | ❌ | 只能用于纯内部工作负载 |
| 只接 net-91 静态(变体 C) | ❌ | ✅ | ✅ | 最简单,但失去集群内通道 |
| 双网卡 + 两侧都 DHCP | ✅ | ❌ | ✅ | 地址不可控,靠 DHCP 保留才能固定 |
| 双网卡 + mgmt DHCP + VLAN 静态 | ✅ | ✅ | ✅ | 本书标准形态 |
| 双网卡 + LB VIP(5.6.3) | ✅ | ✅(VIP 固定) | ✅(经 VIP) | 适合「只暴露服务端口」的场景 |
5.7.2 标准形态的完整定义
所有决策一次写死,第 6 章的批量模板就按这张表生成:
| 项 | 标准值 | 理由 |
|---|---|---|
| eth0 网络 | Management Network,masquerade,DHCP | 集群内部通道 |
| eth0 默认路由 | route-metric: 900 | 永远输给 eth1,杜绝双默认路由撞车(8.4 案例 3) |
| eth0 DNS | use-dns: false | DNS 只由 eth1 下发,resolv.conf 不打架 |
| eth1 网络 | default/net-91,bridge,静态 IP | 固定地址,外部直连 |
| eth1 默认路由 | via 10.181.91.1 metric 100 | 唯一的「主」默认路由 |
| eth1 DNS | 10.181.0.2 + search domain | 内网域名解析 |
| IP 分配 | vms.csv 台账 + gen-vms.sh 校验 | 一台一 IP,避开保留段,可审计 |
networkData 最终形态(与 5.3.2 及 6.5.3 模板完全一致):
yaml
networkdata: |
version: 2
ethernets:
eth0: # 内部通道:Management Network
dhcp4: true
dhcp4-overrides:
use-dns: false
route-metric: 900 # mgmt 默认路由降级为兜底
eth1: # 固定 IP:net-91
dhcp4: false
addresses: [10.181.91.101/24]
nameservers:
addresses: [10.181.0.2]
search: [example.local]
routes:
- to: default
via: 10.181.91.1
metric: 100⚠️
dhcp-identifier: mac只需要写在走外部 DHCP / Managed DHCP 的网卡上(变体 A/B);eth0 的 DHCP 由 KubeVirt 按 Pod 应答,不存在撞车问题,无需加。
5.7.3 实施步骤
- 确认网络底座就绪(第 4 章清单):
kubectl get clusternetwork cn-vm、vlanstatus12 条全 Ready、NADdefault/net-91Ready。 - 从台账取号:在
vms.csv/ip-registry-*.txt里取一个.101–.200未占用地址(gen-vms.sh会自动拦截保留段与冲突)。 - 创建 VM:按 5.3 的 Secret + VM YAML,或直接用第 6 章
gen-vms.sh批量。 - 等待就绪:
kubectl get vm <name> -w到 Running,VM 内cloud-init status为done。 - 跑验收脚本(5.7.4):全绿才算交付。
- 登记台账:把 IP↔VM 对应关系写回
ip-registry。
5.7.4 一键验收脚本
bash
#!/usr/bin/env bash
# appendix/ops/vm-net-verify.sh
# 双网卡 VM(内部互通 + 固定 IP)一键验收
# 用法: ./vm-net-verify.sh <vm-name> [namespace] [ssh-user]
# 前提: 本机 kubectl 可连集群;本机到固定 IP 网段路由可达;VM 内已注入本机公钥
set -uo pipefail
VM="${1:-}"; NS="${2:-default}"; SSH_USER="${3:-ops}"
[ -n "$VM" ] || { echo "用法: $0 <vm-name> [namespace] [ssh-user]"; exit 2; }
NET_PREFIX="${NET_PREFIX:-10.181.91.}" # 固定 IP 网段前缀
GW="${GW:-10.181.91.1}" # VLAN 网关
VIP="${VIP:-10.181.0.100}" # 集群 VIP(内部连通性验证目标)
OK=$'[ OK ]'; BAD=$'[FAIL]'; WRN=$'[WARN]'; FAIL=0
ok(){ printf '%s %s\n' "$OK" "$*"; }
no(){ printf '%s %s\n' "$BAD" "$*"; FAIL=$((FAIL+1)); }
wn(){ printf '%s %s\n' "$WRN" "$*"; }
echo "== ① 平台层:VM 与双网卡 =="
st=$(kubectl get vm -n "$NS" "$VM" -o jsonpath='{.status.printableStatus}' 2>/dev/null)
[ "$st" = "Running" ] && ok "VM $NS/$VM Running" || { no "VM 状态=${st:-不存在},终止"; exit 1; }
nad=$(kubectl get vmi -n "$NS" "$VM" -o jsonpath='{.spec.networks[*].multus.networkName}' 2>/dev/null)
echo "$nad" | grep -q 'net-91' && ok "第二块网卡挂在 net-91($nad)" || no "未找到 net-91 网卡($nad)"
podnet=$(kubectl get vmi -n "$NS" "$VM" -o jsonpath='{.spec.networks[?(@.pod)].name}' 2>/dev/null)
[ -n "$podnet" ] && ok "保留 Management Network(内部通道)" || wn "没有 mgmt 网卡(变体 C,放弃集群内可达)"
echo "== ② 固定 IP:从 cloud-init Secret 解析 =="
VMIP=$(kubectl get secret -n "$NS" "${VM}-cloudinit" -o jsonpath='{.data.networkdata}' 2>/dev/null \
| base64 -d 2>/dev/null | awk '/eth1:/{f=1} f && /^ *- +[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/{print $2; exit}')
VMIP="${VMIP%/*}"
case "$VMIP" in
${NET_PREFIX}*) ok "cloud-init 固定 IP = $VMIP" ;;
"") no "未能从 Secret ${VM}-cloudinit 解析到 eth1 静态 IP"; exit 1 ;;
*) no "解析到 $VMIP,不在 ${NET_PREFIX}0/24 网段" ;;
esac
echo "== ③ 外部可达性(本机 → VM)=="
ping -c2 -W2 "$VMIP" >/dev/null 2>&1 && ok "ping $VMIP 通" || no "ping $VMIP 不通(回第 8 章 L0–L2 查)"
ssh -o BatchMode=yes -o ConnectTimeout=5 "$SSH_USER@$VMIP" true 2>/dev/null \
&& ok "SSH $SSH_USER@$VMIP 可登录" || { no "SSH 不通,VM 内检查终止"; exit 1; }
echo "== ④ VM 内部(经固定 IP SSH 进入)=="
vmin(){ ssh -o BatchMode=yes -o ConnectTimeout=5 "$SSH_USER@$VMIP" "$@" 2>/dev/null; }
nic=$(vmin "ip -br addr" | awk -v ip="$VMIP" 'index($0,ip){print $1; exit}')
[ -n "$nic" ] && ok "Guest 内 $nic 持有 $VMIP" || no "Guest 内未见 $VMIP(cloud-init 未生效?查 8.2.6)"
dev=$(vmin "ip route get 1.1.1.1" | grep -oE 'dev [^ ]+' | head -1 | awk '{print $2}')
[ "$dev" = "$nic" ] && ok "默认路由走 $nic(固定 IP 网卡)" || no "默认路由走 ${dev:-?},固定 IP 在 $nic(metric 冲突,查 8.2.7)"
vmin "ip route show default" | grep -q 'metric 900' && ok "mgmt 默认路由已降级(metric 900)" || wn "未见 mgmt 高 metric 默认路由"
vmin "ping -c2 -W2 $GW" >/dev/null && ok "VM → 网关 $GW 通" || no "VM → 网关不通"
vmin "ping -c2 -W2 $VIP" >/dev/null && ok "VM → 集群 VIP $VIP 通(内部通道正常)" || no "VM → VIP 不通(mgmt/NAT 通道异常)"
vmin "getent hosts harvester.example.local" >/dev/null && ok "DNS 解析正常" || wn "DNS 解析失败(检查 eth1 nameservers)"
vmin "systemctl is-active qemu-guest-agent" | grep -q active && ok "qemu-guest-agent running" || wn "qga 未运行(UI 看不到第二块网卡 IP,见 8.4 案例 5)"
echo
[ "$FAIL" -eq 0 ] && echo "✅ $VM 验收通过" || echo "❌ $VM 有 $FAIL 项未通过"
exit "$FAIL"bash
chmod +x appendix/ops/vm-net-verify.sh
./appendix/ops/vm-net-verify.sh vm-web-01 default ops
# 批量验收(配合第 6 章台账)
tail -n +2 appendix/vm/vms.csv | cut -d, -f1 | while read -r n; do
./appendix/ops/vm-net-verify.sh "$n" || echo "$$ 注意: $n 未通过"
done脚本按「平台层 → 固定 IP → 外部可达 → VM 内部」四层递进,任何一层 FAIL 都给出下一步该去的章节;网卡名不假设 eth1,而是先定位「持有固定 IP 的那块卡」再验证默认路由,因此对 ens3/ens4 命名的镜像同样适用。
5.8 本章验证清单
| # | 检查项 | 命令 | 期望 |
|---|---|---|---|
| 1 | VM 运行 | kubectl get vm,vmi -n default | vm-web-01 Running/Ready |
| 2 | 两块网卡都在 | kubectl get vmi vm-web-01 -o yaml | grep -A4 interfaces: | default(masquerade) + nic-1(bridge) |
| 3 | 网卡挂到了正确 NAD | kubectl get vmi vm-web-01 -o yaml | grep -A3 networks: | multus.networkName: default/net-91 |
| 4 | Guest 内 eth1 有地址 | VM 内 ip -br addr show eth1 | 10.181.91.101/24 |
| 5 | 默认路由正确 | VM 内 ip route get 8.8.8.8 | dev eth1 |
| 6 | 集群内可达 mgmt | 节点上 ping <mgmt IP> | 通 |
| 7 | 外部可 SSH | 外部 ssh ops@10.181.91.101 | 成功 |
| 8 | 外部到网关 | VM 内 ping 10.181.91.1 | 通 |
| 9 | DNS | VM 内 getent hosts harvester.example.local | 解析成功 |
| 10 | MAC 已学习 | 宿主机 bridge fdb show | grep <MAC> | 出现在 cn-vm-br 相关端口 |
| 11 | cloud-init 完成 | VM 内 cloud-init status | done |
| 12 | 重启后地址不变 | virtctl restart vm-web-01(或 Guest 内 reboot / UI ⋮ → Restart) | eth1 仍是 10.181.91.101 |
第 12 项务必做:mgmt IP 重启会变,VLAN IP 不该变。如果 VLAN IP 也变了,说明 cloud-init networkData 没生效(多半是 Secret key 名或引用写错)。
下一章:把这台 VM 变成模板,用脚本一次交付几十台规格统一的机器。