【Ansible】jinjaを用いてcsvファイルからインベントリファイルを作成する

# はじめに
[前回](https://qiita.com/basabasakhr/items/f04514e3490cedbdf0ef)、Ansibleでログを取得するプレイブックを作成しました。
プレイブック自体はインベントリ情報と取得したいコマンドを更新すれば、ターゲットへはSSH接続でログ取得可能になります。
実際の検証では環境が都度都度変更することもあるかと思います。
せっかく作ったプレイブックを仮にAnsibleを知らない人が操作することになってもインベントリファイルが作れるように、
csvの更新だけで、ターゲットへのログ取得を可能にするインベントリファイルを生成する仕組みを作りたいと思います。
具体的には検証機器のログイン情報などを記載したcsvファイルと、jinjaのテンプレートファイルを作成したいと思います。
上記の２ファイルをAnsibleのtemplateモジュールを利用して、最終的なインベントリファイルを作ります。

# 概要
- jinja2のテンプレートを利用してインベントリファイルを作成
- インプットはcsvファイルのみ
- アウトプットはログ取得用のインベントリファイル
- インベントリファイルの内に記載するもの
    - ホストへログインするための情報
        - グループ
        - ホストのIPアドレス
        - SSHユーザー
        - SSHパスワード
    - 取得したいコマンドを記載するファイル名の変数
        - showコマンド
        - routeコマンド
- Ansibleのtemplateモジュールをプレイブック内のタスクに記載して、インベントリファイルを生成

今回の作成にはプレイブックも含まれますが、次回以降はcsvファイルの更新だけ行い、作成したプレイブックでインベントリファイルが更新できるようにするのが目的です。

# jinjaって？
[jinja](https://jinja.palletsprojects.com/en/3.0.x/)はPython用のテンプレートエンジンです。
と言われても私はイメージできませんが、要はあるインプットデータを基にしてテンプレートに従い新たなデータを生成します。
可変する値を持つ動的なインベントリファイルの生成やNW機器のコンフィグを生成することができます。

![テンプレート.002.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/224980/15548a16-b0dd-7111-d727-7e11d4d2d44e.jpeg)




テンプレート自体は自作する必要があり、またそのテンプレートファイルを使用するためのプレイブックの作成も必要になります。
しかし、一度インプットデータに合わせたテンプレートファイルを作成してしまえば、今後はcsvファイルの更新だけでインベントリファイルが統一的な記載で生成されることになります。
Ansibleではjinjaを利用するための[templateモジュール](https://docs.ansible.com/ansible/2.9/modules/template_module.html)が用意されており、このモジュールを利用してファイルを生成します。


# 生成ファイル（インベントリファイル）
先にどんなファイルができるのかイメージを掴むため、作成結果のファイルを貼ります。
まだ空行がうまく扱えません。。




```:dynamic_inventory.ini
[ios:children] # os種別でグループを作り、子グループがあればos種別の配下に
l2
dist
internet



[l2] # 子(なければ親)グループにホストのログイン情報を記載。SSH接続前提。
edge-sw01 ansible_host=10.10.20.172 ansible_ssh_user=cisco ansible_ssh_pass=cisco
[dist]
dist-rtr01 ansible_host=10.10.20.175 ansible_ssh_user=cisco ansible_ssh_pass=cisco
dist-rtr02 ansible_host=10.10.20.176 ansible_ssh_user=cisco ansible_ssh_pass=cisco
[internet]
internet-rtr01 ansible_host=10.10.20.181 ansible_ssh_user=cisco ansible_ssh_pass=cisco

[iosxr] # 子グループがないパターン
core-rtr01 ansible_host=10.10.20.173 ansible_ssh_user=cisco ansible_ssh_pass=cisco
core-rtr02 ansible_host=10.10.20.174 ansible_ssh_user=cisco ansible_ssh_pass=cisco

[nxos]
dist_sw01 ansible_host=10.10.20.178 ansible_ssh_user=cisco ansible_ssh_pass=cisco
dist_sw02 ansible_host=10.10.20.179 ansible_ssh_user=cisco ansible_ssh_pass=cisco

[all:vars]　# これは環境によらないので、固定
ansible_connection=network_cli

[ios:vars] # 親、子グループごとに記載。親はos種別の定義がマスト
ansible_network_os=ios

[l2:vars] # 子グループがある場合は子グループの変数にコマンド記載のファイルを定義
commands_show=l2_show.txt
commands_route=l2_route.txt

[dist:vars]
commands_show=dist_show.txt
commands_route=dist_route.txt


[internet:vars]
commands_show=internet_show.txt
commands_route=internet_route.txt


[iosxr:vars]# 子グループがない場合は親グループの変数にos種別とコマンド記載のファイルを定義
ansible_network_os=iosxr
commands_show=iosxr_show.txt
commands_route=iosxr_route.txt

[nxos:vars]
ansible_network_os=nxos
commands_show=nxos_show.txt
commands_route=nxos_route.txt

```

# スクリプト
## 1. プレイブック
まずはテンプレートを利用するためのプレイブックを作成します。

```:template.yml
---
- hosts: localhost
  gather_facts: no
  connection: local

  vars:
    template: templates/inventory.j2          # テンプレートファイルの指定
    host_list: hosts_list.csv                 # インプットデータであるcsvファイルの指定

  tasks:
    - name: read inventory csv
      read_csv:                               # read_csvモジュールでcsvファイルを読み込み
        path: "{{ host_list }}"
      run_once: yes
      register: inventory_requests            # 読み込んだ結果をレジスターに格納

    - name: display
      debug:
        msg: "{{ inventory_requests.list }}"  # 読み込んだデータはリスト
      run_once: yes

    - name: set command requests
      set_fact:
        lists: "{{ inventory_requests.list }}" # 使用するリスト部分のみを変数に格納
      run_once: yes

    - name: create inventory
      template:                                # テンプレートモジュールにてjinjaを利用
        src: "{{ template }}"                  # 利用するテンプレートファイルの指定
        dest: "dynamic_inventory.ini"          # 生成されるファイル名
        lstrip_blocks: yes                     # 先頭のスペース除去
```

## 2. csvファイル
インプットデータとして用いるcsvファイルです。

- "os"はios,iosxr,nxosのみ
- "os"が親グループになり、"group"があれば子グループとして認識
- ”os”、”group”は並び替えられている前提
- 例によってDEVNET sandboxの[Cisco Modeling Labs](https://devnetsandbox.cisco.com/RM/Diagram/Index/76e4189c-c221-410b-a238-3c7cf33a2071)のMulti Platform Networkのラボの情報

| os<br>（親グループ） | group<br>(子グループ) | host | Username | Password | ip |
|:-:|:-:|:-:|:-:|:-:|:-:|
|ios|l2|edge-sw01|cisco|cisco|10.10.20.172|
|ios|dist|dist-rtr01|cisco|cisco|10.10.20.175|
|ios|dist|dist-rtr02|cisco|cisco|10.10.20.176|
|ios|internet|internet-rtr01|cisco|cisco|10.10.20.181|
|nxos||dist_sw01|cisco|cisco|10.10.20.178|
|nxos||dist_sw02|cisco|cisco|10.10.20.179|
|iosxr||core-rtr01|cisco|cisco|10.10.20.173|
|iosxr||core-rtr02|cisco|cisco|10.10.20.174|

```:hosts_list.csv
os,group,host,Username,Password,ip
ios,l2,edge-sw01,cisco,cisco,10.10.20.172
ios,dist,dist-rtr01,cisco,cisco,10.10.20.175
ios,dist,dist-rtr02,cisco,cisco,10.10.20.176
ios,internet,internet-rtr01,cisco,cisco,10.10.20.181
nxos,,dist_sw01,cisco,cisco,10.10.20.178
nxos,,dist_sw02,cisco,cisco,10.10.20.179
iosxr,,core-rtr01,cisco,cisco,10.10.20.173
iosxr,,core-rtr02,cisco,cisco,10.10.20.174

```


## 3. テンプレートファイル
長いです。想定はios,iosxr,nxosの３種類のみを想定してます。
基本は3種類分をそれぞれ記載しているので、長くなってしまってますのでもっとシンプルに書けるようになりたいです。。

- jinja2構文
    - コメント {# ... #}
    - 制御構文 {% ...%}
    - 変数 {{ ... }}
    - 変数を使用しなければベタ書き
    - 空行、改行の制御には[Whitespace Control](https://www.greptips.com/posts/1282/)


```:templates/inventory.j2
{# create variable for parent-children relations #}
{# for ios #}
{% set ns = namespace(str='') %}                    # for分の中では変数がif文外で利用できないためfor文外で宣言
{% for item in lists %}
    {% if item.os == 'ios' %}
        {% if item.group != '' %}
            {% if ns.str == '' -%}
                [ios:children]
                {% set ns.str = item.group -%}      # if文外でも変数が有効になるようにnamespaceを利用
                {{ item.group }}
            {% else %}
                {% if ns.str != item.group -%}
                {{ item.group }}
                {% set ns.str = item.group %}
                {% endif %}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

{# for iosxr #}
{% set ns = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'iosxr' %}
        {% if item.group != '' %}
            {% if ns.str == '' -%}
                [iosxr:children]
                {% set ns.str = item.group -%}
                {{ item.group }}
            {% else %}
                {% if ns.str != item.group -%}
                {{ item.group }}
                {% set ns.str = item.group %}
               {% endif %}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

{# for nxos #}
{% set ns = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'nxos' %}
        {% if item.group != '' %}
            {% if ns.str == '' -%}
                [nxos:children]
                {% set ns.str = item.group -%}
                {{ item.group }}
            {% else %}
                {% if ns.str != item.group -%}
                    {{ item.group }}
                {% set ns.str = item.group %}
                {% endif %}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

{# create variables for child group #}
{# for ios #}
{% set ns2 = namespace(str='') %}
{% set ns3 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'ios' %}
        {% if item.group != '' %}
            {% if ns2.str == '' -%}
                {{ '[' + item.group + ']' }}
                {% set ns2.str = item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns2.str == item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns2.str != item.group -%}
                {{ '[' + item.group + ']' }}
                {% set ns2.str = item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% endif %}
        {% elif item.group == '' %}
            {% if ns3.str == '' -%}
                {{ '[' + item.os + ']' }}
                {% set ns3.str = item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns3.str == item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns3.str != item.os -%}
                {{ '[' + item.os + ']' }}
                {% set ns3.str = item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

{# for iosxr #}
{% set ns2 = namespace(str='') %}
{% set ns3 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'iosxr' %}
        {% if item.group != '' %}
                {% if ns2.str == '' -%}
                    {{ '[' + item.group + ']' }}
                    {% set ns2.str = item.group -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% elif ns2.str == item.group -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% elif ns2.str != item.group -%}
                    {{ '[' + item.group + ']' }}
                    {% set ns2.str = item.group -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% endif %}
        {% elif item.group == '' %}
                {% if ns3.str == '' -%}
                    {{ '[' + item.os + ']' }}
                    {% set ns3.str = item.os -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% elif ns3.str == item.os -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% elif ns3.str != item.os -%}
                    {{ '[' + item.os + ']' }}
                    {% set ns3.str = item.os -%}
                    {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
                {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

{# for nxos #}
{% set ns2 = namespace(str='') %}
{% set ns3 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'nxos' %}
        {% if item.group != '' %}
            {% if ns2.str == '' -%}
                {{ '[' + item.group + ']' }}
                {% set ns2.str = item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns2.str == item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns2.str != item.group -%}
                {{ '[' + item.group + ']' }}
                {% set ns2.str = item.group -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% endif %}
        {% elif item.group == '' %}
            {% if ns3.str == '' -%}
                {{ '[' + item.os + ']' }}
                {% set ns3.str = item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns3.str == item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% elif ns3.str != item.os -%}
                {{ '[' + item.os + ']' }}
                {% set ns3.str = item.os -%}
                {{ item.host + ' ansible_host=' + item.ip + ' ansible_ssh_user=' + item.Username + ' ansible_ssh_pass=' + item.Password }}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}

[all:vars]
ansible_connection=network_cli

{# create [ios:vars] #}
{% set ns4 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'ios' -%}
        {% if item.group == '' -%}
            {% if ns4.str == '' -%}
                [ios:vars]
            {% endif %}
            {% if ns4.str  == ''  -%}
                ansible_network_os=ios
            {% endif %}
            {% if ns4.str  == ''  -%}
                commands_show=ios_show.txt
            {% endif %}
            {% if ns4.str  == ''  -%}
                commands_route=ios_route.txt
            {% endif %}
            {% set ns4.str = item.os -%}
        {% elif item.group != '' %}
            {% if ns4.str  == '' -%}
                [ios:vars]
            {% endif %}
            {% if ns4.str == '' -%}
                ansible_network_os=ios

            {% endif %}
            {% if ns4.str != item.group -%}
                {{ '[' + item.group + ':vars]' }}
            {% endif %}
            {% if ns4.str != item.group -%}
                {{ 'commands_show=' + item.group + '_show.txt' }}
            {% endif %}
            {% if ns4.str != item.group -%}
                {{ 'commands_route=' + item.group + '_route.txt' }}
            {% endif %}

            {% set ns4.str = item.group -%}
        {% endif %}
    {% endif %}
{% endfor %}

{# create [iosxr:vars] #}
{% set ns4 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'iosxr' -%}
        {% if item.group == '' -%}
            {% if ns4.str == '' -%}
                [iosxr:vars]
            {% endif %}
            {% if ns4.str  == ''  -%}
                ansible_network_os=iosxr
            {% endif %}
            {% if ns4.str == '' -%}
                commands_show=iosxr_show.txt
            {% endif %}
            {% if ns4.str == '' -%}
                commands_route=iosxr_route.txt
            {% endif %}
            {% set ns4.str = item.os -%}
        {% elif item.group != '' %}
            {% if ns4.str  == '' -%}
                [iosxr:vars]
            {% endif %}
            {% if ns4.str  == '' -%}
                ansible_network_os=iosxr

            {% endif %}
            {% if ns4.str  != item.group  -%}
                {{ '[' + item.group + ':vars]' }}
            {% endif %}
            {% if ns4.str != item.group -%}
                {{ 'commands_show=' + item.group + '_show.txt' }}
            {% endif %}
            {% if ns4.str != item.group -%}
                {{ 'commands_route=' + item.group + '_route.txt' }}
            {% endif %}

            {% set ns4.str = item.group -%}
        {% endif %}
    {% endif %}
{% endfor %}

{# create [nxos:vars] #}
{% set ns4 = namespace(str='') %}
{% for item in lists %}
    {% if item.os == 'nxos' -%}
        {% if item.group == '' -%}
            {% if ns4.str == '' -%}
                [nxos:vars]
            {% endif %}
            {% if ns4.str == '' -%}
                ansible_network_os=nxos
            {% endif %}
            {% if ns4.str == '' -%}
                commands_show=nxos_show.txt
            {% endif %}
            {% if ns4.str == '' -%}
                commands_route=nxos_route.txt
            {% endif %}
            {% set ns4.str = item.os -%}
        {% elif item.group != '' %}
            {% if ns4.str  == '' -%}
                [nxos:vars]
            {% endif %}
            {% if ns4.str == '' -%}
                ansible_network_os=nxos

            {% endif %}
            {% if ns4.str != item.group  -%}
                {{ '[' + item.group + ':vars]' }}
                {% if ns4.str != item.group -%}
                    {{ 'commands_show=' + item.group + '_show.txt' }}
                {% endif %}
                 {% if ns4.str != item.group -%}
                    {{ 'commands_route=' + item.group + '_route.txt' }}

                {% endif %}
                {% set ns4.str = item.group -%}
            {% endif %}
        {% endif %}
    {% endif %}
{% endfor %}



```

# 実行結果
生成できたファイルは記載済みなので、Ansible-playbookコマンドの実行ログを添付。

```
❯ ansible-playbook -i localhost template.yml
[WARNING]: Unable to parse /Users/tomohidekimura/Documents/ansible/template-test/health_check/localhost as an inventory source
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [localhost] *********************************************************************************************************************************************

TASK [read inventory csv] ************************************************************************************************************************************
ok: [localhost]

TASK [display] ***********************************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "l2",
            "host": "edge-sw01",
            "ip": "10.10.20.172",
            "os": "ios"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "dist",
            "host": "dist-rtr01",
            "ip": "10.10.20.175",
            "os": "ios"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "dist",
            "host": "dist-rtr02",
            "ip": "10.10.20.176",
            "os": "ios"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "internet",
            "host": "internet-rtr01",
            "ip": "10.10.20.181",
            "os": "ios"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "",
            "host": "dist_sw01",
            "ip": "10.10.20.178",
            "os": "nxos"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "",
            "host": "dist_sw02",
            "ip": "10.10.20.179",
            "os": "nxos"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "",
            "host": "core-rtr01",
            "ip": "10.10.20.173",
            "os": "iosxr"
        },
        {
            "Password": "cisco",
            "Username": "cisco",
            "group": "",
            "host": "core-rtr02",
            "ip": "10.10.20.174",
            "os": "iosxr"
        }
    ]
}

TASK [set command requests] **********************************************************************************************************************************
ok: [localhost]

TASK [create inventory] **************************************************************************************************************************************
changed: [localhost]

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

無事に完了しました!


# 最後に
形は不揃いですが、意図したインベントリファイルが作成できました。
そんなに長くならないとたかをくくり何も考えず着手してしまった結果、整理できてないファイルが出来上がってしまいました。。
途中、テンプレートファイル内の変数にもスコープがあることやif文と組み合わせたloop.indexが動作しないなど、
想定外に相当時間を取られてしまいました。。デバッグのやり方がわからず、大変でした。
今後は、グループ変数を作るテンプレートと取得コマンドリストファイルを生成するプレイブックを作成したいと思います。