てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] 「つまずき Ansible 【Part36】ansible-navigator」ふりかえり

はじめに

2022/03/12 に、YouTube Live で「つまずき Ansible 【Part36】ansible-navigator」という配信をしました。

tekunabe.connpass.com

これまで Playbook の実行といえば、ansible-playbook コマンドでした。 最近 ansible-navigator という新たなツールが出てきました。TUI または CLI のPlaybook 実行ツールです。今回はこれをさわりました。


動画

youtu.be

ansible-navigator とは

個人的に感じている特徴は以下の通りです。

  • TUI を備えている
  • Ansibleの実行環境コンテナ(Execution Environment)を利用できる
  • Playbook 実行だけなくドキュメントの参照などもできる

準備

インストール

今回は pip でインストールします。

pip install ansible-navigator

ansible-runner などの依存パッケージがインストールされます。ansible-navigator から ansible-runner を呼び出すような関係になっています。

$ pip list
Package             Version
------------------- -------
ansible-navigator   1.1.0
ansible-runner      2.1.2
...(略)...

ansibleansible-core はインストールされません。

TUI による Playbook 実行

まずは実行

Playbook の実行にはサブコマンド run を指定します。その後、Playbookファイル名とインベントリファイルを指定します。

$ ansible-navigator run debug.yml -i inventory 

以下のような画面になります。これはPlayの一覧です。

  PLAY NAME    OK  CHANGED   UNREACHABLE    FAILED   SKIPPED   IGNORED   IN PROGRESS     TASK COUNT        PROGRESS
0│localhost     1        0             0         0         0         0             0              1        COMPLETE

左にある番号を入力すると、そのPlay内のタスクの一覧が表示されます。

更に、タスクの番号を入力するとそのタスクの実行結果の詳細が表示されます。

動画を見ていただくほうが、分かりやすいと思います。

このTUIのモードは、設定上は interactive モードと呼ばれています。

ansible-playbook コマンドと同じ形式でもOK

デフォルトは interactive モードですが、ansible-playbook コマンド実行時と同じ形式の stdout モードにも変更できます。

指定は設定ファイル(後述)で行うか、ansible-navigator コマンドで --mode stdout を指定します。

$ ansible-navigator run debug.yml -i inventory --mode stdout

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

TASK [debug] *******************************************************************
ok: [localhost] => {
    "msg": "sakana sakana sakana"
}

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

アーティファクト(ログ)の保存とリプレイ機能

ansible-navigator にはアーティファクト保存機能があります。JSON形式で保存されます。

アーティファクトの例

以下は、ios_command でshow ip route を実行して debug 表示するPlaybookの実行したときのアーティファクトです。

▼クリックして開く

