SONiC-VSのMACアドレス

# はじめに

ふだん仮想マシンを使うとき、MACアドレスについて気にすることはあまりないと思います。筆者もそうだったのですが、[SONiC](https://azure.github.io/SONiC/)の仮想マシンを使っていてMACアドレスを気にせざるを得ない事象に遭遇してしまいました。原因と対応方法まで調べましたので共有したいと思います。

なお、物理マシン向けSONiCには上記のような事象はありませんので念のため。

# きっかけ

SONiCはホワイトボックススイッチ用のOSで、仮想マシン版としてSONiC-VS (Virtual Switch)というものがあります。ホワイトボックススイッチはネットワーク機器なので単体で動かす意味はなく、仮想マシンであってもそれは同様です。

そんなわけで、最小構成としてSONiC-VSが動作する仮想マシンを2台用意して、仮想マシンの(2つめの)NIC同士を接続します。

トポロジーは下記のようになります。`s0`と`s1`がVMです。`default`はホストマシンとつながるネットワークになります(DHCPでアドレスが降ってきます)。

```
(default)                      (default)
eth0|                              |eth0
 ---+---                        ---+---
 | s0  |                        | s1  |
 ---+---                        ---+---
    |10.0.0.0/31        10.0.0.1/31|
    |Ethernet0            Ethernet0|
    ---------------c1---------------
```

`Ethernet0`には手でIPアドレスを割り当てます。

```shell-session:s0
$ sudo config interface ip add Ethernet0 10.0.0.0/31
```

```shell-session:s1
$ sudo config interface ip add Ethernet0 10.0.0.1/31
```

インタフェースもupしてるし、`ping`くらい大丈夫だろう、と送ってみると……応答が返ってきません。`tcpdump`で観測すると、`arp`を受けた相手が`reply`を送ってるのが見えました。それなのに`ping`がとおらない? そんなことあるんだ……

# 状況確認
## なぜ`ping`が通らないのか

あちこち調べたのですが、結論としては`s1`の`Ethernet0`のMACアドレスと、`s2`の`Ethernet0`のMACアドレスが同一だったため通信できませんでした。具体的には、どちらも`52:54:00:12:34:56`でした。

## なぜMACアドレスが同一なのか

まず、QEMU/KVMで起動したVMでは、なにも指定がなければNICにつけられるMACアドレスが**`52:54:00:12:34:56`**固定になります。このこと自体は有名なようで、このMACアドレスで検索するとこの動作について言及しているWebページが山のように出てきます。

SONiCの設定ファイルである`/etc/sonic/config_db.json`には`"mac"`という設定項目があり、起動したSONiC-VSの中を覗き込むと、2つのVMでどちらも`52:54:00:12:34:56`となっているのがわかります。`ip link`で確認できる、フロントパネルポートに割り当たっているMACアドレスもこの値です。

```json:config_db.json
{
    "DEVICE_METADATA": {
        "localhost": {
            "hwsku": "Force10-S6000",
            "platform": "x86_64-kvm_x86_64-r0",
            "mac": "52:54:00:12:34:56",
            "hostname": "sonic",
            "type": "LeafRouter",
            "bgp_asn": "65100"
        }
```

`/etc/sonic/config_db.json`の`"mac"`の値を書き換えて再起動すると、`ip link`で見えるMACアドレスが変更された状態となりました。`ping`も通りましたので、この設定項目が直接原因で間違いないでしょう。

`config_db.json`はSONiCの初回起動時に生成されるので、起動時にどこかに付与されているMACアドレスを拾ってるんだろうなあ、という想像はつきます。なんとなく`eth0`なんだろうなあと思いつつ、追跡して間違いないか確認し、VM起動時に別の値を与えることで症状が改善するか確認してみます。

# 調査: 初期起動時の`config_db.json`生成の流れ

`"mac"`の値を取得するまでの流れは下記でした。

1. `/etc/rc.local`起動
2. `firsttime`ファイルがあれば`touch /tmp/pending_config_initialization`
3. `config-setup.service`開始
4. `ExecStart=`で指定されている`config-setup boot`が起動
5. `boot_config()`で`/tmp/pending_config_initialization`の有無がチェックされる
6. 結果、`do_config_initialization()`が呼ばれる(戻ってきたら`/tmp/...`や`firsttime`ファイルを削除)
7. `generate_config factory`が呼ばれる
8. `sonic-cfggen`コマンドが呼ばれる
9. `device_info.get_system_mac()`でMACアドレスを得ていた
10. `cat /sys/class/net/eth0/address`がMACアドレス!

つまり**`eth0`のMACアドレスが`52:54:00:12:34:56`であることが原因**で間違いないようです。

# 対策: MACアドレスを指定してみた

MACアドレスはVM起動時に`-macaddr=`で指定できますので、指定してみることにします。もちろん初回起動時じゃないと`config_db.json`は生成されませんので、新しいイメージを用意して。(terraform-provider-libvirtを使うときは`network_interface`ブロックに`mac=`を追加すれば指定できます)

起動したので`config_db.json`を見てみます。

```json:config_db.json
{
    "DEVICE_METADATA": {
        "localhost": {
            "hwsku": "Force10-S6000",
            "platform": "x86_64-kvm_x86_64-r0",
            "mac": "52:54:00:12:34:56",
            "hostname": "sonic",
            "type": "LeafRouter",
            "bgp_asn": "65100"
        }
```

……あれ?

指定を間違えたのかと思って`cat /sys/class/net/eth0/address`してみます。

```
admin@sonic:~$ cat /sys/class/net/eth0/address 
52:54:00:12:00:00
```

指定した値になっているのに、`config_db.json`の`"mac"`は`52:54:00:12:34:56`のままです。これはいったいどういうこと?

# 調査: なぜ指定が無視されるのか

当初は、外部で指定していてもいったん`52:54:00:12:34:56`で`eth0`が生えて、その後どこかのタイミングで値が変わるのか? と思い、`sonic-cfggen`に手を入れて`/sys/class/net/eth0/address`の中身を呼ばれるたび吐き出すようにしてみたのですが、ここにはまったく`52:64:00:12:34:56`の痕跡なし。`eth0`は最初から指定した値になっていました。

じゃあもしかして自分の知らないルートで`config_db.json`が生成されている? と思い、あちこち調べてみると、結果として下記のことがわかりました。

- `/etc/rc.local`が呼ばれたときにはすでに`config_db.json`は存在している
- ビルドした`sonic-vs.img`の中にはすでに**`firsttime`ファイルがない**

`firsttime`ファイルがない? へ? そんなことってあるの?

`firsttime`ファイルはSONiC起動時に参照され、初期化が必要かの目印として使われます。初期化が終わると削除されるため、ふつうに使っていればその存在を意識することはありません。が、`config_db.json`の生成は初期化処理です。これは追跡する必要がありそうです。

## `firsttime`ファイルはいつ消える?

ベースとなるファイルシステムの中身は`build_debian.sh`によって作られます。中を見ると`firsttime`ファイルは無条件に作成されているように見えます。

```shell:build_debian.sh
mkdir -p $FILESYSTEM_ROOT
mkdir -p $FILESYSTEM_ROOT/$PLATFORM_DIR
mkdir -p $FILESYSTEM_ROOT/$PLATFORM_DIR/x86_64-grub
touch $FILESYSTEM_ROOT/$PLATFORM_DIR/firsttime
```

しかし、下記手順でqcow2イメージを`mount`し`sonic-vs.img`の中身を起動前に調べてみると、たしかにありませんでした。

```shell-session
$ sudo modprobe nbd max_part=63
$ sudo qemu-nbd -c /dev/nbd0 sonic-vs.img
$ sudo mount /dev/nbd0p3 /mnt
$ ls /mnt/image-*/platform
x86_64-grub
$ sudo umount /mnt
$ sudo qemu-nbd -d /dev/nbd0
```

だとすると、どこかで誰かが消している? しかしソースコードのどこを見てもビルド時に`firsttime`ファイルを削除するという記述はありません。

ん? まさか、ビルド時に一度起動している? ということは、まさか……ビルドした`sonic-vs.img`の中にはすでに**`config_db.json`がある? ……あった!!** orz

# 調査: どこで起動している?

ここまで簡単にまとめます。

- SONiC-VSの仮想マシンイメージは、どうやらビルド中に一度起動している模様
- そのためビルド済みの仮想マシンイメージにはすでに`config_db.json`が存在する
- 初期化処理を示す`firsttime`ファイルもビルド中の起動により削除されている

もしビルド中に起動しているのであれば、そのときのコマンドラインに`-macaddr`指定などありませんので、当然`eth0`のMACアドレスは`52:54:00:12:34:56`になります。これっぽいなー。ということで、どこで起動しているのか、そもそもなぜ起動しているのか、ビルド中の起動は本当に必要なのかを調べます。

## 仮想マシンイメージ作成フロー

ビルドしたばかりのイメージですでに起動済みということは、仮想マシンイメージを作成する手順のどこかで起動しているはず。調べてみると、`scripts/build_kvm_imagee.sh`の中で起動しているっぽいのがわかりました。

```shell:build_kvm_image.sh
/usr/bin/kvm -m $MEM \
    -name "onie" \
    -boot "order=cd,once=d" -cdrom "$ONIE_RECOVERY_ISO" \
    -device e1000,netdev=onienet \
    -netdev user,id=onienet,hostfwd=:0.0.0.0:3041-:22 \
    -vnc 0.0.0.0:0 \
    -vga std \
    -drive file=$DISK,media=disk,if=virtio,index=0 \
    -drive file=$INSTALLER_DISK,if=virtio,index=1 \
    -serial telnet:127.0.0.1:$KVM_PORT,server > $kvm_log 2>&1 &
```

これはなんのためにやっているのだろう? 必要なの? と思ったのですが、これ自体は必要でした。具体的には下記のようなフローでSONiC-VSの仮想マシンイメージを作成していました。よって、ここを削ると正しくイメージを作れないということになります。

1. `qemu-img create`で空のディスクイメージファイルを作る
2. FATのディスクイメージにSONiCのインストーラーを書き込む
3. 2つのディスクイメージとONIEリカバリーISOをつけた状態でKVM起動
4. **`check_install.py`によりコンソール制御開始**
5. `GRUB`メニューが出たらEnterを送ってONIE(リカバリモード)起動
6. リカバリモードの自動処理によってSONiCをインストール

フローがここまでなら問題ないのですが、そのあと`check_install.py`は下記も実行していました。

- 自動リブートのあとSONiCが起動するまで待つ
- SONiCにログインする
- インストールできているかコマンド実行で確認(プロンプトが返ってこないとビルドエラー扱い)

**ここか!!**

# 本命の対策

考えられる対策は2つあります。

- ビルド時にSONiCの起動をやめる。ONIEによるインストールだけにする
- `check_isntall.py`の中で、`firsttime`と`config_db.json`を起動前の状態に戻す

前者はブートメディアの指定(`order=cd`を`order=d`に変更)でおそらくなんとかなります。

`rc.local`内の処理をざっと見た限り、初期起動シーケンスがもう一度動いても問題ないように見えましたので、今回は後者でやってみます。

## ビルド時に生成した`config_db.json`の無効化

`config_db.json`を削除して、`firsttime`ファイルを用意します。

```diff
diff --git a/check_install.py b/check_install.py
index 08759db12..86950e73b 100755
--- a/check_install.py
+++ b/check_install.py
@@ -62,6 +62,10 @@ def main():
     p.expect([cmd_prompt])
     p.sendline('show ip bgp sum')
     p.expect([cmd_prompt])
+    p.sendline('sudo rm /etc/sonic/config_db.json')
+    p.expect([cmd_prompt])
+    p.sendline('cd /host/image-*/platform; sudo touch firsttime; cd -')
+    p.expect([cmd_prompt])
     p.sendline('sync')
     p.expect([cmd_prompt])
 
```

これで大丈夫なはずです。

## 対策の確認

SONiC-VSをビルドして(わりと時間がかかります)、できあがった`sonic-vs.img`を使ってVMを起動してみます。もちろんMACアドレス指定を忘れずに。

そして`config_db.json`を確認。

```json:config_db.json
{
    "DEVICE_METADATA": {
        "localhost": {
            "hwsku": "Force10-S6000",
            "platform": "x86_64-kvm_x86_64-r0",
            "mac": "52:54:00:12:00:00",
            "hostname": "sonic",
            "type": "LeafRouter",
            "bgp_asn": "65100"
```

`ping`も無事通りました。めでたしめでたし。


# おわりに

わかってみればなんてことはないのですが、調べるのに半日以上かかってしまいました。`qcow2`の中身を`mount`できなかったら、もっとかかっていたかもしれません。