Kube-OVN为KubeVirt虚拟机分配固定IP

SuKai June 19, 2022

KubeVirt虚拟机使用固定IP是生产中比较普遍的一个需求,今天我们一起看一下如何通过Kube-OVN为虚拟机提供固定IP。

配置kube-ovn-controller启动参数

Containers:
  kube-ovn-controller:
    Container ID:  containerd://0f8d53403c9821c7b535c64d2c98e1ca130a15e1b2b878270cd9388c36d0e48a
    Image:         kubeovn/kube-ovn:v1.11.0
    Image ID:      docker.io/kubeovn/kube-ovn@sha256:fea623e68a2a81ef78102c6cfe95b96c16c054c57f4c8b9d168bd3ff620b6779
    Port:          <none>
    Host Port:     <none>
    Args:
      /kube-ovn/start-controller.sh
      --default-cidr=10.244.0.0/16
      --default-gateway=10.244.0.1
      --default-gateway-check=true
      --default-logical-gateway=false
      --default-exclude-ips=
      --node-switch-cidr=100.64.0.0/16
      --service-cluster-ip-range=10.211.0.0/16
      --network-type=geneve
      --default-interface-name=
      --default-vlan-id=100
      --pod-nic-type=veth-pair
      --enable-lb=true
      --enable-np=true
      --enable-eip-snat=true
      --enable-external-vpc=true
      --keep-vm-ip=true

创建虚拟机

spec.template.spec.interfaces.macAddress指定虚拟机网卡MAC,每次删除重建这台虚拟机时使用同一个MAC地址,与系统的网卡配置文件/etc/netplan/50-cloud-init.yaml中MAC地址一致,不需要重新进行配置。

spec.template.metadata.annotations中添加两条记录,ovn.kubernetes.io/ip_address指定kube-ovn使用IP,ovn.kubernetes.io/mac_address告诉kube-ovn网卡的MAC地址。

---
apiVersion: kubevirt.io/v1alpha3
kind: VirtualMachine
metadata:
  labels:
    kubevirt.io/vm: ubuntu-c
  name: ubuntu-c
  namespace: mail263
spec:
  running: true
  template:
    metadata:
      labels:
        kubevirt.io/vm: ubuntu-c
      annotations:
        ovn.kubernetes.io/ip_address: 172.16.3.203
        ovn.kubernetes.io/mac_address: 00:00:00:1F:C5:8F
    spec:
      domain:
        cpu:
          cores: 4
          model: host-passthrough
        memory:
          guest: 1Gi
        devices:
          disks:
          - name: bootdisk
            disk:
              bus: virtio
          - disk:
              bus: virtio
            name: cloudinitdisk
          interfaces:
          - name: default
            bridge: {}
            macAddress: 00:00:00:1f:c5:8f
        resources:
          overcommitGuestOverhead: true
      networks:
      - name: default
        pod: {}
      terminationGracePeriodSeconds: 0
      volumes:
      - name: bootdisk
        dataVolume:
          name: ubuntuboot-c
      - name: cloudinitdisk
        cloudInitNoCloud:
          userData: |-
            #cloud-config
            ssh_pwauth: True
            chpasswd:
              list: |
                ubuntu:ubuntu
              expire: False            

固定MAC

如果在KubeVirt的VM配置中指定了网卡MAC地址,那么也需要在annotation中指定kube-ovn分配固定的MAC,否则kube-ovn将会生成一个随机的MAC地址,kube-ovn的交换机上MAC地址与虚拟机MAC地址不一致,网络无法通信。

