Proxmox VEとcloud-initを使って初期設定済みのvJunos-switchを起動する

## サマリー

前回の記事 [Proxmox VEでvJunos-switchを動かしてみる #proxmox - Qiita](https://qiita.com/ymbk990/items/07ab914240a91e3949c4) では、Proxmox VEでvJunos-switchが起動できることを確認しました。  
しかし、これでは新しい仮想マシンを立ち上げるたびに手動で初期設定を入れることになります。

vJunos-switchにはcloud-initのような都合のいいものは無いので、別の方法を見つけなければなりません。  
今回はProxmox VEのcloud-init機能を参照したフックスクリプトで初期コンフィグドライブを生成することで、sshでログイン可能な状態でvJunos-switchを起動するように仮想マシンテンプレートをカスタマイズしてみます。

## 仕組み

### vJunos-Switch側

vJunos-Switchのドキュメント [KVM での vJunos スイッチの導入と管理 |ジュニパーネットワークス](https://www.juniper.net/documentation/jp/ja/software/vjunos/vjunos-switch-kvm/topics/deploy-and-manage-vjunos-switch-onkvm.html) によると、以下のように書かれています。

> vJunos スイッチは、構成が含まれている VM インスタンスに 2 台目のディスクを接続することで、初期構成を受け入れます。提供されたスクリプト make-config.sh を使用してディスク イメージを作成します。

つまり、2台目のディスクに初期コンフィグを入れさえすれば、初回起動時からsshでログインできる構成を作れそうです。  
しかし make-config.sh とは一体何なのでしょうか？

vJunos-Switchのドキュメントには直接的な解決方法が書かれていませんでしたが、仮想化されてある程度経過している同社のvSRXのドキュメントを見るとKVMやOpenStackにおけるブートストラップISOイメージについての記載が見つかりました。

[KVM を使用した vSRX 仮想ファイアウォールでの初期構成の読み込み |vSRX |ジュニパーネットワークス](https://www.juniper.net/documentation/jp/ja/software/vsrx/vsrx-consolidated-deployment-guide/vsrx-kvm/topics/task/security-vsrx-kvm-bootstrap-config.html)

Junos OSのことですから、おそらく同じ仕組みで動作すると予測されます。  
今回は上記の通り mkisofs コマンドを使用して ISO イメージを作成する方法でトライしてみます。

### Proxmox VE側

Proxmox VEには、仮想マシン起動前後でスクリプトを起動する [Hook Scripts](https://pve.proxmox.com/pve-docs/pve-admin-guide.html#_hook_scripts) 機能があります。  
hookscriptにはbashやperlなどのスクリプトが指定できます。おそらく、コマンド的に実行可能であれば何で書かれていても良いはずです。

これを使用して、Proxmox VEがデフォルトで持っているcloud-init設定（ユーザー名や公開鍵設定※）を読み込んでvJunos-switchに対するブートストラップISOイメージを生成することで、sshでログイン可能な状態でvJunos-switchを起動できるのではないでしょうか。

:::note
※たとえcloud-init driveを設定しなくとも、cloud-init用のユーザー名（ciuser）などはCLI経由で設定できます。  
WebUIでは `No CloudInit Drive found` となってしまいますが...
:::

## 設定

それでは、仕組みを実現するためのファイルを準備していきます。

### 1. hookscript

hookscriptの動きは以下の通りです。

1. Proxmox VEの設定から以下のパラメータを抽出

   | Param | Description |
   | ---   | --- |
   | HOSTNAME | 仮想マシンの名前(name)をそのまま使用 |
   | USERNAME | cloud-initで指定されたユーザー名(ciuser)をログインユーザー名として使用 |
   | PUBLIC_KEY | cloud-initで指定された公開鍵(sshkeys)を使用。URLエンコードされているので変換を挟む |
   | FXP_ADDRESS | ipconfig0のアドレスを管理ポート(fxp0)に割り当てるアドレスとして採用
   | FXP_GATEWAY | ipconfig0のゲートウェイアを管理ポート(fxp0)が所属するMgmt-VRFのゲートウェイとして採用

2. 取得した変数でテンプレートを置換して、ブートストラップISOファイルを生成する
3. 起動しようとしている仮想マシンにブートストラップISOをアタッチする

   :::note
   Proxmox VEではhookscriptが起動する時点で仮想マシン設定は固定されており変更できません。  
   そのため、一時的にロックファイルを削除して仮想マシン設定を変更した後、ロックファイルを再設置する必要があります。  
   また、ブートストラップISOがアタッチされた後はhookscriptも不要になりますので、設定から取り除いておきます。
   :::

4. 仮想マシンを再起動します

   :::note warn
   前述の 3. と同様に、Proxmox VEではhookscriptが起動する時点で仮想マシン設定は固定されています。  
   そのため、**hookscriptでブートストラップISOをアタッチしただけでは起動時のqemuコマンドに2台目のディスクが存在しません。**  
   そこで、hookscript設定を外して再度電源を入れ直すコマンドを3秒後に実行するように `systemd-run` でセットしています。  
   今のところ問題は見つかっていませんが環境に合わせたチューニングが必要になる部分だと思われます。
   :::

<details><summary>📒 vjunos-hook.sh の全文はこちら</summary>

```bash
#!/bin/bash

set -eu

vmid=$1
phase=$2
storage="ceph_datastore"

if [[ "$phase" == "pre-start" ]]; then
    echo "=== Generate vJunosSwitch bootstrap ISO ==="
    export HOSTNAME=$(qm config $vmid | grep -o "name: .*" | cut -d " " -f2-)
    export USERNAME=$(qm config $vmid | grep -o "ciuser: .*" | cut -d " " -f2-)
    PUBLIC_KEY_ENC=$(qm config $vmid | grep -o "sshkeys: .*" | cut -d " " -f2-)
    export PUBLIC_KEY=$(printf '%b' "${PUBLIC_KEY_ENC//%/\\x}")
    export FXP_ADDRESS=$(qm config $vmid | grep 'ipconfig0:' | grep -oE 'ip=([0-9]+\.){3}[0-9]+/[0-9]+' | sed 's/ip=//')
    export FXP_GATEWAY=$(qm config $vmid | grep 'ipconfig0:' | grep -oE 'gw=([0-9]+\.){3}[0-9]+' | sed 's/gw=//')

    echo "=== Parameters ==="
    echo "HOSTNAME: ${HOSTNAME}"
    echo "USERNAME: ${USERNAME}"
    echo "PUBLIC_KEY: ${PUBLIC_KEY}"
    echo "FXP_ADDRESS: ${FXP_ADDRESS}"
    echo "FXP_GATEWAY: ${FXP_GATEWAY}"
    if [[ -z "$HOSTNAME" || -z "$USERNAME" || -z "$PUBLIC_KEY" || -z "$FXP_ADDRESS" || -z "$FXP_GATEWAY" ]]; then
        echo "One or more parameters are empty. Exiting..."
        exit 1
    fi
    echo

    echo "=== Configuration ==="
    tmppath=$(mktemp -d)
    cat /mnt/pve/cephfs/snippets/vjunos_template.conf | envsubst | tee $tmppath/juniper.conf
    tar -zcvf $tmppath/juniper.conf.tgz $tmppath/juniper.conf
    rm $tmppath/juniper.conf
    mkisofs -l -o vjunos_${vmid}.iso $tmppath

    echo "=== Attach disk force (remove lock file tempolary) ==="
    # NOTE: https://git.geco-it.net/GECO-IT-PUBLIC/fedora-coreos-proxmox/src/branch/master/hook-fcos.sh
    # Can not set config locked.
    rm -f /var/lock/qemu-server/lock-${vmid}.conf
    qm importdisk ${vmid} vjunos_${vmid}.iso ${storage}
    qm set ${vmid} --virtio1 ${storage}:vm-${vmid}-disk-1,iothread=1
    qm set ${vmid} --delete hookscript
    touch /var/lock/qemu-server/lock-${vmid}.conf

    rm vjunos_${vmid}.iso
    rm -r $tmppath
    # hack for boot with new attached device
    systemd-run --on-active=3 bash -c "qm stop ${vmid} && qm wait ${vmid} && qm start ${vmid}"
    echo "done!"
fi
```

</details>

### 2. bootstrap config template

テンプレートについては、前回記事 [Proxmox VEでvJunos-switchを動かしてみる #proxmox - Qiita](https://qiita.com/ymbk990/items/07ab914240a91e3949c4) で起動した後の設定を抜き出してきて、一部を変数で置き換えたものです。

また、ついでにfxp0インターフェースを管理用VRFに所属させるようにしています。  
これについては説明範囲外なので [デフォルト以外のインスタンスの管理インターフェイス |Junos OS |ジュニパーネットワークス](https://www.juniper.net/documentation/jp/ja/software/junos/junos-getting-started/topics/topic-map/management-interface-in-non-default-instance.html) を参照ください。

root-authentication -> encrypted-password については固定で準備しました。  
現在のバージョン（23.1R1.8）にあわせるなら、sha-512にソルト付きで生成すると丁度いいでしょう。

```
$ mkpasswd -m sha-512 qiita-passowrd 99999999
$6$99999999$X4Cdz5u1wKdxEh0sE3SX5q4irCIxvAebu02dYIO6Q3FeUPaLJZA2kCB/w9lV0bU6tL7wiD14TO8glvu/HztAK0
```

左記のhookscriptで自動生成しても良いかもしれませんね。

<details><summary>📒 vjunos_template.conf の全文はこちら</summary>

```
system {
    host-name ${HOSTNAME};
    root-authentication {
        encrypted-password "$6$99999999$/Eea5D4QA7wqx.NPg0WD/VzHc6y0Htokd9DcEZUWAdOffUwleA4WzdDyBe0D68whleSgU7tL13KE1Qoa72O6x."; ## SECRET-DATA
    }
    login {
        user ${USERNAME} {
            uid 2000;
            class super-user;
            authentication {
                ssh-rsa "${PUBLIC_KEY}"; ## SECRET-DATA
            }
        }
    }
    services {
        ssh;
        netconf {
            ssh {
                port 830;
            }
        }
    }
    arp {
        aging-timer 5;
    }
    management-instance;
    syslog {
        file interactive-commands {
            interactive-commands any;
        }
        file messages {
            any notice;
            authorization info;
        }
    }
}
interfaces {
    fxp0 {
        unit 0 {
            family inet {
                address ${FXP_ADDRESS};
            }
        }
    }
}
multi-chassis {
    mc-lag {
        consistency-check;
    }
}
routing-instances {
    mgmt_junos {
        routing-options {
            static {
                route 10.0.0.0/8 next-hop ${FXP_GATEWAY};
                route 172.16.0.0/12 next-hop ${FXP_GATEWAY};
                route 192.168.0.0/16 next-hop ${FXP_GATEWAY};
            }
        }
        description ManagementInstance;
    }
}
protocols {
    lldp {
        interface all;
    }
    lldp-med {
        interface all;
    }
}
```

</details>

### 3. テンプレートへの適用

それでは、hookscriptに実行権限を付与してvJunos-Switchの仮想マシンテンプレートに設定します。  

```
chmod +x /mnt/pve/cephfs/snippets/vjunos_hook.sh 
qm set 9051 -hookscript=cephfs:snippets/vjunos_hook.sh
```

:::note info
前回作成したvJunos-Switchと同様の設定の仮想マシンテンプレート(9051)が作成されているものとします（手順外）
:::

## 起動確認

それでは、作成したテンプレートを元に仮想マシンを作成して起動してみましょう。

```
# qm clone 9051 1101 --name vjunos-sample --full --storage local-lvm
# qm set 1101 --ipconfig0 ip=192.168.0.11/24,gw=192.168.0.1
# qm set 1101 --ciuser qiita --sshkeys sample01-rsakey.pub
# qm start 1101
```

起動するとhookscriptの出力が出てきます。

```
=== Generate vJunosSwitch bootstrap ISO ===
=== Parameters ===
HOSTNAME: vjunos-sample
USERNAME: qiita
PUBLIC_KEY: ssh-rsa ...
```

hookscriptでセットされたコマンドに従って、仮想マシンは停止した後に再度起動します。

今度は `qm terminal` コマンドで起動時のログを確認します。  
パッケージのマウントが一通り完了した後に `/dev/vtbd1` をマウントして `/mnt/juniper_conf.tgz` を見つけてくれたら、先ほど用意したブートストラップコンフィグが読み込まれているはずです。

```
# qm terminal 1101
...
Verified dsa-x86-64-20230319 signed by PackageProductionECP256_2023 method ECDSA256+SHA256
Mounting dsa-x86-64-20230319.041703_builder_junos_231_r1
@ 1705123641 [2024-01-13 05:27:21 UTC] mount done
Removing /etc/malloc.conf
Sat Jan 13 05:27:22 UTC 2024 vmguest: setup ...
Sat Jan 13 05:27:22 UTC 2024 vmguest: mounted /dev/vtbd1
Sat Jan 13 05:27:22 UTC 2024 vmguest: found /mnt/juniper_conf.tgz
vm_retype=""RE-VMX""
...
```

最後に、sshでログインできることを確認して完了です。

```
> ssh qiita@192.168.0.11
Last login: Sat Jan 13 05:51:48 2024 from 192.168.0.101
--- JUNOS 23.1R1.8 Kernel 64-bit  JNPR-12.1-20230307.3e7c4b6_buil
qiita@vjunos-sample>
```

SSHログイン可能な設定が入った状態でvJunos-Switchが起動できました。 :tada: 

## 終わり

これで沢山のvJunos-Switchを準備することになっても、初期設定の面倒さから逃れることが出来ました。  
Proxmox VEのhookscriptは自由度があるように見えるものの、思いのほか使いどころが限られますね。

複数台の仮想NW装置を用意できると、設定用のコントローラー開発や管理系ソフトウェアの検証などに大いに役立ちます。  
最近はBGPの拡張も目覚ましいところですので、相互接続検証や調査に役立てていきましょう。