Ansible で Bergenholm をぶっ叩く

# はじめに

[Bergenholm](https://github.com/yosshy/bergenholm) というネットワークインストールサーバをご存知だろうか？
数年前、私が [iPXE](http://ipxe.org/) の豊富な機能を最大限活かしたネットワークインストールサーバを作りたくて、Python の Web マイクロフレームワーク [Flask](http://flask.pocoo.org/) をベースに作ったものである。
元々、Ansible のような構成管理ツールと連携して動作する事を想定して作ったものなので、REST API があるだけ。同 API を叩く[専用のクライアントツール](https://github.com/yosshy/bergenholmclient/) はあるが、WebUI 的な物は一切ない。

その割には、これまで Bergenholm のホスト／グループを操作する為の Ansible モジュールが無かったので、今回作ってみた。[こちら](https://github.com/yosshy/ansible-bergenholm)からダウンロードできる。

# 試してみる

手始めに、**Bergenholm サーバ上で**上記のリポジトリをダウンロードする。

```
~$ git clone https://github.com/yosshy/ansible-bergenholm.git
~$ cd ansible-bergenholm
```

中には bergenholm_host, bergenholm_group モジュールに加えて、サンプルの Ansible Playbook (create.yml, delete.yml) やインベントリ (hosts.ini) が含まれている。

```
~/ansible-bergenholm$ ls -RF
.:
ansible.cfg  create.yml  delete.yml  group_vars/  hosts.ini  host_vars/  library/  LICENSE  README.md

./group_vars:
sample

./host_vars:
ansibletest1  ansibletest2  ansibletest3

./library:
bergenholm_group.py  bergenholm_host.py
```

hosts.ini はこの通り。

```ini:hosts.ini
[sample:children]
targets

[targets]
ansibletest1
ansibletest2
ansibletest3
```

git リポジトリには含まれていない開発用のインベントリとグループで区別する為に sample グループを定義しているが、その必要が無ければ targets グループだけで良い。

# CentOS 7 VM を自動構築する

次に、create.yml を見てみよう。

```yaml+jinja:create.yml
- name: CentOS 7 VM を用意する
  hosts: targets
  gather_facts: false
  tasks:
    - name: ESXi 上に VM を作成する
      local_action:
        module: vsphere_guest
        vcenter_hostname: "{{ esxi_host }}"
        username: "{{ esxi_user }}"
        password: "{{ esxi_pass }}"
        guest: "{{ vm_name }}"
        state: powered_on
        validate_certs: false
        vm_extra_config:
          vcpu.hotadd: yes
          mem.hotadd:  yes
        vm_disk:
          disk1:
            size_gb: 20
            type: thin
            datastore: "{{ vm_datastore }}"
            folder: disk1.vmdk
        vm_nic:
          nic1:
            type: vmxnet3
            network: bergenholm
            network_type: standard
        vm_hardware:
          memory_mb: 2048
          num_cpus: 2
          osid: centos64Guest
          scsi: paravirtual
        esxi:
          datacenter: ha-datacenter
          hostname: eval
    - name: ESXi 上の VM の情報を取得する
      local_action:
        module: vsphere_guest
        vcenter_hostname: "{{ esxi_host }}"
        username: "{{ esxi_user }}"
        password: "{{ esxi_pass }}"
        guest: "{{ vm_name }}"
        vmware_guest_facts: true
        validate_certs: false
        esxi:
          datacenter: ha-datacenter
          hostname: eval
      register: newvm
    - name: Bergenholm 上に新しい VM の設定を登録する
      local_action:
        module: bergenholm_host
        uuid: "{{ newvm.ansible_facts.hw_product_uuid }}"
        params:
          groups:
            - centos7
            - centos.amd64
          hostname: "{{ vm_name }}"
          ipaddr: "{{ vm_ipaddr }}"
          netif: ens192
        state: present
      notify:
        - VM を再起動して OS インストールを開始する
  handlers:
    - name: VM を再起動して OS インストールを開始する
      local_action:
        module: vsphere_guest
        vcenter_hostname: "{{ esxi_host }}"
        username: "{{ esxi_user }}"
        password: "{{ esxi_pass }}"
        guest: "{{ vm_name }}"
        state: restarted
        validate_certs: false

- name: OS インストール後の確認を行う
  hosts: targets
  gather_facts: false
  tasks:
    - name: OS インストール完了待ち
      wait_for_connection:
        sleep: 10
        timeout: 3600
    - name: Facts を取得する
      setup:
    - name: ディストリビューション情報を表示する
      debug:
        msg: "{{ ansible_distribution }} {{ ansible_distribution_version }} ({{ansible_distribution_release}})" 
```

上記は少々長くてアレだが、name: だけを抽出するとこうなる。

```
- name: CentOS 7 VM を用意する
    - name: ESXi 上に VM を作成する
    - name: ESXi 上の VM の情報を取得する
    - name: Bergenholm 上に新しい VM の設定を登録する
    - name: VM を再起動して OS インストールを開始する
- name: OS インストール後の確認を行う
    - name: OS インストール完了待ち
    - name: Facts を取得する
    - name: ディストリビューション情報を表示する
```

何とも簡単。説明はほとんど不要だろう。vsphere_guest モジュールについては[こちらの記事](https://qiita.com/hirofumihida/items/e793b8119154e2970001)を参照されたし。
肝心の bergenholm_host モジュールの部分だけ見てみよう。

```yaml+jinja
    - name: Bergenholm 上に新しい VM の設定を登録する
      local_action:
        module: bergenholm_host
        uuid: "{{ newvm.ansible_facts.hw_product_uuid }}"
        params:
          groups:
            - centos7
            - centos.amd64
          hostname: "{{ vm_name }}"
          ipaddr: "{{ vm_ipaddr }}"
          netif: ens192
        state: present
```

今回、Bergenholm サーバ上で ansible-playbook を実行する前提があったので local_action としているが、別のホストの場合は以下のようになる。

```yaml+jinja
    - name: Bergenholm 上に新しい VM の設定を登録する
      bergenholm_host:
        uuid: "{{ newvm.ansible_facts.hw_product_uuid }}"
        params:
          groups:
            - centos7
            - centos.amd64
          hostname: "{{ vm_name }}"
          ipaddr: "{{ vm_ipaddr }}"
          netif: ens192
        state: present
      delegate_to: bergenholmserver.localdomain
```

各パラメータの説明は以下の通り：

* uuid: インストール先サーバの System UUID。物理マシンなら BIOS、各種 VM であれば設定ファイル等で定義されている。
* state: よくある present, absent の他に、OS インストールが完了した状態 (installed)、OS インストールが未完の状態 (uninstalled) の４つの値を指定できる。
* params: ホスト（ここでは VM）単位に設定すべきパラメータの一覧。Bergenholm では基本的に JSON でパラメータを記述するが、本モジュールでは YAML で良い。
* url: Bergenholm サーバの REST API の URL。いずれにせよ今はまだない VM の為のコマンド実行であるので、local_action か delegate_to を使用する必要がある。

また、Playbook 開始時点でアクセス不可なホストを含むインベントリを使用する場合、Playbook の gather_facts は false にしておく必要がある。ハマりどころなので注意。

では、Let's Play.

```
~/ansible-bergenholm$ time ansible-playbook -i hosts.ini create.yml

PLAY [CentOS 7 VM を用意する] ********************************************************************************************************************

TASK [ESXi 上に VM を作成する] *********************************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]

TASK [ESXi 上の VM の情報を取得する] ******************************************************************************************************************
ok: [ansibletest3 -> localhost]
ok: [ansibletest1 -> localhost]
ok: [ansibletest2 -> localhost]

TASK [Bergenholm 上に新しい VM の設定を登録する] *********************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]

RUNNING HANDLER [VM を再起動して OS インストールを開始する] **************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]

PLAY [OS インストール後の確認を行う] *********************************************************************************************************************

TASK [OS インストール完了待ち] ************************************************************************************************************************
ok: [ansibletest3]
ok: [ansibletest1]
ok: [ansibletest2]

TASK [Facts を取得する] **************************************************************************************************************************
ok: [ansibletest2]
ok: [ansibletest3]
ok: [ansibletest1]

TASK [ディストリビューション情報を表示する] *******************************************************************************************************************
ok: [ansibletest1] => {
    "msg": "CentOS 7.4.1708 (Core)"
}
ok: [ansibletest2] => {
    "msg": "CentOS 7.4.1708 (Core)"
}
ok: [ansibletest3] => {
    "msg": "CentOS 7.4.1708 (Core)"
}

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=7    changed=3    unreachable=0    failed=0   
ansibletest2               : ok=7    changed=3    unreachable=0    failed=0   
ansibletest3               : ok=7    changed=3    unreachable=0    failed=0   


real	22m20.011s
user	1m20.872s
sys	0m14.432s
```

***Wonderful!!!!***
インベントリで定義された VM ３つが作成され、CentOS 7.4.1708 がインストールされた事がわかる。
では、同じコマンドを再度実行してみよう。Let's play.

```
~/ansible-bergenholm$ time ansible-playbook -i hosts.ini create.yml

PLAY [CentOS 7 VM を用意する] ********************************************************************************************************************

TASK [ESXi 上に VM を作成する] *********************************************************************************************************************
ok: [ansibletest3 -> localhost]
ok: [ansibletest1 -> localhost]
ok: [ansibletest2 -> localhost]

TASK [ESXi 上の VM の情報を取得する] ******************************************************************************************************************
ok: [ansibletest3 -> localhost]
ok: [ansibletest2 -> localhost]
ok: [ansibletest1 -> localhost]

TASK [Bergenholm 上に新しい VM の設定を登録する] *********************************************************************************************************
ok: [ansibletest2 -> localhost]
ok: [ansibletest3 -> localhost]
ok: [ansibletest1 -> localhost]

PLAY [OS インストール後の確認を行う] *********************************************************************************************************************

TASK [OS インストール完了待ち] ************************************************************************************************************************
ok: [ansibletest2]
ok: [ansibletest3]
ok: [ansibletest1]

TASK [Facts を取得する] **************************************************************************************************************************
ok: [ansibletest2]
ok: [ansibletest3]
ok: [ansibletest1]

TASK [ディストリビューション情報を表示する] *******************************************************************************************************************
ok: [ansibletest1] => {
    "msg": "CentOS 7.4.1708 (Core)"
}
ok: [ansibletest2] => {
    "msg": "CentOS 7.4.1708 (Core)"
}
ok: [ansibletest3] => {
    "msg": "CentOS 7.4.1708 (Core)"
}

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=6    changed=0    unreachable=0    failed=0   
ansibletest2               : ok=6    changed=0    unreachable=0    failed=0   
ansibletest3               : ok=6    changed=0    unreachable=0    failed=0   


real	0m28.405s
user	0m9.816s
sys	0m1.384s
```

***Cool!!!!***
bergenholm_host (と bergenholm_group) モジュールは冪等性を持っているので、現状のリソース登録を調べて、必要が無ければ何もしない。Ansible モジュールたる者、冪等性ぐらい備えていて当然だ。

# VM を自動削除する

では、作ったばかりの VM 群を削除しよう。delete.yml を見てみる。

```yaml+jinja:delete.yml
- name: VM を削除する
  hosts: targets
  gather_facts: false
  tasks:
    - name: ESXi 上の VM 情報を取得する
      local_action:
        module: vsphere_guest
        vcenter_hostname: "{{ esxi_host }}"
        username: "{{ esxi_user }}"
        password: "{{ esxi_pass }}"
        guest: "{{ vm_name }}"
        vmware_guest_facts: true
        validate_certs: false
        esxi:
          datacenter: ha-datacenter
          hostname: eval
      register: newvm
      ignore_errors: true
    - name: ESXi 上の VM を削除する
      local_action:
        module: vsphere_guest
        vcenter_hostname: "{{ esxi_host }}"
        username: "{{ esxi_user }}"
        password: "{{ esxi_pass }}"
        guest: "{{ vm_name }}"
        state: absent
        force: true
        validate_certs: false
        esxi:
          datacenter: ha-datacenter
          hostname: eval
    - name: Bergenholm 上の VM の設定を削除する
      local_action:
        module: bergenholm_host
        uuid: "{{ newvm.ansible_facts.hw_product_uuid }}"
        state: absent
      when: newvm | success
```

更に簡単。最早説明は要るまい。
では、Let's play.

```
~/ansible-bergenholm$ time ansible-playbook -i hosts.ini delete.yml

PLAY [VM を削除する] *****************************************************************************************************************************

TASK [ESXi 上の VM 情報を取得する] *******************************************************************************************************************
ok: [ansibletest3 -> localhost]
ok: [ansibletest2 -> localhost]
ok: [ansibletest1 -> localhost]

TASK [ESXi 上の VM を削除する] *********************************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest3 -> localhost]
changed: [ansibletest2 -> localhost]

TASK [Bergenholm 上の VM の設定を削除する] ************************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest3 -> localhost]
changed: [ansibletest2 -> localhost]

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=3    changed=2    unreachable=0    failed=0   
ansibletest2               : ok=3    changed=2    unreachable=0    failed=0   
ansibletest3               : ok=3    changed=2    unreachable=0    failed=0   


real	0m19.939s
user	0m8.300s
sys	0m0.924s
```

***Amazing!!!!***
もちろん、delete.yml も再度実行できる。

```
~/ansible-bergenholm$ time ansible-playbook -i hosts.ini delete.yml

PLAY [VM を削除する] *****************************************************************************************************************************

TASK [ESXi 上の VM 情報を取得する] *******************************************************************************************************************
fatal: [ansibletest1 -> localhost]: FAILED! => {"changed": false, "msg": "No such VM ansibletest1. Fact gathering requires an existing vm"}
...ignoring
fatal: [ansibletest3 -> localhost]: FAILED! => {"changed": false, "msg": "No such VM ansibletest3. Fact gathering requires an existing vm"}
...ignoring
fatal: [ansibletest2 -> localhost]: FAILED! => {"changed": false, "msg": "No such VM ansibletest2. Fact gathering requires an existing vm"}
...ignoring

TASK [ESXi 上の VM を削除する] *********************************************************************************************************************
ok: [ansibletest2 -> localhost]
ok: [ansibletest1 -> localhost]
ok: [ansibletest3 -> localhost]

TASK [Bergenholm 上の VM の設定を削除する] ************************************************************************************************************
skipping: [ansibletest1]
skipping: [ansibletest2]
skipping: [ansibletest3]

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=2    changed=0    unreachable=0    failed=0   
ansibletest2               : ok=2    changed=0    unreachable=0    failed=0   
ansibletest3               : ok=2    changed=0    unreachable=0    failed=0   


real	0m6.962s
user	0m5.584s
sys	0m0.588s
```

***Yes!!!!***
今回の delete.yml では、bergenholm_host モジュールは vsphere_guest モジュールが拾ってくる VM のシステム UUID に依存している。２回目の実行では VM が既に存在しないため、when: で bergenholm_host の実行をスキップしている。もし UUID が確定しているのであればスキップする必要はない。

# Ubuntu 16.04 VM を自動構築する

create.yml を vi で編集して、下記のようにしてみよう。

```diff
~/ansible-bergenholm$ git diff
diff --git a/create.yml b/create.yml
index 6e4f2fd..6f49227 100644
--- a/create.yml
+++ b/create.yml
@@ -1,4 +1,4 @@
-- name: CentOS 7 VM を用意する
+- name: Ubuntu 16.04 VM を用意する
   hosts: targets
   gather_facts: false
   tasks:
@@ -52,8 +52,8 @@
         uuid: "{{ newvm.ansible_facts.hw_product_uuid }}"
         params:
           groups:
-            - centos7
-            - centos.amd64
+            - ubuntu1604
+            - ubuntu.amd64
           hostname: "{{ vm_name }}"
           ipaddr: "{{ vm_ipaddr }}"
           netif: ens192
```

これで Ubuntu 16.04 が入った VM ３つが作成されるはずである。
では、Let's play.

```
~/git/ansible-bergenholm$ time ansible-playbook -i hosts.ini create.yml

PLAY [Ubuntu 16.04 VM を用意する] ****************************************************************************************************************

TASK [ESXi 上に VM を作成する] *********************************************************************************************************************
changed: [ansibletest3 -> localhost]
changed: [ansibletest2 -> localhost]
changed: [ansibletest1 -> localhost]

TASK [ESXi 上の VM の情報を取得する] ******************************************************************************************************************
ok: [ansibletest2 -> localhost]
ok: [ansibletest3 -> localhost]
ok: [ansibletest1 -> localhost]

TASK [Bergenholm 上に新しい VM の設定を登録する] *********************************************************************************************************
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]
changed: [ansibletest1 -> localhost]

RUNNING HANDLER [VM を再起動して OS インストールを開始する] **************************************************************************************************
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]
changed: [ansibletest1 -> localhost]

PLAY [OS インストール後の確認を行う] *********************************************************************************************************************

TASK [OS インストール完了待ち] ************************************************************************************************************************
ok: [ansibletest2]
ok: [ansibletest3]
ok: [ansibletest1]

TASK [Facts を取得する] **************************************************************************************************************************
ok: [ansibletest1]
ok: [ansibletest2]
ok: [ansibletest3]

TASK [ディストリビューション情報を表示する] *******************************************************************************************************************
ok: [ansibletest1] => {
    "msg": "Ubuntu 16.04 (xenial)"
}
ok: [ansibletest2] => {
    "msg": "Ubuntu 16.04 (xenial)"
}
ok: [ansibletest3] => {
    "msg": "Ubuntu 16.04 (xenial)"
}

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=7    changed=3    unreachable=0    failed=0   
ansibletest2               : ok=7    changed=3    unreachable=0    failed=0   
ansibletest3               : ok=7    changed=3    unreachable=0    failed=0   


real	31m50.377s
user	1m45.036s
sys	0m19.536s
```

***Brilliant!!!!***
期待通り、Ubuntu 16.04 の VM が３つ作成された。
では、後片付けをしよう。

```
~/ansible-bergenholm$ time ansible-playbook -i hosts.ini delete.yml

PLAY [VM を削除する] *****************************************************************************************************************************

TASK [ESXi 上の VM 情報を取得する] *******************************************************************************************************************
ok: [ansibletest1 -> localhost]
ok: [ansibletest3 -> localhost]
ok: [ansibletest2 -> localhost]

TASK [ESXi 上の VM を削除する] *********************************************************************************************************************
changed: [ansibletest3 -> localhost]
changed: [ansibletest1 -> localhost]
changed: [ansibletest2 -> localhost]

TASK [Bergenholm 上の VM の設定を削除する] ************************************************************************************************************
changed: [ansibletest1 -> localhost]
changed: [ansibletest2 -> localhost]
changed: [ansibletest3 -> localhost]

PLAY RECAP **********************************************************************************************************************************
ansibletest1               : ok=3    changed=2    unreachable=0    failed=0   
ansibletest2               : ok=3    changed=2    unreachable=0    failed=0   
ansibletest3               : ok=3    changed=2    unreachable=0    failed=0   


real	0m20.222s
user	0m8.280s
sys	0m0.968s
```

***Excellent!!!!***

今回は VMware ESXi 上の VM で実験を行ったが、BIOS のシステム UUID さえ判っていれば、Bergenholm を使って物理マシンだろうが KVM だろうが OS の自動インストールが出来る。

# 最後に

興味を持って頂けたなら、是非 Bergenholm や Ansible bergenholm_host / bergenholm_group モジュールを使ってみて欲しい。

なお、「Ansible」という名称は [SF小説に出てくる超光速通信装置](https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%B3%E3%82%B7%E3%83%96%E3%83%AB)、「Bergenholm」は同じく [SF小説の超光速エンジン](https://en.wikipedia.org/wiki/Inertialess_drive)から採られている。

※この記事は [Ansible Advent Calendar 2017](https://qiita.com/advent-calendar/2017/ansible) の 12/22 分です。