SoftEther VPNの設定をAnsibleで更新する

# はじめに
このところリモートワーク推進の流れでVPNの設定をいじることが増えてきました。
いちいち手動で変更するのは管理が大変なので、今回はSoftEther VPN/PacketiX VPNの設定をAnsibleで更新するようにしてみました。
検証した環境は商用ライセンスのPacketiX VPNですが、無償版のSoftEther VPNも同じと思われます。以下ではまとめてSoftEther VPNと呼びます。

# 環境

* PacketiX VPN Server 4.0 (Version 4.29 Build 9680)
* Ansible 2.7.1

# 作成するロール

Ansibleのロールとして`vpn_config`を作成することにします。すなわち以下のようなディレクトリ構成を想定しています。

```console
$ tree roles
roles
`-- vpn_config
    |-- files
    `-- tasks
```


# 設定ファイルの準備
SoftEther VPNの設定ファイルはサーバのバイナリと同じディレクトリに`vpn_server.config`というファイル名で存在します。
設定ファイルを一から準備するのは大変なので、このファイルをベースにAnsibleで同期したい設定ファイルを作っていきます。

設定ファイルのフォーマットは以下のようになっています。

```
declare root
{
   uint ConfigRevision 112
   bool IPsecMessageDisplayed true
   string Region JP
   bool VgsMessageDisplayed false

...

}
```

この中から設定したくない項目を削っていきます。削るべき項目としては

* `ConfigRevision`: これは設定更新毎にサーバ側でインクリメントされる値なので、必ずしも同期の必要はないです。
* `declare DDnsClient{}`の`key`と`LocalHostname`: ホスト毎に違う値が生成されるので削っておきます。
* `declare LicenseManager{}`: ライセンス情報です。これもJinja2で埋め込んでもいいですが、今回は除外して手動で設定することにしました
* 各種統計情報: サーバ側で勝手にカウントアップしていくような値は除外します。具体的には`BroadcastBytes`, `BroadcastCount`, `UnicastBytes`, `UnicastCount`, `LastCommTime`, `LastLoginTime`, `NumLogin`です。これらはそもそも設定不能なので、設定ファイルに残っていても悪さはしませんが、Ansible側の更新判定がやりにくいので削ります。

が考えられます。不要な項目については行ごと削除します。

できた設定ファイルを`roles/vpn_config/files/vpn_server.config.master`として保存しておきます。

# 設定チェックスクリプト

次に現在のサーバ設定と先ほど用意した設定が一致しているかどうかを判定するスクリプトを作ります。

```roles/vpn_config/files/check_config.sh
#!/bin/sh
cd `dirname $0`

./vpncmd localhost:8888 /server /PASSWORD:xxx /CMD ConfigGet vpn_server.config.tmp

diff vpn_server.config.tmp vpn_server.config.master | grep "^>"
ret=$?

rm vpn_server.config.tmp

if [ $ret = 0 ]; then
    exit 1
else
    exit 0
fi
```

`vpncmd`はコマンドラインからサーバを操作するツールで、サーバのバイナリと同じところにあると思います。
このコマンドで現在のサーバ設定を取得して`vpn_server.config.tmp`に保存しています。
その後、`diff`コマンドでmasterのファイルと差分があるかを確認しています。

ここではサーバのパスワード`xxx`を平文で書いていますが、必要に応じてAnsible Vaultでの暗号化などをしてください。

# 設定スクリプト

実際にサーバ設定を反映するスクリプトです。

```roles/vpn_config/files/set_config.sh
#!/bin/sh
cd `dirname $0`
./vpncmd localhost:8888 /server /PASSWORD:xxx /CMD ConfigSet vpn_server.config.master
```

先ほどの`ConfigGet`もそうですが、`ConfigSet`は引数に`/`を含むパスを取れないようです。
なのであらかじめ`vpn_server.config.master`が存在するディレクトリに`cd`するようにしています。

# Ansibleのタスク

最後にAnsibleのタスクです。

```roles/vpn_config/tasks/main.yml
- name: send config
  copy:
      src  : "{{ item.src  }}"
      dest : "{{ item.dest }}"
      owner: root
      group: root
      mode : "{{ item.mode }}"
  with_items:
      - { src: 'vpn_server.config.master', dest: '/usr/local/vpnserver/vpn_server.config.master', mode: '0600' }
      - { src: 'check_config.sh'         , dest: '/usr/local/vpnserver/check_config.sh'         , mode: '0700' }
      - { src: 'set_config.sh'           , dest: '/usr/local/vpnserver/set_config.sh'           , mode: '0700' }

- name: check config
  command: /usr/local/vpnserver/check_config.sh
  register: check_config
  check_mode: no
  failed_when: no
  changed_when: 'check_config.rc != 0'

- name: debug config
  debug:
      var: check_config
  when: check_config is changed

- name: set config
  command: /usr/local/vpnserver/set_config.sh
  when: check_config is changed
```

`send config`で設定ファイルとスクリプトの転送、`check config`で設定変更が必要かどうかのチェック、`set config`で反映です。また、設定変更があった場合は`debug config`が変更内容をダンプします。

# まとめ

最終的なロールの構成は以下のようになりました。

```console
$ tree roles
roles
`-- vpn_config
    |-- files
    |   |-- check_config.sh
    |   |-- set_config.sh
    |   `-- vpn_server.config.master
    `-- tasks
        `-- main.yml
```