func (subnet *Subnet) GetStaticAddress(podName, nicName string, ip IP, mac string, force bool, checkConflict bool) (IP, string, error) {
	subnet.mutex.Lock()
	defer func() {
		subnet.pushPodNic(podName, nicName)
		subnet.mutex.Unlock()
	}()

	var v4, v6 bool
	if net.ParseIP(string(ip)).To4() != nil {
		v4 = subnet.V4CIDR != nil
	} else {
		v6 = subnet.V6CIDR != nil
	}
	if v4 && !subnet.V4CIDR.Contains(net.ParseIP(string(ip))) {
		return ip, mac, ErrOutOfRange
	}
	if v6 && !subnet.V6CIDR.Contains(net.ParseIP(string(ip))) {
		return ip, mac, ErrOutOfRange
	}

	if mac == "" {
		if m, ok := subnet.NicToMac[nicName]; ok {
			mac = m
		} else {
			mac = subnet.GetRandomMac(podName, nicName)
		}
	} else {
		if err := subnet.GetStaticMac(podName, nicName, mac, checkConflict); err != nil {
			return ip, mac, err
		}
	}

	if v4 {
		if existPod, ok := subnet.V4IPToPod[ip]; ok {
			pods := strings.Split(existPod, ",")
			if !util.ContainsString(pods, podName) {
				if !checkConflict {
					subnet.V4NicToIP[nicName] = ip
					subnet.V4IPToPod[ip] = fmt.Sprintf("%s,%s", subnet.V4IPToPod[ip], podName)
					return ip, mac, nil
				}
				return ip, mac, ErrConflict
			}
			if !force {
				return ip, mac, nil
			}
		}

		if subnet.V4ReservedIPList.Contains(ip) {
			subnet.V4NicToIP[nicName] = ip
			subnet.V4IPToPod[ip] = podName
			return ip, mac, nil
		}

		if split, newFreeList := splitIPRangeList(subnet.V4FreeIPList, ip); split {
			subnet.V4FreeIPList = newFreeList
			subnet.V4NicToIP[nicName] = ip
			subnet.V4IPToPod[ip] = podName
			return ip, mac, nil
		} else {
			if split, newReleasedList := splitIPRangeList(subnet.V4ReleasedIPList, ip); split {
				subnet.V4ReleasedIPList = newReleasedList
				subnet.V4NicToIP[nicName] = ip
				subnet.V4IPToPod[ip] = podName
				return ip, mac, nil
			}
		}
	} else if v6 {
		if existPod, ok := subnet.V6IPToPod[ip]; ok {
			pods := strings.Split(existPod, ",")
			if !util.ContainsString(pods, podName) {
				if !checkConflict {
					subnet.V6NicToIP[nicName] = ip
					subnet.V6IPToPod[ip] = fmt.Sprintf("%s,%s", subnet.V6IPToPod[ip], podName)
					return ip, mac, nil
				}
				return ip, mac, ErrConflict
			}
			if !force {
				return ip, mac, nil
			}
		}

		if subnet.V6ReservedIPList.Contains(ip) {
			subnet.V6NicToIP[nicName] = ip
			subnet.V6IPToPod[ip] = podName
			return ip, mac, nil
		}

		if split, newFreeList := splitIPRangeList(subnet.V6FreeIPList, ip); split {
			subnet.V6FreeIPList = newFreeList
			subnet.V6NicToIP[nicName] = ip
			subnet.V6IPToPod[ip] = podName
			return ip, mac, nil
		} else {
			if split, newReleasedList := splitIPRangeList(subnet.V6ReleasedIPList, ip); split {
				subnet.V6ReleasedIPList = newReleasedList
				subnet.V6NicToIP[nicName] = ip
				subnet.V6IPToPod[ip] = podName
				return ip, mac, nil
			}
		}
	}
	return ip, mac, ErrNoAvailable
}

查看虚拟机系统网卡

ubuntu@ubuntu-c:~$ cat /etc/netplan/50-cloud-init.yaml
# This file is generated from information provided by the datasource.  Changes
# to it will not persist across an instance reboot.  To disable cloud-init's
# network configuration capabilities, write a file
# /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg with the following:
# network: {config: disabled}
network:
    ethernets:
        enp1s0:
            dhcp4: true
            match:
                macaddress: 00:00:00:1f:c5:8f
            set-name: enp1s0
    version: 2
ubuntu@ubuntu-c:~$ ip a
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host
       valid_lft forever preferred_lft forever
2: enp1s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP group default qlen 1000
    link/ether 00:00:00:1f:c5:8f brd ff:ff:ff:ff:ff:ff
    inet 172.16.3.203/24 brd 172.16.3.255 scope global dynamic enp1s0
       valid_lft 86266506sec preferred_lft 86266506sec
    inet6 fe80::200:ff:fe1f:c58f/64 scope link
       valid_lft forever preferred_lft forever
ubuntu@ubuntu-c:~$ ping -c 1 8.8.8.8
PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
64 bytes from 8.8.8.8: icmp_seq=1 ttl=115 time=42.8 ms

--- 8.8.8.8 ping statistics ---
1 packets transmitted, 1 received, 0% packet loss, time 0ms
rtt min/avg/max/mdev = 42.762/42.762/42.762/0.000 ms
ubuntu@ubuntu-c:~$

查看OVN交换机

可以看到ubuntu-c.mail263端口上的MAC和IP与虚拟机一致。

ningsuan@ubuntuserver:~/yaml/vms$ kubectl ko nbctl show 0629dbe7-beab-45f1-8d42-0ce2f2a89a79
switch 0629dbe7-beab-45f1-8d42-0ce2f2a89a79 (mail263)
    port localnet.mail263
        type: localnet
        addresses: ["unknown"]
    port ubuntu-b.mail263
        addresses: ["00:00:00:F1:B6:40 172.16.3.202"]
    port mailserver.mail263
        addresses: ["00:00:00:68:56:C6 172.16.3.250"]
    port ubuntu-a.mail263
        addresses: ["00:00:00:D8:95:33 172.16.3.201"]
    port ubuntu-c.mail263
        addresses: ["00:00:00:1F:C5:8F 172.16.3.203"]
ningsuan@ubuntuserver:~/yaml/vms$