Open vSwitch + Libvirt で VLAN over LACP (802.3ad) による VM 間通信

LACP による bonding は、基本的に物理的なネットワークスイッチと物理マシンを用いて行われるが、物理環境への移行前の事前検証などで VM 上で LACP を試したいケースがある。
本記事では、KVM 上で Open vSwitch を使い、VM 間で VLAN over LACP を構築する手順を解説する。

* (1) ホストに Open vSwitch のスイッチを作成する
* (2) Terraform で VM （Open vSwitch への tap 作成を含む） を自動プロビジョニングする

本記事で説明する VM およびネットワークの構成図は次の通り：

![overview.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/39ba5bc9-5d65-2015-6449-7d0c31b71091.png)

サンプルコード：

https://github.com/sawa2d2/vlan-over-lacp

## 前提条件
ホスト OS は Rocky Linux 9.5 を使用する。また、事前に次のソフトウェアがインストールされているものとする：
- KVM packages
  - `qemu-kvm`
  - `libvirt`
- Terraform
  - `crdtools`
- Open vSwtich

## 1. VM 1 台での LACP 構築
土台となる OVS bridge を手動で作成しておき、portと Linux の ネットワークインタフェース部分を自動で作成する。

### 1.1 OVS Bridge の作成
`ovsbr0` という名前の OVS Bridge を作成する：

```bash
$ sudo ovs-vsctl add-br ovsbr0
```

OVS Bridge `ovsbr0` が作成されたことを確認する：
```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port ovsbr0
            Interface ovsbr0
                type: internal
    ovs_version: "3.3.1"
```

Bridge が作成されると、同名の (例：`ovsbr0`) Port および Interface のセットが自動的に作成され、これらを削除することはできない。
Bridge と Port は物理スイッチで例えるとそれぞれスイッチ本体と LAN ポートに対応する概念であり、Interface は Linux 上で認識される Network interface で、`ip a`　コマンドから確認できる。 

```
$ ip a
...
4: ovs-system: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether fe:56:9a:a7:6f:8f brd ff:ff:ff:ff:ff:ff
5: ovsbr0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
    link/ether 6a:39:9a:98:50:40 brd ff:ff:ff:ff:ff:ff
```

本記事では、OVS における Bridge, Port, Interface の構成図を次のように表すことにする。
![default.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/fd44f5e1-8acd-0e9e-d4c4-1ade615e9c91.png)

### 1.2 VM の作成

