てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] 「つまずき Ansible 【Part21】ansible.utils collection を試す」ふりかえり

はじめに

2020/12/12 に、YouTube Live で「つまずき Ansible 【Part21】ansible.utils collection を試す」という配信をしました。

実際に作業しながら(ときには)エラーと戦って進めるシリーズです。

tekunabe.connpass.com

今回は、先日 1.0.0 になった、ansible.utils collection のモジュールを触ってみます。

ネストされた変数の比較やマージに便利そうだったので、興味をそそりました。

同じような機能でも、フィルター、ルックアッププラグイン、モジュールのように複数の利用形態があるものもあります。

やったことをふりかえります。

  • 環境
    • ansible 2.10.0
    • ansible-base 2.10.3
    • ansible.utils collection 1.0.1

動画

youtu.be


■ やったこと

各ドキュメントの Example の載ってる例を参考にして、いろいろ試しました。

準備

2.9 でも 2.10 系での標準では含まれていない collection なので、予め以下のコマンドでインストールしておきます。

ansible-galaxy collection install ansible.utils

インストールしてないとこんなメッセージが表示されました。

[WARNING]: an unexpected error occurred during Jinja2 environment setup: unable
to locate collection ansible.utils
fatal: [localhsot]: FAILED! => {"msg": "template error while templating string: unable to locate collection ansible.utils. String: {{ a|ansible.utils.get_path(path) }}"}

get_path フィルター

get_path フィルター

ネストされた変数に対して、ドット区切りのパスを指定することで、そのパスに該当する値を抽出します。

---
- hosts: all
  gather_facts: false

  tasks:
    - ansible.builtin.set_fact:
        a:
          b:
            c:
              d:
              - 0
              - 1
              e:
              - True
              - False
    - name: Retrieve a value deep inside a using a path
      ansible.builtin.debug:
        msg: "{{ a | ansible.utils.get_path(path) }}"
      vars:
        path: b.c.d[0]

実行結果(抜粋)

TASK [ansible.builtin.set_fact] *************************************************
ok: [localhsot]

TASK [Retrieve a value deep inside a using a path] ******************************
ok: [localhsot] => {
    "changed": false,
    "msg": "0"
}

もしかしたら、

        msg: "{{ a.b.c.d[0] }}"

の動きと変わらないので、特に有り難みがないかも知れませんね。。

to_paths フィルター

to_paths フィルター

ネストされた変数内で、条件にマッチした値のパスを取得します。

---
- hosts: all
  gather_facts: false

  tasks:
    - ansible.builtin.set_fact:
        a:
          b:
            c:
              d:
              - 0
              - 1
              e:
              - True
              - False

    - ansible.builtin.debug:
        msg: "{{ a|ansible.utils.to_paths(prepend='a') }}"

実行結果(抜粋)

TASK [ansible.builtin.set_fact] ***********************************
ok: [localhsot]

TASK [ansible.builtin.debug] **************************************
ok: [localhsot] => {
    "changed": false,
    "msg": {
        "a.b.c.d[0]": 0,
        "a.b.c.d[1]": 1,
        "a.b.c.e[0]": true,
        "a.b.c.e[1]": false
    }
}

prepend='a' を指定すると、a からのパスの取得になります。

この表示形式は、後述の fact_diff と組み合わせでも活用できます。

index_of フィルター

index_of フィルター

条件にマッチした値のインデックス(0オリジン)を取得します。

---
- hosts: all
  gather_facts: false

  tasks:
    - set_fact:
        data:
          - 1 # index 0
          - 2 # index 1
          - 3 # index 2

    - name: Find the index of 2
      ansible.builtin.debug:
        msg: "{{ data | ansible.utils.index_of('eq', 2) }}"  # 2 の値

    - name: 2
      ansible.builtin.debug:
        msg: "{{ data | ansible.utils.index_of('ge', 2) }}"  # 2以上の値

実行結果(抜粋)

TASK [Find the index of 2] *********************************
ok: [localhsot] => {
    "changed": false,
    "msg": "1"
}

TASK [ansible.builtin.debug] *******************************
ok: [localhsot] => {
    "changed": false,
    "msg": [
        1,
        2
    ]
}

条件に使用した、eqge は色々変更できそうです。

あとからツイート拝見したのですが、

気になってたしたらやっぱり空のリストがかえってきました。

TASK [Find the index of 9999] *****
ok: [localhsoaaat] => {
    "changed": false,
    "msg": []
}

update_fact モジュール

update_fact モジュール

ネストされた変数に対して、パスを指定した特定の部分のみを更新できます。

---
- hosts: all
  gather_facts: false

  tasks:
    # Update an exisitng fact, dot or bracket notation
    - name: Set a fact
      set_fact:
        a:
          b:
            c:
              - 1 # -> 10
              - 2 # -> 20

    - name: Update the fact
      ansible.utils.update_fact:
        updates:
          - path: a.b.c.0
            value: 10
          - path: "a['b']['c'][1]"
            value: 20
      register: updated

    - name:
      debug:
        msg: "{{ updated }}"

実行結果(抜粋)

TASK [Set a fact] ********************************
ok: [localhsot]

TASK [Update the fact] ***************************
changed: [localhsot]

TASK [debug] *************************************
ok: [localhsot] => {
    "msg": {
        "a": {
            "b": {
                "c": [
                    10,
                    20
                ]
            }
        },
        "changed": true,
        "failed": false
    }
}