{
    "version": "1.0.0",
    "plays": [
        {
            "playbook": "/home/admin/stumble/ios_show.yml",
            "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
            "play": "ios",
            "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
            "play_pattern": "ios",
            "name": "ios",
            "pattern": "ios",
            "uuid": "a29dee92-8fae-871d-4141-000000000007",
            "__play_name": "ios",
            "tasks": [
                {
                    "playbook": "/home/admin/stumble/ios_show.yml",
                    "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
                    "play": "ios",
                    "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
                    "play_pattern": "ios",
                    "task": "exec show commands",
                    "task_uuid": "a29dee92-8fae-871d-4141-000000000009",
                    "task_action": "cisco.ios.ios_command",
                    "task_args": "",
                    "task_path": "/home/admin/stumble/ios_show.yml:6",
                    "host": "ios01",
                    "uuid": "8db1e3c2-ab2f-4ff7-917b-cd5bab31cfdb",
                    "__host": "ios01",
                    "__result": "OK",
                    "__changed": false,
                    "__duration": "1s",
                    "__number": 0,
                    "__task": "exec show commands",
                    "__task_action": "cisco.ios.ios_command",
                    "remote_addr": "ios01",
                    "res": {
                        "changed": false,
                        "stdout": [
                            "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\n       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \n       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\n       E1 - OSPF external type 1, E2 - OSPF external type 2\n       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\n       ia - IS-IS inter area, * - candidate default, U - per-user static route\n       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\n       a - application route\n       + - replicated route, % - next hop override, p - overrides from PfR\n\nGateway of last resort is not set\n\n      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks\nC        192.168.1.0/24 is directly connected, GigabitEthernet0/0\nL        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                        ],
                        "stdout_lines": [
                            [
                                "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP",
                                "       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area ",
                                "       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2",
                                "       E1 - OSPF external type 1, E2 - OSPF external type 2",
                                "       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2",
                                "       ia - IS-IS inter area, * - candidate default, U - per-user static route",
                                "       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP",
                                "       a - application route",
                                "       + - replicated route, % - next hop override, p - overrides from PfR",
                                "",
                                "Gateway of last resort is not set",
                                "",
                                "      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks",
                                "C        192.168.1.0/24 is directly connected, GigabitEthernet0/0",
                                "L        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                            ]
                        ],
                        "invocation": {
                            "module_args": {
                                "commands": [
                                    "show ip route"
                                ],
                                "match": "all",
                                "retries": 10,
                                "interval": 1,
                                "wait_for": null,
                                "provider": null
                            }
                        },
                        "ansible_facts": {
                            "discovered_interpreter_python": "/usr/libexec/platform-python"
                        },
                        "_ansible_no_log": false
                    },
                    "start": "2022-03-12T10:48:19.084551",
                    "end": "2022-03-12T10:48:21.031434",
                    "duration": 1.946883,
                    "event_loop": null
                },
                {
                    "playbook": "/home/admin/stumble/ios_show.yml",
                    "playbook_uuid": "6b70c8f8-4e21-4288-8fe0-53bc29178723",
                    "play": "ios",
                    "play_uuid": "a29dee92-8fae-871d-4141-000000000007",
                    "play_pattern": "ios",
                    "task": "debug show commands",
                    "task_uuid": "a29dee92-8fae-871d-4141-00000000000a",
                    "task_action": "ansible.builtin.debug",
                    "task_args": "",
                    "task_path": "/home/admin/stumble/ios_show.yml:12",
                    "host": "ios01",
                    "uuid": "4ee85d46-5d0b-4a99-a806-4be244eb1fed",
                    "__host": "ios01",
                    "__result": "OK",
                    "__changed": false,
                    "__duration": "0s",
                    "__number": 1,
                    "__task": "debug show commands",
                    "__task_action": "ansible.builtin.debug",
                    "remote_addr": "ios01",
                    "res": {
                        "msg": [
                            "Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP",
                            "       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area ",
                            "       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2",
                            "       E1 - OSPF external type 1, E2 - OSPF external type 2",
                            "       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2",
                            "       ia - IS-IS inter area, * - candidate default, U - per-user static route",
                            "       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP",
                            "       a - application route",
                            "       + - replicated route, % - next hop override, p - overrides from PfR",
                            "",
                            "Gateway of last resort is not set",
                            "",
                            "      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks",
                            "C        192.168.1.0/24 is directly connected, GigabitEthernet0/0",
                            "L        192.168.1.11/32 is directly connected, GigabitEthernet0/0"
                        ],
                        "_ansible_verbose_always": true,
                        "_ansible_no_log": false,
                        "changed": false
                    },
                    "start": "2022-03-12T10:48:21.039585",
                    "end": "2022-03-12T10:48:21.544976",
                    "duration": 0.505391,
                    "event_loop": null
                }
            ]
        }
    ],
    "stdout": [
        "",
        "PLAY [ios] *********************************************************************",
        "",
        "TASK [exec show commands] ******************************************************",
        "\u001b[0;32mok: [ios01]\u001b[0m",
        "",
        "TASK [debug show commands] *****************************************************",
        "\u001b[0;32mok: [ios01] => {\u001b[0m",
        "\u001b[0;32m    \"msg\": [\u001b[0m",
        "\u001b[0;32m        \"Codes: L - local, C - connected, S - static, R - RIP, M - mobile, B - BGP\",\u001b[0m",
        "\u001b[0;32m        \"       D - EIGRP, EX - EIGRP external, O - OSPF, IA - OSPF inter area \",\u001b[0m",
        "\u001b[0;32m        \"       N1 - OSPF NSSA external type 1, N2 - OSPF NSSA external type 2\",\u001b[0m",
        "\u001b[0;32m        \"       E1 - OSPF external type 1, E2 - OSPF external type 2\",\u001b[0m",
        "\u001b[0;32m        \"       i - IS-IS, su - IS-IS summary, L1 - IS-IS level-1, L2 - IS-IS level-2\",\u001b[0m",
        "\u001b[0;32m        \"       ia - IS-IS inter area, * - candidate default, U - per-user static route\",\u001b[0m",
        "\u001b[0;32m        \"       o - ODR, P - periodic downloaded static route, H - NHRP, l - LISP\",\u001b[0m",
        "\u001b[0;32m        \"       a - application route\",\u001b[0m",
        "\u001b[0;32m        \"       + - replicated route, % - next hop override, p - overrides from PfR\",\u001b[0m",
        "\u001b[0;32m        \"\",\u001b[0m",
        "\u001b[0;32m        \"Gateway of last resort is not set\",\u001b[0m",
        "\u001b[0;32m        \"\",\u001b[0m",
        "\u001b[0;32m        \"      192.168.1.0/24 is variably subnetted, 2 subnets, 2 masks\",\u001b[0m",
        "\u001b[0;32m        \"C        192.168.1.0/24 is directly connected, GigabitEthernet0/0\",\u001b[0m",
        "\u001b[0;32m        \"L        192.168.1.11/32 is directly connected, GigabitEthernet0/0\"\u001b[0m",
        "\u001b[0;32m    ]\u001b[0m",
        "\u001b[0;32m}\u001b[0m",
        "",
        "PLAY RECAP *********************************************************************",
        "\u001b[0;32mios01\u001b[0m                      : \u001b[0;32mok=2   \u001b[0m changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   "
    ],
    "status": "successful",
    "status_color": 10
}

リプレイ機能で再現表示

そのまま表示するのも良いですが、リプレイ機能が便利です。

ansible-navigator replay アーティファクトファイル名

このように指定して実行すると、ansible-navigator run 実行を再現したような表示になります。Ansible Tower / Automation Controller でいうと過去のジョブ実行結果を後から表示するイメージです。

ansible-navigator runinteractive モードで実行したときのアーティファクトstdout モードで表示することもできますし、その逆もできます。


設定は ansible-navigator.yml

ansible-navigator の挙動を指定する設定ファイルは ansible-navigator.yml というファイルです。

厳密には、YAML でも JSON でも構いません。YAML の場合は、拡張子 は .yml または .yaml です。

  • 環境変数 ANSIBLE_NAVIGATOR_CONFIG で指定したパスのファイル
  • カレントディレクトリの ansible-navigator.yml
  • ~/.ansible-navigator.yml

ansible.cfg と似てますね。

この設定ファイルでは、利用するイメージの指定やモード、アーティファクトファイルの保存パスの指定などができます。設定項目の一覧は、公式ドキュメントに掲載されています。

ansible-navigator.readthedocs.io

なお、配信中にいじっていた設定ファイルは以下のようなものです。

---
ansible-navigator:

  # ansible:
  #   inventories:
  #     - inventory

  execution-environment:
    container-engine: podman
    enabled: true
    # image: quay.io/ansible/creator-ee:v0.2.0
    image: quay.io/ansible/ansible-runner:stable-2.12-latest
    # image: ghcr.io/akira6592/my-ee:latest
    pull-policy: missing

  # mode: stdout

  playbook-artifact:
    enable: True
    save-as: "artifacts/{playbook_name}-{ts_utc}.json"

Ansibleの実行環境コンテナの利用

利用イメージの定義

利用するイメージは、ansible-navigator.yml では以下のように image で指定します。

---
ansible-navigator:
  execution-environment:
    container-engine: podman
    enabled: true
    image: quay.io/ansible/ansible-runner:stable-2.12-latest

たとえば、このイメージに入っていないコレクションのモジュールを使おうとするとエラーになります。

イメージは、できあいの物を利用するか ansible-builder というツールを使って作成したものを利用します。

コンテナを使わなくてもOK

コンテナの実行環境を使わないで、ansible-navigator を実行している自身の環境の Ansible を利用できます。

---
ansible-navigator:
  execution-environment:
    enabled: false    # ここで無効を指定

コマンドのオプションで指定する場合は --ee false です。

Playbook 実行以外にも

ansible-navigator のサブコマンドを run 以外に切り替えることによって、ドキュメントの参照やコンフィグの参照などする機能もあります。

ansible-navigator のサブコマンドと、既存の ansible-* コマンドとの対応を以下のページにまとめられています。

ansible-navigator.readthedocs.io

ドキュメントの参照

ansible-doc コマンドに相当します。

モジュールを指定する場合、コマンド書式は以下のとおりです。

$ ansible-navigator doc モジュール名

おわりに

簡単ですが、ansible-navigator の紹介をしました。

コンテナによる実行環境が主流になってくると、コマンドとしては、ansible-navigator を使う機会も増えてくるかも知れません。

stdout モードが使えるのは気が利いているなと思います。

アーティファクトの保存とリプレイ機能も便利だと感じました。

参考


Part37にむけて

以下のネタを検討中です。気が向いたものをやります。 connpass申込時のアンケートでいただいたものも含めています。

  • ansible-builder
  • connection: local ななにか
  • Windows
  • cli_parse モジュール(Part15 の続き)
  • モジュールのテスト
  • Tower / AWX
  • role と Playbook のリポジトリ分割と読み込み
  • AWXとの共存を念頭に入れたDirectory構成

[Ansible] よく見る公式ドキュメントのページ5選

はじめに

チーム内で、Ansible の公式ドキュメントの見方についての話になったときに、よく参照するドキュメントはどこかという話に発展しました。

5つ紹介します。対象プラットフォームに限定しないものを取り上げています。

1. 各モジュールの説明ページ

おそらく多くの方も同じだと思います。

例えば ansible.builtin.debug モジュールであれば以下のページです。

docs.ansible.com

何をするモジュールか、どんなオプションがあってどんな値を指定できるか、使用例などが掲載されています。

私は初見のモジュールの場合は、まず例(Examples)を見て雰囲気を掴んでから、各オプションの説明を読む、という流れが多いです。

2. Using filters to manipulate data

フィルターの説明ページです。

docs.ansible.com

使い慣れているフィルターならいいのですが、たまに使うフィルターだと使い方を覚えていないのでここを起点に調べています。

あとは適当に眺めてると「こんなのがあったのか!」という発見もあります。

必要に応じてJinja2のドキュメントも参照します。

3. Ansible Configuration Settings

ansible.cfg環境変数などで設定できる設定項目の一覧です。

docs.ansible.com

例えば INTERPRETER_PYTHON であれば以下の箇所です。

docs.ansible.com

ini というのが ansible.cfg だと高設定するよという意味です。INTERPRETER_PYTHON であれば、section[defaults]なので、以下のように指定します。

[defaults]
interpreter_python=ここに設定値を指定

ansible.cfg で指定する場合、環境変数で指定する場合、変数で指定する場合、それぞれ名前が異なるので注意です。

これもやっぱり、眺めてると新しい発見があります。

4. Playbook Keywords

PlayやTask などの単位で、どういうキーワードを指定できるかの一覧です。

docs.ansible.com

たとえば、Taskに、become があるのであれば、以下のような指定ができるということが分かります。

# ...(略)...
  tasks:
    - name: yum
      ansible.builtin.yum:
        name: git
      become: true     # タスク単位でも指定可

5. Special Variables

予約変数の一覧です。

docs.ansible.com

ansible_ で始まるもの以外にもいろいろあることが分かります。

groups なんて、つい独自定義で使ってしまいそうですが・・

[Ansible] pip install ansible でバージョン5以降がインストールできないときはpythonバージョンを確認

とある環境で pipansible の 5系をインストールしようと思ったら、エラーになったことがありました。

$ pip install ansible==5.2.0
ERROR: Could not find a version that satisfies the requirement ansible==5.2.0 (from versions: 1.0, 1.1, 1.2, 1.2.1, 1.2.2, 1.2.3, 1.3.0, 1.3.1, 1.3.2, 1.3.3, 1.3.4, 1.4, 1.4.1, 1.4.2, 1.4.3, 1.4.4, 1.4.5, 1.5, 1.5.1, 1.5.2, 1.5.3, 1.5.4, 1.5.5, 1.6, 1.6.1, 1.6.2, 1.6.3, 1.6.4, 1.6.5, 1.6.6, 1.6.7, 1.6.8, 1.6.9, 1.6.10, 1.7, 1.7.1, 1.7.2, 1.8, 1.8.1, 1.8.2, 1.8.3, 1.8.4, 1.9.0.1, 1.9.1, 1.9.2, 1.9.3, 1.9.4, 1.9.5, 1.9.6, 2.0.0.0, 2.0.0.1, 2.0.0.2, 2.0.1.0, 2.0.2.0, 2.1.0.0, 2.1.1.0, 2.1.2.0, 2.1.3.0, 2.1.4.0, 2.1.5.0, 2.1.6.0, 2.2.0.0, 2.2.1.0, 2.2.2.0, 2.2.3.0, 2.3.0.0, 2.3.1.0, 2.3.2.0, 2.3.3.0, 2.4.0.0, 2.4.1.0, 2.4.2.0, 2.4.3.0, 2.4.4.0, 2.4.5.0, 2.4.6.0, 2.5.0a1, 2.5.0b1, 2.5.0b2, 2.5.0rc1, 2.5.0rc2, 2.5.0rc3, 2.5.0, 2.5.1, 2.5.2, 2.5.3, 2.5.4, 2.5.5, 2.5.6, 2.5.7, 2.5.8, 2.5.9, 2.5.10, 2.5.11, 2.5.12, 2.5.13, 2.5.14, 2.5.15, 2.6.0a1, 2.6.0a2, 2.6.0rc1, 2.6.0rc2, 2.6.0rc3, 2.6.0rc4, 2.6.0rc5, 2.6.0, 2.6.1, 2.6.2, 2.6.3, 2.6.4, 2.6.5, 2.6.6, 2.6.7, 2.6.8, 2.6.9, 2.6.10, 2.6.11, 2.6.12, 2.6.13, 2.6.14, 2.6.15, 2.6.16, 2.6.17, 2.6.18, 2.6.19, 2.6.20, 2.7.0.dev0, 2.7.0a1, 2.7.0b1, 2.7.0rc1, 2.7.0rc2, 2.7.0rc3, 2.7.0rc4, 2.7.0, 2.7.1, 2.7.2, 2.7.3, 2.7.4, 2.7.5, 2.7.6, 2.7.7, 2.7.8, 2.7.9, 2.7.10, 2.7.11, 2.7.12, 2.7.13, 2.7.14, 2.7.15, 2.7.16, 2.7.17, 2.7.18, 2.8.0a1, 2.8.0b1, 2.8.0rc1, 2.8.0rc2, 2.8.0rc3, 2.8.0, 2.8.1, 2.8.2, 2.8.3, 2.8.4, 2.8.5, 2.8.6, 2.8.7, 2.8.8, 2.8.9, 2.8.10, 2.8.11, 2.8.12, 2.8.13, 2.8.14, 2.8.15, 2.8.16rc1, 2.8.16, 2.8.17rc1, 2.8.17, 2.8.18rc1, 2.8.18, 2.8.19rc1, 2.8.19, 2.8.20rc1, 2.8.20, 2.9.0b1, 2.9.0rc1, 2.9.0rc2, 2.9.0rc3, 2.9.0rc4, 2.9.0rc5, 2.9.0, 2.9.1, 2.9.2, 2.9.3, 2.9.4, 2.9.5, 2.9.6, 2.9.7, 2.9.8, 2.9.9, 2.9.10, 2.9.11, 2.9.12, 2.9.13, 2.9.14rc1, 2.9.14, 2.9.15rc1, 2.9.15, 2.9.16rc1, 2.9.16, 2.9.17rc1, 2.9.17, 2.9.18rc1, 2.9.18, 2.9.19rc1, 2.9.19, 2.9.20rc1, 2.9.20, 2.9.21rc1, 2.9.21, 2.9.22rc1, 2.9.22, 2.9.23rc1, 2.9.23, 2.9.24rc1, 2.9.24, 2.9.25rc1, 2.9.25, 2.9.26rc1, 2.9.26, 2.9.27rc1, 2.9.27, 2.10.0a1, 2.10.0a2, 2.10.0a3, 2.10.0a4, 2.10.0a5, 2.10.0a6, 2.10.0a7, 2.10.0a8, 2.10.0a9, 2.10.0b1, 2.10.0b2, 2.10.0rc1, 2.10.0, 2.10.1, 2.10.2, 2.10.3, 2.10.4, 2.10.5, 2.10.6, 2.10.7, 3.0.0b1, 3.0.0rc1, 3.0.0, 3.1.0, 3.2.0, 3.3.0, 3.4.0, 4.0.0a1, 4.0.0a2, 4.0.0a3, 4.0.0a4, 4.0.0b1, 4.0.0b2, 4.0.0rc1, 4.0.0, 4.1.0, 4.2.0, 4.3.0, 4.4.0, 4.5.0, 4.6.0, 4.7.0, 4.8.0, 4.9.0, 4.10.0, 5.0.0a1, 5.0.0a2, 5.0.0a3, 5.0.0b1, 5.0.0b2, 5.0.0rc1)
ERROR: No matching distribution found for ansible==5.2.0

もちろんタイミング的にには、5系の正式版がリリースされてるのですが、バージョンの一覧に 5系の正式版が表示されません。

もしかしてと思ったら、Python 3.6 を使っていました。

ansible 5 の内部である ansible-core 2.12 では、コントロールノード(Ansibleをインストールするノード)の要件が Python 3.8 以上になりました。

Installing Ansible — Ansible Core Documentation

For your control node (the machine that runs Ansible), you can use any machine with Python 3.8 or newer installed.

ということで、Python 3.8 以上の環境で pip install ansible==5.2.0 を実行したら無事にインストールできました。

[Terraform] IOS XE Terraform provider で設定変更を試してみた

はじめに

Terraform の Cisco IOS XE 向けの Provider がリリースされたことを、先日知りました。

紹介ブログ https://blogs.cisco.com/developer/terraformiosxe01

内部的には CLI を実行するわけではく、RESTCONF ベースののようです。

Terraform はまだ慣れていないのですが、興味があったのでためしてみました。


■ 1. 共通 .tf ファイルの準備

1.1. terraform.tf の作成

利用するプロバイダーの指定です。Registry 上の iosxe providerUSE PROVIDER をクリックして表示されたものを参考しました。

terraform {
  required_providers {
    iosxe = {
      source  = "CiscoDevNet/iosxe"
      version = "0.1.1"
    }
  }
}

1.2. provider.tf の作成

iosxe プロバイダーの各種設定です。 GitHub リポジトリ上のサンプルを参考しました。

provider "iosxe" {
  host            = "https://ネットワーク機器のアドレス"
  device_username = "dummy_user"
  device_password = "dummy_password"

  insecure        = true
}

各種パラメーターの意味はこちらに記載があります。

接続先、ユーザー名、パスワードは、それぞれ以下の環境変数でも良いようです。

  • HOST_IOSXE
  • DEVICE_PASSWORD_IOSXE
  • DEVICE_USERNAME_IOSXE

insecure は、SSL/TLS 証明書の検証をしない指定です。デフォルトでも true です。

■ 2. NTP サーバー設定0台から3台へ (POST)

ここから、実際に実現したい処理の定義です。今回は、Terraform でネットワーク機器に参照先 NTP サーバーの設定(コマンドでいう ntp server)をしてみます。

ネットワーク機器側は NTP サーバーの設定がない状態から始めます。

csrv1000##sh run | inc ntp
csrv1000#

この状態から 3台分追加する処理を試します。

2.1. ntp_post.tf の作成

Getting Startedによると、aclbgpntpospfvlan などさまざまな設定を扱えるようです。[こちらにサンプルがたくさん]8https://github.com/CiscoDevNet/terraform-provider-iosxe/tree/main/examples/examples_tf)あります。どのサンプルを見ても、リソースは iosxe_rest なので、RESTCONF で設定できるものは設定できると思ってもいいのかもしれません。

今回はお試しということで、シンプルに試せそうという点で ntp にしました。

以下のファイルは、NTP サーバー 10.0.0.110.0.0.210.0.0.3 を追加するものです。

resource "iosxe_rest" "ntp_post" {
  method = "POST"
  path   = "/data/Cisco-IOS-XE-native:native/ntp/server"
  payload = jsonencode(
    {
      "Cisco-IOS-XE-ntp:server-list" : [
        {
          "ip-address" : "10.0.0.1",
        },
        {
          "ip-address" : "10.0.0.2",
        },
        {
          "ip-address" : "10.0.0.3",
        }
      ]
    }
}
パラメーター名 説明
method POSTPUTPATCHDELETE` などのような RESTCONF のメソッドを指定
path 設定するためのエンドポイントを指定
payload 設定刷るための body を指定

ということで、ほとんど RESTCONF で叩くとき同じ感覚ですね。

2.2. terraform init の実行

ここまで以下のファイルを作りました。

ntp_post.tf
provider.tf
terraform.tf

同じディレクトリで、terraform init を実行します

% terraform init 

Initializing the backend...

Initializing provider plugins...
- Finding ciscodevnet/iosxe versions matching "0.1.1"...
- Installing ciscodevnet/iosxe v0.1.1...
- Installed ciscodevnet/iosxe v0.1.1 (self-signed, key ID F8EC53CAB70CD366)
...(略)...

2.3. terraform plan の実行

続いて terraform plan を実行します。3台分追加しますよ、とのことです。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # iosxe_rest.ntp_post will be created
  + resource "iosxe_rest" "ntp_post" {
      + id       = (known after apply)
      + method   = "POST"
      + path     = "/data/Cisco-IOS-XE-native:native/ntp/server"
      + payload  = jsonencode(
            {
              + Cisco-IOS-XE-ntp:server-list = [
                  + {
                      + ip-address = "10.0.0.1"
                    },
                  + {
                      + ip-address = "10.0.0.2"
                    },
                  + {
                      + ip-address = "10.0.0.3"
                    },
                ]
            }
        )
      + response = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

2.4. terraform apply の実行

いよいよ terraform apply です。

% terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # iosxe_rest.ntp_post will be created
  + resource "iosxe_rest" "ntp_post" {
      + id       = (known after apply)
      + method   = "POST"
      + path     = "/data/Cisco-IOS-XE-native:native/ntp/server"
      + payload  = jsonencode(
            {
              + Cisco-IOS-XE-ntp:server-list = [
                  + {
                      + ip-address = "10.0.0.1"
                    },
                  + {
                      + ip-address = "10.0.0.2"
                    },
                  + {
                      + ip-address = "10.0.0.3"
                    },
                ]
            }
        )
      + response = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes    # yes を入力

iosxe_rest.ntp_post: Creating...
iosxe_rest.ntp_post: Creation complete after 1s [id=4155581422]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

ネットワーク機器側を確認すると無事に3台分が追加されていることが確認できました。

csrv1000(config)#do sh run | inc ntp   
ntp server 10.0.0.1
ntp server 10.0.0.2
ntp server 10.0.0.3

2.5. terraform apply の「再」実行

さて、この状態からもう一度 terraform apply を実行するとどうなるのだろうと思いました。

普通に RESTCONF で考えると、すでに設定が入ってる状態に 再度 POST すると 409 Conflict あたりになるかと思います。

ということで試しました。

% terraform plan 
iosxe_rest.ntp_post: Refreshing state... [id=4155581422]

No changes. Your infrastructure matches the configuration.

Terraform has compared your real infrastructure against your configuration and found no differences, so no changes are needed.

No changes. なので、特に何もしない結果になりました。

これは、Terraform 側の処理として手元に保管されている state ファイルと比較した結果、一致しているのでリクエストすら出さなかった、という状態だと思います。このあたりは、Terraform らしさでしょうか。


■ 3. NTP サーバー設定3台から2台 (POSTのまま、エラー)

続いて、エラーになるかなと思いつつも、tf ファイルで4台目を追加して POST のまま実行にする、というのを試しました。

3.1. ntp_post.tf の修正

ntp_post.tf を以下ように、4台目の分を追記します。

resource "iosxe_rest" "ntp_post" {
  method = "POST"
  path   = "/data/Cisco-IOS-XE-native:native/ntp/server"
  payload = jsonencode(
    {
      "Cisco-IOS-XE-ntp:server-list" : [
        {
          "ip-address" : "10.0.0.1"
        },
        {
          "ip-address" : "10.0.0.2"
        },
        {
          "ip-address" : "10.0.0.3"
        },
        {
          "ip-address" : "10.0.0.4"     # 4台目追加
        }
      ]
    }
  )
}

3.2. terraform plan の実行

terraform plan を実行します。

これだけ見ると 4台目だけいい感じに追加されるようにみえますが・・・。

% terraform plan
iosxe_rest.ntp_post: Refreshing state... [id=4155581422]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_post will be updated in-place
  ~ resource "iosxe_rest" "ntp_post" {
        id      = "4155581422"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-ntp:server-list = [
                    # (2 unchanged elements hidden)
                    {
                        ip-address = "10.0.0.3"
                    },
                  + {
                      + ip-address = "10.0.0.4"
                    },
                ]
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

3.3.terraform apply の実行

実際に terraform apply を実行するとエラーになります。

% terraform apply
iosxe_rest.ntp_post: Refreshing state... [id=4155581422]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_post will be updated in-place
  ~ resource "iosxe_rest" "ntp_post" {
        id      = "4155581422"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-ntp:server-list = [
                    # (2 unchanged elements hidden)
                    {
                        ip-address = "10.0.0.3"
                    },
                  + {
                      + ip-address = "10.0.0.4"
                    },
                ]
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

iosxe_rest.ntp_post: Modifying... [id=4155581422]
╷
│ Error: failed: status code: 409 - error: {
│   "errors": {
│     "error": [
│       {
│         "error-message": "object already exists: /ios:native/ios:ntp/ios-ntp:server/ios-ntp:server-list[ios-ntp:ip-address='10.0.0.1']",
│         "error-path": "/Cisco-IOS-XE-native:native/ntp/Cisco-IOS-XE-ntp:server",
│         "error-tag": "data-exists",
│         "error-type": "application"
│       }
│     ]
│   }
│ }
│ 
│ 
│   with iosxe_rest.ntp_post,
│   on ntp_post.tf line 1, in resource "iosxe_rest" "ntp_post":
│    1: resource "iosxe_rest" "ntp_post" {
│ 
╵

Terraform 的に、差分ありと判断してリクエストを出した結果、すでにある設定を POST してるので、エラーになったということだと思います。ステータスコード409、また "error-tag": "data-exists" とあります。

3.4. 仕切り直し

さて、こうなると少々厄介です。 state ファイルには 4台目がある状態で、ネットワーク機器側3台分のままです。

今回は挙動をいろいろ確認するためなので、だいぶ乱暴ですがいったん state ファイル類を削除します(通常は推奨されることではないと思います)。

rm -fr .terraform .terraform.lock.hcl terraform.tfstate terraform.tfstate.backup

ネットワーク機器側も NTP サーバーの設定を削除します。

csrv1000(config)#no ntp server 10.0.0.1
csrv1000(config)#no ntp server 10.0.0.2
csrv1000(config)#no ntp server 10.0.0.3
csrv1000(config)#do sh run | inc ntp   
csrv1000(config)#


■ 4. NTP サーバー設定0台から3台へ (PUT)

Terraform 側もネットワーク機器側も状態をもとに戻したところで、仕切り直しです。

4.1. ntp_put.tf の作成

ntp_post.tf は削除して、別途 ntp_put.tf を作成します。ntp_post.tf と比較すると、method"POST" にしている他、pathpayload も微妙に変更しています。

resource "iosxe_rest" "ntp_put" {
  method = "PUT"
  path   = "/data/Cisco-IOS-XE-native:native/ntp"
  payload = jsonencode(
    {
      "Cisco-IOS-XE-native:ntp" : {
        "Cisco-IOS-XE-ntp:server" : {
          "server-list" : [
            {
              "ip-address" : "10.0.0.1"
            },
            {
              "ip-address" : "10.0.0.2"
            },
            {
              "ip-address" : "10.0.0.3"
            }
          ]
        }
      }
    }
  )
}

4.2. terraform init の実行

先程、自動生成されたファイルを削除したので、terraform init を実行します。

4.3. terraform plan の実行

PUT 版で terraform plan を実行します。

3台分追加するよ、と示される点は POST のときとだいたい同じように見えます。

% terraform plan

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be created
  + resource "iosxe_rest" "ntp_put" {
      + id       = (known after apply)
      + method   = "PUT"
      + path     = "/data/Cisco-IOS-XE-native:native/ntp"
      + payload  = jsonencode(
            {
              + Cisco-IOS-XE-native:ntp = {
                  + Cisco-IOS-XE-ntp:server = {
                      + server-list = [
                          + {
                              + ip-address = "10.0.0.1"
                            },
                          + {
                              + ip-address = "10.0.0.2"
                            },
                          + {
                              + ip-address = "10.0.0.3"
                            },
                        ]
                    }
                }
            }
        )
      + response = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

4.4 terraform apply の実行

terraform apply を実行します。

% terraform apply

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  + create

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be created
  + resource "iosxe_rest" "ntp_put" {
      + id       = (known after apply)
      + method   = "PUT"
      + path     = "/data/Cisco-IOS-XE-native:native/ntp"
      + payload  = jsonencode(
            {
              + Cisco-IOS-XE-native:ntp = {
                  + Cisco-IOS-XE-ntp:server = {
                      + server-list = [
                          + {
                              + ip-address = "10.0.0.1"
                            },
                          + {
                              + ip-address = "10.0.0.2"
                            },
                          + {
                              + ip-address = "10.0.0.3"
                            },
                        ]
                    }
                }
            }
        )
      + response = (known after apply)
    }

Plan: 1 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

iosxe_rest.ntp_put: Creating...
iosxe_rest.ntp_put: Creation complete after 1s [id=4117814902]

Apply complete! Resources: 1 added, 0 changed, 0 destroyed.

ネットワーク機器側の設定を確認すると無事に設定が入っていました。

csrv1000(config)#do sh run | inc ntp
ntp server 10.0.0.1
ntp server 10.0.0.2
ntp server 10.0.0.3


■ 5. NTP サーバー設定3台から4台へ (PUT)

3台分設定されている状態から、PUTで4台分にします。

5.1. ntp_put.tf の修正

PUT で 3台追加したところで、今度は先程使った ntp_put.tf を編集して、4台の設定にします。

resource "iosxe_rest" "ntp_put" {
  method = "PUT"
  path   = "/data/Cisco-IOS-XE-native:native/ntp"
  payload = jsonencode(
    {
      "Cisco-IOS-XE-native:ntp" : {
        "Cisco-IOS-XE-ntp:server" : {
          "server-list" : [
            {
              "ip-address" : "10.0.0.1"
            },
            {
              "ip-address" : "10.0.0.2"
            },
            {
              "ip-address" : "10.0.0.3"
            },
            {
              "ip-address" : "10.0.0.4"    # 4台目
            }
          ]
        }
      }
    }
  )
}

5.2. terraform plan の実行

terraform plan を実行します。4台目 10.0.0.4 を追加しますよ、と表示されます。

% terraform plan 
iosxe_rest.ntp_put: Refreshing state... [id=4117814902]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be updated in-place
  ~ resource "iosxe_rest" "ntp_put" {
        id      = "4117814902"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-native:ntp = {
                  ~ Cisco-IOS-XE-ntp:server = {
                      ~ server-list = [
                            # (2 unchanged elements hidden)
                            {
                                ip-address = "10.0.0.3"
                            },
                          + {
                              + ip-address = "10.0.0.4"
                            },
                        ]
                    }
                }
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

5.3. terraform apply の実行

terraform apply を実行します。

% terraform apply
iosxe_rest.ntp_put: Refreshing state... [id=4117814902]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be updated in-place
  ~ resource "iosxe_rest" "ntp_put" {
        id      = "4117814902"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-native:ntp = {
                  ~ Cisco-IOS-XE-ntp:server = {
                      ~ server-list = [
                            # (2 unchanged elements hidden)
                            {
                                ip-address = "10.0.0.3"
                            },
                          + {
                              + ip-address = "10.0.0.4"
                            },
                        ]
                    }
                }
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

iosxe_rest.ntp_put: Modifying... [id=4117814902]
iosxe_rest.ntp_put: Modifications complete after 2s [id=477754463]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

無事に設定されました。

csrv1000(config)#do sh run | inc ntp
ntp server 10.0.0.1
ntp server 10.0.0.2
ntp server 10.0.0.3
ntp server 10.0.0.4


■ 6. NTP サーバー設定4台から1台へ (PUT)

4台分設定されている状態から、PUTで1台分にします。

6.1. ntp_put.tf の修正

ntp_put.tf を以下のように、1台分(10.0.0.1)だけになるように修正します。method"PUT" のままです。

resource "iosxe_rest" "ntp_put" {
  method = "PUT"
  path   = "/data/Cisco-IOS-XE-native:native/ntp"
  payload = jsonencode(
    {
      "Cisco-IOS-XE-native:ntp" : {
        "Cisco-IOS-XE-ntp:server" : {
          "server-list" : [
            {
              "ip-address" : "10.0.0.1"
            }
          ]
        }
      }
    }
  )
}

他の3台を削除する、といった指定はせず、とにかく指定した1台分(10.0.0.1)になってほしい、意味合いの指定です。

6.2. terraform plan の実行

terraform plan を実行します。10.0.0.210.0.0.310.0.0.4 は削除しますよと示されます。

% terraform plan
iosxe_rest.ntp_put: Refreshing state... [id=477754463]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be updated in-place
  ~ resource "iosxe_rest" "ntp_put" {
        id      = "477754463"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-native:ntp = {
                  ~ Cisco-IOS-XE-ntp:server = {
                      ~ server-list = [
                            {
                                ip-address = "10.0.0.1"
                            },
                          - {
                              - ip-address = "10.0.0.2"
                            },
                          - {
                              - ip-address = "10.0.0.3"
                            },
                          - {
                              - ip-address = "10.0.0.4"
                            },
                        ]
                    }
                }
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

6.3. terraform apply の実行

terraform apply を実行します。

% terraform apply
iosxe_rest.ntp_put: Refreshing state... [id=477754463]

Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the
following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # iosxe_rest.ntp_put will be updated in-place
  ~ resource "iosxe_rest" "ntp_put" {
        id      = "477754463"
      ~ payload = jsonencode(
          ~ {
              ~ Cisco-IOS-XE-native:ntp = {
                  ~ Cisco-IOS-XE-ntp:server = {
                      ~ server-list = [
                            {
                                ip-address = "10.0.0.1"
                            },
                          - {
                              - ip-address = "10.0.0.2"
                            },
                          - {
                              - ip-address = "10.0.0.3"
                            },
                          - {
                              - ip-address = "10.0.0.4"
                            },
                        ]
                    }
                }
            }
        )
        # (2 unchanged attributes hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value: yes

iosxe_rest.ntp_put: Modifying... [id=477754463]
iosxe_rest.ntp_put: Modifications complete after 1s [id=1111786887]

Apply complete! Resources: 0 added, 1 changed, 0 destroyed.

無事に 10.0.0.1 のみになりました。

csrv1000(config)#do sh run | inc ntp
ntp server 10.0.0.1
csrv1000(config)#

method としては、他にも PATCHDELETE もしてできますが、今回はこの辺にしたいと思います。




まとめ・所感

Terraform の IOS XE Provider を利用して、IOS XE の機器にのNTPサーバー設定を試してみました。

以下、まとめと所感です。

  • RESTCONF ベースなのでCLIベースの自動化の悩み(noコマンドの生成)はなさそう
    • その代わり RESTCONF の知識が必要
  • 手続き型に考えが染まっているので、宣言型の良さをまだ活かせる自身がない
    • リソースをどう扱えばよいか悩みそう
    • method をどうすればよいか悩みそう。PUT が一番相性がいいように思う
    • 本格的に使う上では考慮事項がそれなりにありそう
  • 同じ内容のPOST を2回 実行してもエラーにならなかったのは Terraform らしさを感じた
  • Terraform か RESTCONF のどちらかに慣れていないと、エラーが発生したときにどちらの問題か判断しにくい
  • Terraform なのでWindowsからも実行できるはず(今回はmacOSから)

参考資料

紹介ブログ https://blogs.cisco.com/developer/terraformiosxe01

GitHub リポジトリ https://github.com/CiscoDevNet/terraform-provider-iosxe

tf ファイルのサンプル https://github.com/CiscoDevNet/terraform-provider-iosxe/tree/main/examples/examples_tf

動画 Cisco IOS XE Terraform provider introduction and demo https://www.youtube.com/watch?v=oB_QZ2mDiW0

上記動画の資料 https://github.com/CiscoDevNet/terraform-provider-iosxe/blob/main/docs/resources/intro_to_terraform_video.pdf

Terraform Registry https://registry.terraform.io/providers/CiscoDevNet/iosxe/latest

はてなブログで目次を生成する見出しレベルをCSSで調整する

目次機能とは

はてなブログには目次を簡単に生成できる目次機能があります。

[:contents]

と差し込むだけです。

f:id:akira6592:20220306110301p:plain
ボタンもある

目次の対象にされるのは markdown でいう # 見出し による見出しです。## 見出し`### 見出し のような下のレベルまで目次の対象になります。

レベルが深いとノイジーな目次になってしまいます。Word ではどのレベルまでの目次を表示するか設定できるので、同じことをしたいなと思っていました。

ためしたら、デザインCSSで、少しスタイルを追加するだけでできました。(ただし仕様上、スマホ表示には非対応)

調整方法

レベル3まである目次を生成すると以下のように <ur> <li> によるリストになってました。(少し整形しています)

    <!-- 略 -->
    <div class="entry-content">
      <ul class="table-of-contents">
        <li><a href="#はじめに">はじめに</a></li>
        <li><a href="#動画">動画</a></li>
        <li><a href="#ロールと変数とチェック">ロールと変数とチェック</a></li>
        <li><a href="#Role-argument-validation">Role argument validation</a>
          <ul>
            <li><a href="#必須チェック">必須チェック</a>
              <ul>
                <li><a href="#エラーになるケース">エラーになるケース</a></li>
                <li><a href="#正常なケース">正常なケース</a></li>
              </ul>
            </li>
            <li><a href="#チェックの無効化">チェックの無効化</a></li>
            <li><a href="#型チェック">型チェック</a></li>
            <li><a href="#選択肢チェック">選択肢チェック</a></li>
          </ul>
        </li>
    <!-- 略 -->

なのでトップの <ul>table-of-contents までクラス名で追いつつ、非表示にしたいレベルの <ul>display: none を指定します。

レベル1までのみ表示

.entry-content .table-of-contents li ul {
    display: none;
}

f:id:akira6592:20220306112112p:plain
レベル1まで表示

レベル2までのみ表示

.entry-content .table-of-contents li ul li ul {
    display: none;
}

f:id:akira6592:20220306112145p:plain
レベル2まで表示

レベル3以降は、さらにセレクタの末尾に li ul を追加していくだけです。

[Ansible] 「つまずき Ansible 【Part35】ロールの実行に必要な変数をチェック」ふりかえり

はじめに

2022/03/05 に、YouTube Live で「つまずき Ansible 【Part35】ロールの実行に必要な変数をチェック」という配信をしました。

tekunabe.connpass.com

今回は、ロールの実行に必要な変数のバリデーションができる「Role argument validation」を試します。

ansible-core 2.11 から利用できる機能です。


動画

youtu.be

ロールと変数とチェック

ロール内で変数を利用する場合は、あらじめ必要な変数が定義されているかなどをチェックしたいところです。

古くから使える方法としては、assert モジュールがあり、柔軟なチェックができます。

一方 ansible-core 2.11 からは Role argument validation という機能が利用できます(正確な機能名はわからないですが、ドキュメントの見出しからとっています)。 assert モジュールのよいな汎用的な機能ではなく、ロールの argument (内部で利用する変数と解釈しています)専用のチェック機能です。

Role argument validation

Role argument validation では、チェックするルールをロール内の meta/argument_specs.yml 内に定義します。

必須チェック

エラーになるケース

必要な変数だと定義しているのに、定義してない場合です。

meta/argument_specs.yml を以下のようにします。myapp_intmyapp_strrequired: true で必須としています。

---
argument_specs:
  main:
    short_description: The main entry point for the myapp role
    options:
      myapp_int:
        type: "int"
        required: true
        description: "The integer value"

      myapp_str:
        type: "str"
        required: true
        description: "The string value"

以下の Playbook を実行することにします。myapp_intmyapp_str も未定義です。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: import test
      ansible.builtin.import_role:
        name: test

ロールの処理の中身も変数を利用するだけです。

---
- name: debug test 1
  debug:
    msg: "{{ myapp_int }}"

- name: debug test 2
  debug:
    msg: "{{ myapp_str }}"

この状態で、Playbook を実行します。(エラーを改行表示するために -vvv を付けてます。)

$ ansible-playbook -i localhost, site.yml -vvv 
...(略)...

PLAYBOOK: site.yml ******************************************************************************************************************************************************************************
1 plays in site.yml

PLAY [localhost] ********************************************************************************************************************************************************************************
META: ran handlers

TASK [test : Validating arguments against arg spec 'main' - The main entry point for the myapp role] ********************************************************************************************
task path: /home/admin/ansible-ee/site.yml:2
fatal: [localhost]: FAILED! => {
    "argument_errors": [
        "missing required arguments: myapp_int, myapp_str"   # ここがポイント
    ],
    "argument_spec_data": {
        "myapp_int": {
            "description": "The integer value",
            "required": true,
            "type": "int"
        },
        "myapp_str": {
            "description": "The string value",
            "required": true,
            "type": "str"
        }
    },
    "changed": false,
    "msg": "Validation of arguments failed:\nmissing required arguments: myapp_int, myapp_str",
    "validate_args_context": {
        "argument_spec_name": "main",
        "name": "test",
        "path": "/home/admin/ansible-ee/roles/test",
        "type": "role"
    }
}

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

ポイントは "missing required arguments: myapp_int, myapp_str" です。2つの変数の分がいっぺんに表示してくれるのが嬉しいです。

validate が1つのタスクとてログに表示される点も特徴かなと思います。

正常なケース

今度は正常なケースです。myapp_intmyapp_str をきちんと定義します。

どこで定義されててもいいのですが、今回は import_role モジュールのタスクの vars で定義します。

---
- hosts: localhost
  gather_facts: false

  tasks:
    - name: import test
      ansible.builtin.import_role:
        name: test
      vars:
        myapp_int: 123
        myapp_str: hello

これで実行すると成功します。validate のタスクの状態も ok です。

TASK [test : Validating arguments against arg spec 'main' - The main entry point for the myapp role] ********************************************************************************************
task path: /home/admin/ansible-ee/site.yml:2
ok: [localhost] => {
    "changed": false,
    "msg": "The arg spec validation passed",    # 成功
    "validate_args_context": {
        "argument_spec_name": "main",
        "name": "test",
        "path": "/home/admin/ansible-ee/roles/test",
        "type": "role"
    }
}

TASK [test : debug test 1] **********************************************************************************************************************************************************************
task path: /home/admin/ansible-ee/roles/test/tasks/main.yml:2
ok: [localhost] => {
    "msg": 123
}

TASK [test : debug test 2] **********************************************************************************************************************************************************************
task path: /home/admin/ansible-ee/roles/test/tasks/main.yml:6
ok: [localhost] => {
    "msg": "hello"
}

チェックの無効化

ロールに meta/argument_specs.yml があると validate 処理がされますが、無効化もできます。

import_role モジュールで呼び出す場合は rolespec_validate オプションfalse を指定します。デフォルトは true です。

# ...(略)...
    - name: import test
      ansible.builtin.import_role:
        name: test
        rolespec_validate: false

この場合は、validate 処理はされず、ログにも出てきません。

型チェック

次は型チェックです。meta/argument_specs.yml 内では、 myapp_inttype: "int" という指定をしています。

なので、意図的に 文字に指定してエラーになるようにしてみます。

# ...(略)...
    - name: import test
      ansible.builtin.import_role:
        name: test
      vars:
        myapp_int: abc    # 文字列
        myapp_str: hello

こんなエラーになりました。

fatal: [localhost]: FAILED! => {
    "argument_errors": [
        "argument 'myapp_int' is of type <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> and we were unable to convert to int: <class 'ansible.parsing.yaml.objects.AnsibleUnicode'> cannot be converted to an int"
    ],

unable to convert to int とあるので、変換しようとしてエラー、ということのようです。 なお、myapp_int: "123" という指定はエラーになりませんでした。

選択肢チェック

AかBであること、のような選択肢のチェックです。meta/argument_specs.yml に以下を追加します。

# ...(略)...
      kind:
        type: "str"
        choices:
          - kingyo
          - ugui
        required: true
        description: "kind of sakana"

この場合は、kind の値は kingyougui のいずれかであること、という指定です。

Playbook 側の vars では、意図的にエラーになるように、kind: buri を指定します。

# ...(略)...
    - name: import test
      ansible.builtin.import_role:
        name: test
      vars:
        myapp_int: 123
        myapp_str: hello
        kind: buri

こんなエラーになりました。"value of kind must be one of: kingyo, ugui, got: buri" と表示されています。

...(略)...
TASK [test : Validating arguments against arg spec 'main' - The main entry point for the myapp role] ***
task path: /home/admin/ansible-ee/site.yml:2
fatal: [localhost]: FAILED! => {
    "argument_errors": [
        "value of kind must be one of: kingyo, ugui, got: buri"  # ポイント
    ],
    "argument_spec_data": {
        "kind": {
            "choices": [
                "kingyo",
                "ugui"
            ],
            "description": "kind of sakana",
            "required": true,
            "type": "str"
        },
        "myapp_int": {
            "description": "The integer value",
            "required": true,
            "type": "int"
        },
        "myapp_str": {
            "description": "The string value",
            "required": true,
            "type": "str"
        }
    },
    "changed": false,
    "msg": "Validation of arguments failed:\nvalue of kind must be one of: kingyo, ugui, got: buri",
    "validate_args_context": {
        "argument_spec_name": "main",
        "name": "test",
        "path": "/home/admin/ansible-ee/roles/test",
        "type": "role"
    }
}

validate_argument_spec モジュール

Role argument validation は、ロール読み込み時に meta/argument_specs.yml があるとチェックするという動作でした。

似たようなものとして、ansible.builtin.validate_argument_specモジュールがあります。 これは、任意のタイミングで、 meta/argument_specs.yml のような定義(厳密には meta/argument_specs.ymloptions 配下のみ) を利用してチェックするモジュールです。

タイミングが任意の代わりに、ファイルを明示的に指定する必要があります。

[2024/01/13 追記] 別の記事で書きました。

tekunabe.hatenablog.jp

チェックモードでもvalidateされる

Role argument validation は ansible-playbook コマンドの -C または --check によるチェックモードによる実行でもvalidateしてくれます。

ansible-docによるargument情報の表示

meta/argument_specs.yml に定義した情報は、ansible-doc コマンドで表示できあます。

ロール一覧

ロール一覧の表示では、short_description に定義した情報が表示されまます。

$ ansible-doc -t role -r roles -l
...(略)...
test                                                main         The main entry point for the myapp role!!!!

ロール個別表示

ロールと指定すると書く argument のタイプ、説明なども表示されます。

$ ansible-doc -t role -r roles test
> TEST    (/home/admin/ansible-ee/roles/test)

ENTRY POINT: main - The main entry point for the myapp role

OPTIONS (= is mandatory):

= kind
        kind of sakana
        (Choices: kingyo, ugui)
        type: str

= myapp_int
        The integer value

        type: int

= myapp_str
        The string value

        type: str

まとめ

ロールの実行に必要な変数をチェックする Role argument validation 機能をためしてみました。

柔軟さでは assert や ansible.utils.validate`モジュールのほうが勝ると思いますが、meta/argument_specs.ymlドキュメンテーションも兼ねる点は便利かなと思います。


Part36にむけて

以下のネタを検討中です。気が向いたものをやります。 connpass申込時のアンケートでいただいたものも含めています。

  • ansible-builder、ansible-runner、ansible-navigator
  • connection: local ななにか
  • Windows
  • cli_parse モジュール(Part15 の続き)
  • モジュールのテスト
  • Tower / AWX
  • role と Playbook のリポジトリ分割と読み込み
  • AWXとの共存を念頭に入れたDirectory構成

[Ansible] 安全側に倒す ansible.cfg の設定

はじめに

意図しないまま実行を進めないようにするため、ちょっとしたことでもエラーに倒して早めに処理を止める設定の紹介です。インベントリ周りが中心です。

  • ansible.cfg
[defaults]
duplicate_dict_key=error

[inventory]
host_pattern_mismatch=error
any_unparsed_is_failed=True

要件などに合わせて調整は必要かと思いますが、このあたりからベースにすると良いのではないかと思っています。

インベント周りはデフォルトでは結構ゆるい印象があります。

以下詳細(ansible 2.9.23 で確認)です。

YAMLのキーの重複時の挙動の指定(DUPLICATE_YAML_DICT_KEY

DUPLICATE_YAML_DICT_KEY

YAMLのキーの重複時の挙動を指定する。デフォルトは warn で、警告を表示して処理を継続する。

例えば以下の Playbook では、モジュール名である debug が重複している。

---
- hosts: ios
  gather_facts: false

  tasks:
    - name:
      debug:
        msg: msg1
      debug:
        msg: msg2

これを実行すると、警告が表示されるが処理を続ける。後に定義したキーが優先される。

$ ansible-playbook -i inventory.ini test.yml 
[WARNING]: While constructing a mapping from /home/ansible/test.yml, line 10, column 7, found a duplicate dict key (debug). Using last defined value only.

PLAY [ios] ********************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [ios01] => {
    "msg": "msg2"
}

PLAY RECAP ********************************************************************************************************
ios01                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

$ echo $?
0

キーの重複時にエラーとしたい場合は error を指定する。

[defaults]
duplicate_dict_key=error

この設定で Playbook を実行すると以下のようにエラーとなる。エラーメッセージは分かりにくい。

$ ansible-playbook -i inventory.ini test.yml 
ERROR! Unexpected Exception, this is probably a bug: 'NoneType' object has no attribute 'line'
to see the full traceback, use -vvv
$ echo $?
250

なお、モジュール指定の重複だけでなく、オプションや変数名の重複(同一YAML内)でもエラーになる。

hosts の指定が誤っている場合の挙動の指定(HOST_PATTERN_MISMATCH

HOST_PATTERN_MISMATCH

Playbook の hosts: の指定したホストやグループ名が、インベントリ内に見つからない場合の挙動。デフォルトは warning で、警告を表示するのみ。ansible-playbook コマンドのリターンコードは正常 0 となる。

$ ansible-playbook -i inventory.ini test.yml 
[WARNING]: Could not match supplied host pattern, ignoring: not_existed

PLAY [not_existed] ************************************************************************************************
skipping: no hosts matched

PLAY RECAP ********************************************************************************************************

$ echo $?
0

この場合、Ansible Tower のワークフローとしても正常扱いで次のジョブテンプレートへと進む。

hosts の指定先がインベントリ内に見つからない状況は、その後に想定外の状況を引き起こしかねないので、エラーとして扱って処理を中止した場合もある。特に hosts: "{{ target_host }}" のように変数で指定する場合は、起こりやすい。

エラーとして扱う場合は ansible.cfg で以下のように設定する。defaultsセクションではないので注意。

[inventory]
host_pattern_mismatch=error

以下のように、エラー扱い(リターンコード 1)となる。

$ ansible-playbook -i inventory.ini test.yml 
ERROR! Could not match supplied host pattern, ignoring: not_existed
$ echo $?
1

パースできないインベントリがある場合にエラーとするかの指定(INVENTORY_ANY_UNPARSED_IS_FAILED

INVENTORY_ANY_UNPARSED_IS_FAILED

インベントリファイルの書式の誤りで、正しくパースできないインベントリがある場合にエラーとするかどうかの指定。デフォルトは False で、エラーとしない扱い。

例えば、以下は書式が誤っているインベントリファイル(inventory2.ini)。

[ios2]
ios02 ansible_host=192.168.1.11

# vars とすべきところ var
[ios2:var] 
ansible_network_os=ios
ansible_connection=network_cli

このインベントリを指定して Playbook を実行すると、警告は表示されるがPlaybookの処理は続行しようとする。

$ ansible-playbook -i inventory.ini -i inventory2.ini test.yml 
[WARNING]:  * Failed to parse /home/ansible/inventory2.ini with ini plugin: /home/ansible/inventory2.ini:5: Section [ios:var] has unknown type: var
[WARNING]: Unable to parse /home/ansible/inventory2.ini as an inventory source

PLAY [ios] ********************************************************************************************************

TASK [debug] ******************************************************************************************************
ok: [ios01] => {
    "msg": "Hello Ansible!"
}

PLAY RECAP ********************************************************************************************************
ios01                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

$ echo $?
0

早めにエラーとして処理を中止したい場合は True に設定する。

[inventory]
any_unparsed_is_failed=True

以下のように、Playbookの実行前にエラーで処理が止まる。

$ ansible-playbook -i inventory.ini -i inventory2.ini test.yml 
[WARNING]:  * Failed to parse /home/ansible/inventory2.ini with ini plugin: /home/ansible/inventory2.ini:5: Section [ios:var] has unknown type: var
ERROR! Completely failed to parse inventory source /home/ansible/inventory2.ini
$ echo $?
1

ダイナミックインベントリにも有効。なので、認証情報に誤りがあってインベントリを取得できないときは処理を中止する」という使い方もできる。(aws_ec2 で確認)

補足

似た設定に INVENTORY_UNPARSED_IS_FAILEDもあるが、これは指定されたインベントリのうち1つでもパースできればエラーにしない。 一方、INVENTORY_ANY_UNPARSED_IS_FAILEDは、全てのインベントリがパースできれば正常、1つでもパースできなければエラーとするため、より安全。