#### 1.2.1 Base image の用意
本記事では VM の OS として Rocky Linux 9.5 を使用する。[Rocky Linux 公式の配布ページ](https://download.rockylinux.org/pub/rocky/9.5/images/x86_64/) よりベースイメージの `qcow2` ファイルを `/var/lib/libvirt/images` にダウンロードする：

```bash
$ sudo curl -L -o /var/lib/libvirt/images/Rocky-9-GenericCloud.latest.x86_64.qcow2 https://download.rockylinux.org/pub/rocky/9.2/images/x86_64/Rocky-9-GenericCloud.latest.x86_64.qcow2 
```

#### 1.2.2 cloudinit file の作成

次のような `cloud_init.cfg` を用意する。パスワード `<ROOT-PASSWORD>` は自身が使うものに書き換える：

```yaml:cloud_init.cfg
#cloud-config
chpasswd:
  list: |
    root:<ROOT-PASSWORD>
  expire: False
```

#### 1.2.3 Terraform file の作成
本記事では Terraform Libvirt Provider を用いて仮想 VM を自動プロビジョニングする。次の `main.tf` を作成する：
```main.tf
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.8.1"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_cloudinit_disk" "commoninit" {
  name      = "commoninit.iso"
  user_data = templatefile("cloud_init.cfg", {})
  pool      = "default"
}

resource "libvirt_domain" "vm" {
  name   = "vm1"
  vcpu   = 4
  memory = 4096

  network_interface {
    bridge = "ovsbr0"
  }

  network_interface {
    bridge = "ovsbr0"
  }

  disk {
    volume_id = libvirt_volume.volume.id
  }

  cloudinit = libvirt_cloudinit_disk.commoninit.id

  cpu {
    mode = "host-passthrough"
  }

  graphics {
    type        = "vnc"
    listen_type = "address"
  }

  # Makes the tty0 available via `virsh console`
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }
  
  xml {
    xslt = file("${path.module}/tap.xsl")
  }
}

resource "libvirt_volume" "volume" {
  name           = "system.qcow2"
  pool           = "default"
  format         = "qcow2"
  base_volume_id = "/var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2"
  size           = 500 * 1024 * 1024 * 1024
}
```
#### 1.2.4 XSLT patch の作成
次のような XSLT patch を用意̩する：

- domain に `<virtualport type="openvswitch"/>` を追加することで、Libvirt が Open vSwitch に対して port を自動的に作成できるようになる。
- `<!-- Enable Open vSwitch support `... のブロックで、tap の名前を固定するようにする。（`tap1p<N>` のような固定した名前を割り当てる）このように __tap 名を固定することが後に重要になる__。

```xml:patch.xsl
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <!-- Identity transform to copy everything as is -->
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <!-- Enable Open vSwitch support and assign a static name to each device -->
  <xsl:template match="/domain/devices/interface[@type='bridge']">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
      <xsl:if test="source/@bridge='ovsbr0'">
        <virtualport type="openvswitch"/>
      </xsl:if>
      <target>
        <xsl:attribute name="dev">
          <xsl:value-of select="concat('tap1p', count(preceding::interface[@type='bridge']) + 1)"/>
        </xsl:attribute>
      </target>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
```

:::note
Libvirt domain の Open vSwitch サポートについては 
[libvirt: Domain XML format #Bridge to LAN](https://libvirt.org/formatdomain.html#bridge-to-lan) を参照。
:::

#### 1.2.5 VM の作成

次のコマンドを実行し、VM を作成する：
```
$ ls
main.tf  cloud_init.cfg  patch.xsl
$ terraform init --upgrade
$ terraform apply -auto-approve
```

VM が作成されると、libvirt が次のように tap に対応した ports が自動で作成される（__ただし、これらは次のステップですぐに削除する。__）。

```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port tap1p1
            Interface tap1p1
        Port tap1p2
            Interface tap1p2
        Port ovsbr0
            Interface ovsbr0
                type: internal
    ovs_version: "3.3.1"
```

この時点でのネットワーク構成図を表すと下図のようになる：

![created.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/6a63e9d7-c9ee-704c-e572-45cf802a168e.png)

### 1.3 OVS 側の Bonding 設定
libvirt によって自動的作成されたポートを削除する：
```
sudo ovs-vsctl del-port ovsbr0 tap1p1
sudo ovs-vsctl del-port ovsbr0 tap1p2
```

次のコマンドを実行し、新たに bond `bond-vm1` を作成する：

```
sudo ovs-vsctl add-bond ovsbr0 bond-vm1 tap1p1 tap1p2 \
        -- set port bond-vm1 \
        lacp=active \
        bond_mode=balance-tcp \
        other_config:lacp-time=fast \
        other_config:bond-hash-policy=layer2+3
```

bond が作成されたことを確認する：

```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port ovsbr0
            tag: 10
            Interface ovsbr0
                type: internal
        Port bond-vm1
            Interface tap1p1
            Interface tap1p2
    ovs_version: "3.3.1"
```

この時点でのネットワーク構成図を表すと下図のようになる：

![bond.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/c40666c5-cf6d-d99b-d0a8-e762c4f4e089.png)


### 1.4 VM 側の Bonding 設定

次のコマンドを実行し、`eth0` と `eth1` から成る `bond0` を作成する：

```
nmcli con add type bond con-name bond0 ifname bond0 bond.options "mode=802.3ad,lacp_rate=fast,xmit_hash_policy=layer2+3" ipv6.method disabled  connection.autoconnect-slave 1
nmcli con add type ethernet con-name bond0p0 ifname eth0 master bond0 
nmcli con add type ethernet con-name bond0p1 ifname eth1 master bond0
nmcli con mod bond0 ipv4.method manual ipv4.address 10.0.0.1/24
```

:::note
bond option の説明は本記事では割愛する。
- `mode` オプションの詳細は [3.10. 異なるネットワークボンディングモード | Red Hat Product Documentation](https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_networking/the-different-network-bonding-modes_configuring-network-bonding#the-different-network-bonding-modes_configuring-network-bonding) を参照。
- `xmit_hash_policy` オプションの詳細は [3.11. xmit_hash_policy ボンディングパラメーター | Red Hat Product Documentation](https://docs.redhat.com/ja/documentation/red_hat_enterprise_linux/9/html/configuring_and_managing_networking/ref_xmit-hash-policy-bonding-parameter_configuring-network-bonding) を参照。
:::

ここで、__KVM 特有の設定として、NIC の Speed と Duplex（＝通信速度の上限および送受信方式のモード）の設定が必要になる__。これは、物理的な NIC と異なり、KVM がデフォルトで使用する NIC モデル `virtio` には Speed や Duplex の値が設定されていないためである。

:::note
この状態のままでは Bonding を有効化できない。試しに、現時点で `bond0` の有効化を試みると、次のようなエラーメッセージが延々と表示される：

```
# nmcli con up bond0
[ 1298.601300] net_ratelimit: 88 callbacks suppressed
[ 1298.601322] bond0: (slave eth1): failed to get link speed/duplex
[ 1298.601325] bond0: (slave eth0): failed to get link speed/duplex
[ 1298.705236] bond0: (slave eth1): failed to get link speed/duplex
[ 1298.705249] bond0: (slave eth0): failed to get link speed/duplex
[ 1298.809231] bond0: (slave eth1): failed to get link speed/duplex
[ 1298.809240] bond0: (slave eth0): failed to get link speed/duplex
[ 1298.913249] bond0: (slave eth1): failed to get link speed/duplex
[ 1298.913261] bond0: (slave eth0): failed to get link speed/duplex
[ 1299.017328] bond0: (slave eth1): failed to get link speed/duplex
[ 1299.017356] bond0: (slave eth0): failed to get link speed/duplex
...
```
:::

:::note
各 Interface に Speed および Duplex がセットされていないことは次のコマンドから確認できる：
```
# ethtool eth0 | grep -e Speed -e Duplex
    Speed: Unknown!
    Duplex: Unknown! (255)
# ethtool eth1 | grep -e Speed -e Duplex
    Speed: Unknown!
    Duplex: Unknown! (255)
```
:::

本記事では、各 NIC を 25G Ethernet としてエミュレートすることにする。次のコマンドを実行し、Speed: `2500` [MB/s], Duplex: `Full` (双方向で同時通信) をセットする：

```
nmcli con mod bond0p0 \
  802-3-ethernet.speed 2500 \
  802-3-ethernet.duplex full \
  802-3-ethernet.auto-negotiate no
nmcli con mod bond0p1 \
  802-3-ethernet.speed 2500 \
  802-3-ethernet.duplex full \
  802-3-ethernet.auto-negotiate no
```

設定を反映させる：
```
nmcli con up bond0
```

`bond0` が正常に起動したことを確認する：

```
$ nmcli con show
NAME         UUID                                  TYPE      DEVICE
bond0        0be0bec5-fe4f-4394-a93e-6cf99a808623  bond      bond0
bond0p0      30bedb37-1e0e-4b60-b96f-06ee3c3a56eb  ethernet  eth0
bond0p1      3cd0f51f-6d76-4a15-a1f1-b3a66cbc9922  ethernet  eth1
...
```

:::note
`virtio` に期待される挙動については次のリンクを参照：[Why is the KVM guest VM not displaying the Speed: Unknown! and Duplex: Unknown! (255)? - Red Hat Customer Portal](https://access.redhat.com/solutions/7051532)

なお、`virtio` 以外の Device Model Type を使用する場合、上記のように Speed / Duplex を手動で設定する必要はない。たとえば `e1000e` は 1Gbps、`rtl8139` は 100Mbps が既定値として設定されており、ゲストはこの値を自動で取得することができる。
```
# ethtool <e1000e-device-name> | grep -e Speed -e Duplex
        Speed: 1000Mb/s
        Duplex: Full

# ethtool <rtl8139-device-name> | grep -e Speed -e Duplex
        Speed: 100Mb/s
        Duplex: Full
```
:::


この時点で、VM 側の Bond と Open vSwitch 側の Bond の間で LACP のネゴシエーションが完了する。
ホスト側で次のコマンドを実行し、bond の `lacp_status` が `negotiated` になっていることを確かめる：
```
$ sudo ovs-appctl bond/show
---- bond-vm1 ----
bond_mode: balance-tcp
bond may use recirculation: yes, Recirc-ID : 20
bond-hash-basis: 0
lb_output action: disabled, bond-id: -1
updelay: 0 ms
downdelay: 0 ms
next rebalance: 5849 ms
lacp_status: negotiated
lacp_fallback_ab: false
active-backup primary: <none>
active member mac: fe:54:00:d6:2b:70(tap1p1)

member tap1p1: enabled
  active member
  may_enable: true

member tap1p2: enabled
  may_enable: true
```

この時点でのネットワーク構成図を表すと下図のようになる：

![bond.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/1ddc36ea-d8c2-3fb8-5bbd-4c1ac0b57053.png)


### 1.5 VLAN over bonding

ホスト側で次を実行し、Tap の各 port を Trunk port にする：
```bash
sudo ovs-vsctl set port bond-vm1 trunks=10,20
```

各 port が Trunk port になったことを確認する：

```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port ovsbr0
            tag: 10
            Interface ovsbr0
                type: internal
        Port bond-vm1
            trunks: [10, 20]
            Interface tap1p1
            Interface tap1p2
    ovs_version: "3.3.1"
```

ゲスト側で次を実行し、VLAN ports を作成する：
```
sudo nmcli con mod bond0 ipv4.method disabled ipv4.address ""
sudo nmcli con add type vlan con-name vlan10 dev bond0 id 10
sudo nmcli con add type vlan con-name vlan20 dev bond0 id 20
sudo nmcli con mod bond0 ipv4.method manual ipv4.address 10.0.10.1/24
sudo nmcli con mod bond0 ipv4.method manual ipv4.address 10.0.20.1/24
```

VLAN ports `bond0.10` と `bond0.20` が作成されたことを確認する：

```
$ nmcli con show
NAME                UUID                                  TYPE      DEVICE
bond0.10            e56ff19c-ac35-401f-a360-2c6d25b5658b  vlan      bond0.10
bond0.20            cf36a0c3-1768-4cce-93ed-fcb47f56a5cc  vlan      bond0.20
bond0               33b02755-6b66-4e68-b728-c40f53179b2f  bond      bond0
bond0p0             8d0124bd-e3f1-4dbb-8404-63230f8c4386  ethernet  eth0
bond0p1             52a93d2e-ddd6-494d-b89f-6ee06241df58  ethernet  eth1
...
```

この時点でのネットワーク構成図を表すと下図のようになる：

![vlan.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/b1c40ec6-61df-a717-71cb-a02b0aaa9782.png)


## 2. VM 2 台での LACP 構築
### 2.1. VM の作成

1章の内容をおさえた上で、次の図のような2台の VM による検証環境を改めて自動構築する。

![vlan-2vms.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/f8fad27f-89f0-de7f-c7b3-376646086c35.png)

:::note
サブネットの構成だけ表現すれば、次のようなネットワーク構成に過ぎない：
![simple.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/45bfae58-c5f9-6559-e4de-4a30f463b932.png)
:::

さきほど作成した VM と OVS Bridge は削除しておく：
```bash
# VM の削除
terraform destroy

# OVS Bridge の削除
sudo ovs-vsctl del-br ovsbr0
```

改めて OVS Bridge `ovsbr0` を作成する：

```bash
$ sudo ovs-vsctl add-br ovsbr0
```

OVS Bridge が作成されたことを確認する：
```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port ovsbr0
            Interface ovsbr0
                type: internal
    ovs_version: "3.3.1"
```

#### 2.1.1 cloudinit file の作成
次のような `cloud_init.cfg` を作成する：

```yaml:cloud_init.cfg
#cloud-config
users:
  - name: root
    ssh-authorized-keys:
      - <SSH_KEY>

runcmd:
  - nmcli con add type bond con-name bond0 ifname bond0 bond.options "mode=802.3ad,lacp_rate=fast,xmit_hash_policy=layer2+3" ipv4.method disabled ipv6.method disabled connection.autoconnect-slave 1
  - nmcli con add type ethernet con-name bond0p0 ifname eth0 master bond0 802-3-ethernet.speed 1000 802-3-ethernet.duplex full 802-3-ethernet.auto-negotiate no
  - nmcli con add type ethernet con-name bond0p1 ifname eth1 master bond0 802-3-ethernet.speed 1000 802-3-ethernet.duplex full 802-3-ethernet.auto-negotiate no
  - nmcli con add type vlan con-name bond0.10 dev bond0 id 10 ipv4.method manual ipv4.address ${vlan10_ipv4_address} connection.autoconnect yes
  - nmcli con add type vlan con-name bond0.20 dev bond0 id 20 ipv4.method manual ipv4.address ${vlan20_ipv4_address} connection.autoconnect yes
  - nmcli con up bond0
```

#### 2.1.2 XSLT patch の作成

次の XLST patch を作成する。なお、1章で使用したものと異なるのは `<xsl:value-of ...` で tap の prefix も自動で決定するようにした点であり、残りの部分は同じである。

```xml:tap.xsl.template
<?xml version="1.0" ?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output omit-xml-declaration="yes" indent="yes"/>

  <!-- Identity transform to copy everything as is -->
  <xsl:template match="node()|@*">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
    </xsl:copy>
  </xsl:template>

  <!-- Enable Open vSwitch support and assign a static name to each device -->
  <xsl:template match="/domain/devices/interface[@type='bridge']">
    <xsl:copy>
      <xsl:apply-templates select="node()|@*"/>
      <xsl:if test="source/@bridge='ovsbr0'">
        <virtualport type="openvswitch"/>
      </xsl:if>
      <target>
        <xsl:attribute name="dev">
          <xsl:value-of select="concat('${tap_prefix}p', count(preceding::interface[@type='bridge']) + 1)"/>
        </xsl:attribute>
      </target>
    </xsl:copy>
  </xsl:template>

</xsl:stylesheet>
```

#### 2.1.3 Terraform file の作成
次の `main.tf` を作成する：
```main.tf
terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "0.8.1"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_cloudinit_disk" "commoninit" {
  count     = 2
  name      = "commoninit_${count.index}.iso"
  user_data = templatefile("cloud_init.cfg", {
    vlan10_ipv4_address = "10.0.10.${count.index + 1}/24"
    vlan20_ipv4_address = "10.0.20.${count.index + 1}/24"
  })
  pool      = "default"
}

resource "libvirt_domain" "vm" {
  count  = 2
  name   = "vm${count.index}"
  vcpu   = 4
  memory = 4096

  network_interface {
    bridge = "ovsbr0"
  }

  network_interface {
    bridge = "ovsbr0"
  }

  disk {
    volume_id = libvirt_volume.volume[count.index].id
  }

  cloudinit = libvirt_cloudinit_disk.commoninit[count.index].id

  cpu {
    mode = "host-passthrough"
  }

  graphics {
    type        = "vnc"
    listen_type = "address"
  }

  # Makes the tty0 available via `virsh console`
  console {
    type        = "pty"
    target_port = "0"
    target_type = "serial"
  }
  
  xml {
    xslt = templatefile("tap.xsl.template", {
      tap_prefix = "tap${count.index + 1}"
    })
  }
}

resource "libvirt_volume" "volume" {
  count          = 2
  name           = "system_${count.index}.qcow2"
  pool           = "default"
  format         = "qcow2"
  base_volume_id = "/var/lib/libvirt/images/Rocky-9-GenericCloud-Base.latest.x86_64.qcow2"
  size           = 500 * 1024 * 1024 * 1024
}
```

次のコマンドで VM を作成する
```
$ terraform init --upgrade
$ terraform apply --auto-approve
```

### 2.2 Open vSwitch の設定

VM 1台のときと同様に、自動作成される OVS 側の port を削除して、新たに bond `bond-vm1` および `bond-vm2` を作成する：

```bash
# Delete the existing ports
sudo ovs-vsctl del-port ovsbr0 tap1p1
sudo ovs-vsctl del-port ovsbr0 tap1p2
sudo ovs-vsctl del-port ovsbr0 tap2p1
sudo ovs-vsctl del-port ovsbr0 tap2p2

# Creating new bonds 
# 1. Set each LACP port to active mode
# 2. Set each bond as balance-tcp mode
# 3. Accelerate LACP negotiation by setting it to fast mode
# 4. Use layer2+3 policy 
# 5. Configure the bond to allow VLAN trunking for VLANs
sudo ovs-vsctl add-bond ovsbr0 bond-vm1 tap1p1 tap1p2 \
        -- set port bond-vm1 \
        lacp=active \
        bond_mode=balance-tcp \
        other_config:lacp-time=fast \
        other_config:bond-hash-policy=layer2+3 \
        trunks=10,20
sudo ovs-vsctl add-bond ovsbr0 bond-vm2 tap2p1 tap2p2 \
        -- set port bond-vm2 \
        lacp=active \
        bond_mode=balance-tcp \
        other_config:lacp-time=fast \
        other_config:bond-hash-policy=layer2+3 \
        trunks=10,20
```

bond が作成されたことを確認する：
```
$ sudo ovs-vsctl show
913e15b8-dbd7-4cf9-b6d8-2437a84cc64d
    Bridge ovsbr0
        Port ovsbr0
            tag: 10
            Interface ovsbr0
                type: internal
        Port bond-vm2
            trunks: [10, 20]
            Interface tap2p1
            Interface tap2p2
        Port bond-vm1
            trunks: [10, 20]
            Interface tap1p1
            Interface tap1p2
    ovs_version: "3.3.1"
```

`lacp_status` が `negotiated` になっていることを確認する：
```
$ sudo ovs-appctl bond/show
---- bond-vm2 ----
bond_mode: balance-tcp
bond may use recirculation: yes, Recirc-ID : 23
bond-hash-basis: 0
lb_output action: disabled, bond-id: -1
updelay: 0 ms
downdelay: 0 ms
next rebalance: 5593 ms
lacp_status: negotiated
lacp_fallback_ab: false
active-backup primary: <none>
active member mac: fe:54:00:72:d3:42(tap2p2)

member tap2p1: enabled
  may_enable: true

member tap2p2: enabled
  active member
  may_enable: true

---- bond-vm1 ----
bond_mode: balance-tcp
bond may use recirculation: yes, Recirc-ID : 22
bond-hash-basis: 0
lb_output action: disabled, bond-id: -1
updelay: 0 ms
downdelay: 0 ms
next rebalance: 5593 ms
lacp_status: negotiated
lacp_fallback_ab: false
active-backup primary: <none>
active member mac: fe:54:00:d6:2b:70(tap1p1)

member tap1p1: enabled
  active member
  may_enable: true

member tap1p2: enabled
  may_enable: true
```

## 3. 疎通テスト

### 3.1 (Optional) VM ネットワークにホストを接続する
VM 上にツールを導入する際には、事前に何かしらホスト経由でネットワーク接続する必要がある（例えばホストからパッケージを直接送信したり、あるいは VM をインターネットに接続してツールをインストールする）。本節では、次の図のように、ホストを VM のネットワーク VLAN 10 に接続し、かつ NAT 化して VM からインターネットに接続できるようにする （必ずしも本記事のやり方に従う必要はない）。

![nat.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/e53dfeef-9c9a-19ce-9733-0dac5293d2fb.png)

#### 3.1.1 ホストを VLAN 10 に接続する
ホスト側で次の設定を行い、VLAN 10 にアクセスするための Interface を作成する：
- Port `ovsbr0` を VLAN 10 の access port にする
- Interface `ovsbr0` に IP `10.0.10.100` を設定する
- Subnet `10.0.10.0/24` への route を追加する

```bash
sudo ovs-vsctl set port ovsbr0 tag=10
sudo ip addr add 10.0.10.100/24 dev ovsbr0
sudo ip link set ovsbr0 up
sudo ip route add 10.0.10.0/24 dev ovsbr0
```

ホストから VM1 / VM2 に ping が到達することを確認する：
```
$ ping 10.0.10.1
$ ping 10.0.10.2
```

#### 3.1.2 ホストを NAT 化する
VM をインターネットに接続する場合は、ホスト上で次のコマンドを実行して NAT 化する：

```bash 
# IP forwarding の有効化
echo 1 > /proc/sys/net/ipv4/ip_forward
# 反映
sysctl -p
```

masquerade を有効化する：
```
firewall-cmd --permanent --add-masquerade
firewall-cmd --reload
```

#### 3.1.3 VM 側からのインターネット接続を確認する
VM1, VM2 側で VLAN 10 の interface に gateway と dns を追加する：

```
[root@vm1 ~]$ nmcli con mod bond0.10 ipv4.gateway 10.0.10.100 ipv4.dns "8.8.4.4 8.8.8.8"
```
インターネットに接続できることを確かめる：

```
[root@vm1 ~]$ ping google.com
```

### 3.2 VLAN の疎通テスト
各 VM にパケットキャプチャツール `tcpdump` をインストールする：
```
sudo dnf install -y tcpdump
```

Node 1 から Node 2 に VLAN 10 経由で ping する：

```
ping 10.0.10.2
```

この状態のまま、tcpdump を起動し、Node 2 側で VLAN 10 のパケットのみをキャプチャする （SSH login した状態だと、そのパケットも拾ってしまってログが見づらいので、`not port 22` も指定しておく）：
```
# tcpdump -i bond0 not port 22 and vlan 10
dropped privs to tcpdump 
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on bond0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
15:58:06.622076 IP 10.0.20.1 > localhost: ICMP echo request, id 8, seq 69, length 64
15:58:06.622132 IP localhost > 10.0.20.1: ICMP echo reply, id 8, seq 69, length 64
...
```

![image1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/5bf57c25-9fbe-1620-d98c-d782254634ca.png)

指定したパケットをキャプチャできていることが確認できる。

同様に、宛先 の IP とキャプチャする VLAN tag の組み合わせを変えて実験すると、次の表に示す結果が得られる：

| 宛先 IP | キャプチャする VLAN tag | キャプチャ可否 |
| -- | -- | -- |
| `10.0.10.1` | `10` | success |
| `10.0.10.2` | `10` | success |
| `10.0.20.1` | `10` | failure |
| `10.0.20.2` | `10` | failure |
| `10.0.10.1` | `20` | failure |
| `10.0.10.2` | `20` | failure |
| `10.0.20.1` | `20` | success |
| `10.0.20.2` | `20` | success |

### 3.3 LACP 動作テスト
各 VM に、次のツールをインストールする：
- `sysstat`: マシンリソースのモニタリング
- `iperf3`： ネットワーク用のパフォーマンス測定

```
sudo dnf install -y sysstat
sudo dnf install -y iperf3
```

iperf で VM1 から VM2 にパケットを送信する。

VM2 側で次を実行：
```
[root@vm2 ~]# iperf3 -s
```

VM1 側で次を実行
```
[root@vm1 ~]# iperf3 -c 10.0.10.1 -P 10
```

VM2 側で NIC の使用率を観測する：
```
[root@vm2 ~]# sar -n DEV 1 5
```

`rxkB/s` 列が受信パケットに関するログである。各 interface `eth0`, `eth1` にパケットが分散されていることを確認する。

![measurement.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261727/0cebc7eb-6124-3947-af5c-ac78d46767fa.png)


### 3.4 (Optional) 検証環境のクリーンアップ

VM を削除する：
```
terraform destroy
```

OVS Bridge を削除する：
```
sudo ovs-vsctl del-br ovsbr0
```

ホストの NAT 設定を削除：
```
$ echo 0 > /proc/sys/net/ipv4/ip_forward
$ firewall-cmd --permanent --remove-masquerade
```

## 参考資料

### Libvirt + Open vSwitch に関する資料

https://qiita.com/MahoTakara/items/c48afa29199e3ad8266e

https://qiita.com/Kaz_K/items/d9d50ac70c02a09c498f

https://qiita.com/kure/items/590ea69e9338a14c2bce

### Terraform Libvirt Provider + Open vSwitch に関する資料

https://qiita.com/masru0714/items/17aa6b2fe55188c89420


https://github.com/dmacvicar/terraform-provider-libvirt/issues/616