AnsibleのVmware系モジュールを使ったのでまとめ

# はじめに

タイトルのようにAnsibleでVmwareのGuestVMを作ったり色々便利に使いたい、というケースがあったのでこれを機に触ってみました。 
ついでにDynamic Inventoryも使えると便利かなと思い、こちらも触ってみました。


# 動作検証環境

- Vmware側
    - VMware vCenter Server Appliance, 6.7.0, 42000
    - VMware ESXi, 6.7.0, 13006603
- クライアント(Ansible実行環境)側
    - OS: CentOS 7
        - 一応Dockerのpython:latestでも試しました
    - Python 3.8.1
        - python2系でもvmware系モジュールは動きますが、Dynamic Inventoryを使おうとするとsetuptoolsという依存パッケージがPython 3.5以上でないと対応してなかったため3.5以上のほうが幸せになれそうです
    - ansible==2.9.5
    - pyvmomi==6.7.3

# 使用したモジュール

[Vmwareモジュール一覧](https://docs.ansible.com/ansible/latest/modules/list_of_cloud_modules.html#vmware)

Vmware系のモジュールは歴史的には大きく分けて2種類あったようで、例えばGuestVMの作成・削除をするモジュールとしては

- [vmware_guestモジュール](https://docs.ansible.com/ansible/latest/modules/vmware_guest_module.html#vmware-guest-module)
    - pythonのPyVmomiパッケージを利用
    - **推奨**
- [vsphere_guestモジュール](https://docs.ansible.com/ansible/2.8/modules/vsphere_guest_module.html)
    - pythonのPysphereパッケージを利用
    - 長らく`DEPRECATED`だったが、ansible 2.9でついに**削除**された

の二通りの書き方があったようですが、上記のようにvmware_guestに統合されたようなので、こちらを使っていきます。
（細かい歴史的経緯はよく知りません。。）

# 各種準備

## Vmware側

vmware系モジュールの多くはvCenterサーバ経由でのAPIを利用しているようで、vCenterサーバがほぼ必須となります。 
(vsphere系モジュールにはESXiに対してアクションできるものもあったとか何とか） 
私が検証した環境にはvCenterサーバがなかったので仮想アプライアンスをデプロイするところから始めましたが、今回はこちらの内容は割愛します。

vCenterの各種情報をパラメータとして指定する必要があるのであらかじめ整理・取得しておきましょう。
今回は例として以下とします。

|vCenter情報 |例 |
|---|---|
|ホスト名 | 192.168.0.1 (IPアドレスで可)|
|ユーザ名|`administrator@vsphere.local`|
|パスワード|`password`|
|データセンター名|`Datcenter`|

## クライアント(Ansible実行環境)側

今回は簡単に検証したかったのでansibleも入っていない状況からバージョン指定せずにインストールしていきます。

```bash
$ pip install ansible pyvmomi
```

# 実際にPlaybook等を書いていく

ここからPlaybookなどを書いていきますが、その前に以降のPlaybookで使い回しするであろうvCenterの情報をあらかじめ変数としてファイルに書いておきましょう。

```yaml:vars_vmware.yml
vcenter_hostname: 192.168.0.1
vcenter_username: administrator@vsphere.local
vcenter_password: password
vcenter_datacenter: Datacenter
```

## Vmwareへの接続確認

細かい操作・変更を始める前にちゃんとVmwareに接続できているか確認してみましょう。
とりあえずシンプルそうな[vmware_about_info](https://docs.ansible.com/ansible/latest/modules/vmware_about_info_module.html)モジュールを使ってました。
`tasks`の中身はリンクにあるサンプルほぼそのままです。
一般的なモジュールはssh接続したリモートサーバで実行するものが多いですが、
vmware系のモジュールは大抵`delegate_to: localhost`と書かれているように`pyvmomi`パッケージを持つansible実行ホストで実行します。

```yaml:test.yml
- hosts: local
  gather_facts: false
  vars_files:
  - vars_vmware.yml
  tasks:
  - name: Provide information about vCenter
    vmware_about_info:
      hostname: "{{ vcenter_hostname }}"
      username: "{{ vcenter_username }}"
      password: "{{ vcenter_password }}"
      validate_certs: false
    delegate_to: localhost
    register: cluster_info

  - debug:
      var: cluster_info
```

インベントリも今回はlocalhostを設定しました。

```ini:local.ini 
[local]
localhost
```

Playbookを実行してみます。

```bash
$ ansible-playbook -i local.ini test.yml 

PLAY [local] ***********************************************************************************************************************************************************************************************

TASK [Provide information about vCenter] *******************************************************************************************************************************************************************
ok: [localhost -> localhost]

TASK [debug] ***********************************************************************************************************************************************************************************************
ok: [localhost] => {
    "cluster_info": {
        "about_info": {
            "api_type": "VirtualCenter", 
            "api_version": "6.7.3", 
            "build": "15129973", 
            "instance_uuid": "*****-******", 
            "license_product_name": "VMware VirtualCenter Server", 
            "license_product_version": "6.0", 
            "locale_build": "000", 
            "locale_version": "INTL", 
            "os_type": "linux-x64", 
            "product_full_name": "VMware vCenter Server 6.7.0 build-15129973", 
            "product_line_id": "vpx", 
            "product_name": "VMware vCenter Server", 
            "vendor": "VMware, Inc.", 
            "version": "6.7.0"
        }, 
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "failed": false
    }
}

PLAY RECAP *************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
```

エラーなく接続して値が取得できてますね！

## GuestVMの作成(From Template)

今度は[vmware_guest](https://docs.ansible.com/ansible/latest/modules/vmware_guest_module.html)モジュールを使ってGuestVMを作成します。
おそらく一番使われていそうなモジュールだと思いますが、実際に動かす前に諸々準備をする必要があります。

### VMテンプレートの作成

vmware_guestモジュールで最小限のパラメータでゲストVMを作成した場合はOSがインストールされる前の空っぽの状態になります。
せっかくAnsibleでゲストVMを作成したのにそのままパッケージインストールやアプリのデプロイができないのは片手落ち感がありました。
KickStart用のISOイメージを作成・マウントして...みたいな方法もできそうですが、
ここはスマートに（？）VMテンプレートの作成してクローンとしてGuestVMを作成しようと思います。

VMテンプレートとしてはお好きなものを作ってもらえればいいので詳細は省きますが、
私の例ではCentOS7を最小構成インストールしたものを使用しました。
また、その状態のテンプレートを使用した場合、VMNICが有効にならないなどの事象があったため、
追加で`open-vm-tools`パッケージをインストールする必要があるようなので、これと`open-vm-tools`を動かす`perl`だけははインストールしておきましょう。

```bash
$ sudo yum install -y open-vm-tools perl
```

※ Debian系の場合でも同名のパッケージがあるようなのでapt等でインストールしましょう。

今回は`Template_CentOS`という名前の仮想マシンを作成し、テンプレートに変換しました。
※ ここではテンプレートに変換していますが、起動中のVMを指定しても特に問題なくクローン作成されるようです。


### folderの確認

また`vmware_guest`モジュールでGuestVMの**作成**をする場合には`folder`というパラメータを入れる必要があるので、この`folder`の値を確認します。
Vmware側のドキュメントやvSphereWebClient上から確認する方法が分かりませんでしたが（ご指摘いただけると幸いです）、
[vmware_folder_info](https://docs.ansible.com/ansible/latest/modules/vmware_folder_info_module.html)モジュールで取得するのが手っ取り早かったです。

```yml:get_folders.yml
- hosts: local
  gather_facts: false
  vars_files:
  - vars_vmware.yml
  tasks:
  - name: Provide information about vCenter folders
    vmware_folder_info:
      hostname: "{{ vcenter_hostname }}"
      username: "{{ vcenter_username }}"
      password: "{{ vcenter_password }}"
      datacenter: "{{ vcenter_datacenter }}"
      validate_certs: false
    delegate_to: localhost
    register: folder_info
  - debug:
      var: folder_info
```

```bash
$ ansible-playbook -i local.ini get_folders.yml 

PLAY [local] ***********************************************************************************************************************************************************************************************

TASK [Provide information about vCenter folders] ***********************************************************************************************************************************************************
ok: [localhost -> localhost]

TASK [debug] ***********************************************************************************************************************************************************************************************
ok: [localhost] => {
    "folder_info": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": false, 
        "failed": false, 
        "folder_info": {
            "datastoreFolders": {
                "path": "/Datacenter/datastore", 
                "subfolders": {}
            }, 
            "hostFolders": {
                "path": "/Datacenter/host", 
                "subfolders": {}
            }, 
            "networkFolders": {
                "path": "/Datacenter/network", 
                "subfolders": {}
            }, 
            "vmFolders": {
                "path": "/Datacenter/vm", 
                "subfolders": {}
            }
        }
    }
}

PLAY RECAP *************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

この中の`vmFolders`に記載された`/Datacenter/vm`が仮想マシン用のfolderのようです。

### やっとGuestVMの作成

これらのパラメータを使って、ようやく[vmware_guest](https://docs.ansible.com/ansible/latest/modules/vmware_guest_module.html)モジュールを使用できます。

仮想マシン名は`test1`としています。
仮想マシン名は(厳密には一意ではないようですが)一意の名前を指定しましょう。
既存の仮想マシン名を指定した場合はGuestVMの作成は起こらず、既存の仮想マシンの`state`で指定した電源状態に変化するだけのようです。（いわゆる冪等性ってやつだと思います）

なお今回は`Cluster`という名前のvSphereクラスタを作成し、その配下にGuestVMを作成しましたが、
クラスタがない場合や作成するESXiホストを指定したい場合は`cluster`の代わりに`esxi_hostname`を指定してください。

その他パラメータは[ドキュメント]((https://docs.ansible.com/ansible/latest/modules/vmware_guest_module.html))を確認してください。

```yml:create_guestvm.yml
- hosts: local
  gather_facts: false
  vars_files:
  - vars_vmware.yml
  tasks:
  - name: Create a virtual machine on given ESXi hostname
    vmware_guest:
      name: test1                  # VM名
      hostname: "{{ vcenter_hostname }}"
      username: "{{ vcenter_username }}"
      password: "{{ vcenter_password }}"
      datacenter: "{{ vcenter_datacenter }}"
      folder: /Datacenter/vm　　　　# vmware_folder_infoで取得したfolder名
      validate_certs: false
      template: Template_CentOS　  # 準備で作成したテンプレート名
      state: poweredon             # 起動状態にする
      # guest_id: centos7_64Guest  # 仮想マシンの作成時には必須であり、テンプレートからの作成時には不要
      cluster: Cluster
      # esxi_hostname: 192.168.*.* # clusterないしesxi_hostnameのいずれかを指定する
      disk:
      - size_gb: 16
        type: thin
        datastore: datastore
      hardware:
        memory_mb: 1024
        num_cpus: 1
        scsi: paravirtual
      networks:
      - name: VM Network
        start_connected: true
      wait_for_ip_address: yes
      customization:
        hostname: test1
    delegate_to: localhost
    register: vm_info

  - debug:
      var: vm_info
```

```bash
$ ansible-playbook -i local.ini create_guestvm.yml 

PLAY [local] ***********************************************************************************************************************************************************************************************

TASK [Create a virtual machine on given ESXi hostname] *****************************************************************************************************************************************************
changed: [localhost -> localhost]

TASK [debug] ***********************************************************************************************************************************************************************************************
ok: [localhost] => {
    "vm_info": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/bin/python"
        }, 
        "changed": true, 
        "failed": false, 
        "instance": {
            "annotation": "", 
            "current_snapshot": null, 
            "customvalues": {}, 
            "guest_consolidation_needed": false, 
            "guest_question": null, 
            "guest_tools_status": "guestToolsRunning", 
            "guest_tools_version": "10336", 
            "hw_cluster": "Cluster", 
            "hw_cores_per_socket": 1, 
            "hw_datastores": [
                "datastore"
            ], 
            "hw_esxi_host": "192.168.*.*", 
            "hw_eth0": {
                "addresstype": "assigned", 
                "ipaddresses": [
                    "192.168.*.*", 
~~~~ 中略 ~~~~
            "instance_uuid": "****^****", 
            "ipv4": "192.168.*.*", 
            "ipv6": null, 
            "module_hw": true, 
            "moid": "vm-***", 
            "snapshots": [], 
            "vimref": "vim.VirtualMachine:vm-***", 
            "vnc": {}
        }
    }
}

PLAY RECAP *************************************************************************************************************************************************************************************************
localhost                  : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

スペックによってはそれなりの時間がかかると思いますが、これでGuestVMが作成されましたね！
`.instance.ipv4`にIPアドレスの情報もあるので、[add_host](https://docs.ansible.com/ansible/latest/modules/add_host_module.html)モジュールで追加してその後のタスクを実行！なんてことも可能です。

## オマケ：VmwareのDynamic Inventoryを使ってみる

AnsibleはVmwareについてDynamic Inventoryを使用することができます。
Dynamic Inventory自体については以下を参考してもらえるのがよいですが、簡単にいうと
タスクを実行したい対象が数台ぐらいであればVMのホスト名（IP）をインベントリに書けばいいのですが、対象が3桁・4桁と増えていくとそれも大変になります。
そこでホスト情報を直接記載するのではなく、API等からホスト情報を取得することで動的にインベントリに必要な情報を取ってこよう、というものです。
https://docs.ansible.com/ansible/latest/user_guide/intro_dynamic_inventory.html

こちらのページを参考に、ついでにDynamic Inventoryを使ってみました。
https://docs.ansible.com/ansible/latest/scenario_guides/vmware_scenarios/vmware_inventory.html


準備として、先にインストールした`pyvmomi`モジュールに加えて、[vSphere Automation SDK](https://code.vmware.com/web/sdk/65/vsphere-automation-python)が必要なようです。
こちらの[手順](https://github.com/vmware/vsphere-automation-sdk-python#installing-required-python-packages)にしたがってインストールします。
なお、[動作検証環境](#動作検証環境)で記載したように`setuptools`パッケージがpython3.5未満ではインストールできなかったため、python3系で実行しています。
(`setuptools`はバージョン指定をすればインストールできたのですが、その後のsdkのインストールには失敗したのでそれ以降は追いかけてません。回避方法等があればご指摘ください)

```bash
$ pip install --upgrade pip setuptools
$ pip install --upgrade git+https://github.com/vmware/vsphere-automation-sdk-python.git
```

まず`ansible.cfg`でVmwareのDynamic Inventoryに必要なプラグインを有効化します。

```config:ansible.cfg 
[inventory]
enable_plugins = vmware_vm_inventory
```

続いて、インベントリですが、vCenterの情報を記載します。またファイル名の末尾が`.vmware.yml` or `.vmware.yaml`である必要があります。

```yaml:inventory.vmware.yml 
plugin: vmware_vm_inventory
strict: False
hostname: 192.168.0.1
username: administrator@vsphere.local
password: password
validate_certs: False
with_tags: True
```

ここで確認で`ansible-inventory`コマンドを実行するとJSON形式のインベントリ情報が出力されます。

```bash
$ ansible-inventory -i inventory.vmware.yml --list
{
    "_meta": {
        "hostvars": {
            "test1_****-****": {
                "ansible_host": "192.168.*.*",
                "config.cpuHotAddEnabled": false,
                "config.cpuHotRemoveEnabled": false,
                "config.hardware.numCPU": 1,
                "config.instanceUuid": "****-****",
                "config.name": "test1",
                "config.template": false,
                "guest.guestId": "centos7_64Guest",
                "guest.guestState": "running",
                "guest.hostName": "localhost.localdomain",
                "guest.ipAddress": "192.168.*.*",
                "name": "test1",
                "runtime.maxMemoryUsage": 1024
            },
~~~ 中略 ~~~
        }
    },
    "all": {
        "children": [
            "centos7_64Guest",
            "other3xLinux64Guest",
            "poweredOff",
            "poweredOn",
            "ungrouped"
        ]
    },
    "centos7_64Guest": {
        "hosts": [
            "test1_****-****",
            "",
~~~ 中略 ~~~
        ]
    },

~~~ 中略 ~~~
}
```

`all`グループの`children`として`guest_id`や電源状態のグループがあり、`hosts`の中に配列でホスト名が記載されており、ホスト毎の詳細な情報は`host_vars`として記載されているようです。
なお、ホスト名はVM名に`hw_product_uuid`というUUIDをつけた形式のようです。

最後に例として[vmware_guest_tools_upgrade](https://docs.ansible.com/ansible/latest/modules/vmware_guest_tools_upgrade_module.html)モジュールを使ってVmwareToolのアップグレードをやってみたいと思います。
今回は他のVMに影響を与えたくなかったのでtest1のみに対してアップグレードを実行しますが、
hostsを`all`やグループ名にすれば複数台に対して実行されるはずです。

```yaml:vmtool_upgrade.yml
- hosts: test1_***-****
  gather_facts: false
  vars_files:
  - vars_vmware.yml
  tasks:
  - name: Upgrade VMware Tools using VM name
    vmware_guest_tools_upgrade:
      hostname: "{{ vcenter_hostname }}"
      username: "{{ vcenter_username }}"
      password: "{{ vcenter_password }}"
      datacenter: "{{ vcenter_datacenter }}"
      name: "{{ name }}"
      validate_certs: false
    delegate_to: localhost
```

実行した結果、作成したばかりのVMだったためVmwareToolも最新版だったようで変化はありませんでしたが、
ある程度の期間と台数を運用している環境で実行すれば中々強力な作業自動化に繋がると思います。

```bash
$ ansible-playbook -i inventory.vmware.yml play.yml -v
Using /work/ansible.cfg as config file

PLAY [test1_***-***] **********************************************************************************************************************************************************

TASK [Upgrade VMware Tools using VM name] ******************************************************************************************************************************************************************
ok: [test1_***-*** -> localhost] => {"changed": false, "msg": "VMware tools is already up to date"}

PLAY RECAP *************************************************************************************************************************************************************************************************
test1_***-*** : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```