もとの変数はそのままで、変更後の値は regsiter で受け取る必要があります。非破壊的な動作ですね。

fact_diff モジュール

fact_diff モジュール

今回一番便利だと思ったものです。2つのネストされた変数で異なる値の部分を diff 表示します。

前述の to_paths フィルターと組合わると、表示方法も変更できます。

---
- hosts: all
  gather_facts: false

  tasks:
    - set_fact:
        before:
          a:
            b:
              c:
                d:
                  - 0
                  - 1
        after:
          a:
            b:
              c:
                d:
                  - 2
                  - 3

    - name: diff1
      ansible.utils.fact_diff:
        before: "{{ before }}"
        after: "{{ after }}"

    - name: diff2
      ansible.utils.fact_diff:
        before: "{{ before | ansible.utils.to_paths }}"
        after: "{{ after | ansible.utils.to_paths }}"

実行結果(抜粋)

TASK [set_fact] *****************************************
ok: [localhsot]

TASK [diff1] ********************************************
--- before
+++ after
@@ -3,8 +3,8 @@
         "b": {
             "c": {
                 "d": [
-                    0,
-                    1
+                    2,
+                    3
                 ]
             }
         }

changed: [localhsot]

TASK [diff2] ********************************************
--- before
+++ after
@@ -1,4 +1,4 @@
 {
-    "a.b.c.d[0]": 0,
-    "a.b.c.d[1]": 1
+    "a.b.c.d[0]": 2,
+    "a.b.c.d[1]": 3
 }

changed: [localhsot]

ツイートいただいて、changed になってることに気が付きました。

差分がない場合は、以下のように ok になりました。

PLAY [all] **********************************

TASK [set_fact] *****************************
ok: [localhsot]

TASK [diff1] ********************************
ok: [localhsot]

TASK [diff2] ********************************
ok: [localhsot]

validate モジュール

validate モジュール

サンプルの準備ができずためせませんでした。

JSON Schema などの定義を利用して妥当性のチェックができるようです。

[2020/12/12 23:20 追記] あとでテストにあるファイル oper_status_up.json や、show_interfaceを参考にて試せました。

環境としては、まず pip install jsonschema が必要です。

---
- hosts: all
  gather_facts: false

  tasks:
    - name: set facts for data and criteria
      set_fact:
        data: "{{ lookup('file', './files/show_interface.json')}}"
        criteria: "{{ lookup('file', './files/oper_status_up.json') }}"
        
    - name: validate data in with jsonschema engine (by passing task vars as configurable plugin options)
      ansible.utils.validate:
        data: "{{ data }}"
        criteria: "{{ criteria }}"
        engine: ansible.utils.jsonschema
      vars:
        ansible_jsonschema_draft: draft7

実行結果: NGケース(抜粋)(見やすさのため callback plugin を yaml に変更)

TASK [validate data in with jsonschema engine (by passing task vars as configurable plugin options)] ******
fatal: [localhost]: FAILED! => changed=false 
  errors:
  - data_path: GigabitEthernet0/0/0/0.oper_status
    expected: up
    found: down
    json_path: $.GigabitEthernet0/0/0/0.oper_status
    message: '''down'' does not match ''up'''
    relative_schema:
      pattern: up
      type: string
    schema_path: patternProperties.^.*.properties.oper_status.pattern
    validator: pattern
  msg: |-
    Validation errors were found.
    At 'patternProperties.^.*.properties.oper_status.pattern' 'down' does not match 'up'.

上記は、 oper_status_up.json

// 略
                "oper_status": {
                    "type": "string",
                    "pattern": "up"
                }
// 略

といいう定義に対して、show_interface"GigabitEthernet0/0/0/0""oper_status": "down" だったため、 validation 失敗しました。

実行結果: OKケース(抜粋)

show_interface"GigabitEthernet0/0/0/0""oper_status": "up" に修正したところ以下のように ok となりました。

TASK [set facts for data and criteria] ********************************************************************
ok: [localhost]

TASK [validate data in with jsonschema engine (by passing task vars as configurable plugin options)] ******
ok: [localhost]

cli_parse モジュール

cli_parse モジュール

ansbile.netcommon collectionansbile.netcommon.cli_parse モジュールと同じようにみえますが、こちらにもある経緯などはわかりませせん・・。

[2020/12/14 追記] ansible.utils.cli_parse モジュールを追加する Pull requesut には、ansible.netcommon collection から移動、とありました。現状はまだ ansible.netcommon.cli_parseはあるようですが、そのうちなくなるかもしれません。

ansbile.netcommon.cli_parse モジュール は Part 15 で触れました。 tekunabe.hatenablog.jp


おわりに

地味なのものが多いですが、変数の操作を標準機能で賄おうとして Playbook が複雑になってしまいそうなときは、この collection を思い出すと良いかなと思いました。


Part22 にむけて

以下のネタを検討中です。気が向いたものをやります。

  • AWX 16.0.0
  • Ansible 2.10 関連ほかにも
  • connection: local ななにか
  • Ansible Toewr / AWX をコマンドがら操作する
  • ansible.cfg
  • Jinja2、フィルター
  • Windows
  • ESXi で VM作成
  • parse_cli モジュール(Part15 の続き)