てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] dict のようで dict でない値にキーを指定してエラーが発生した場合の対処(from_jsonフィルター)

はじめに

コマンドの出力結果などで dict らしき出力を受け取った後、その中の値から必要な値を抽出する機会がときどきあります。

ところが、たとえば、キーを指定して dictらしき結果.キー名 を指定すると、以下のようなエラーまってしまうことがあります。

ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'キー名'

前段で debug モジュールで表示しても dict らしき値として表示されますが、よく確認するとただのテキストだったりします。

こんなケースでの確認や、対処方法をまとめます。

  • 検証環境
    • ansible-core 2.14.4

再現用のPlaybook

以下のPlaybookを再現に利用します。

---
- name: Test play
  hosts: localhost
  connection: local
  gather_facts: false

  tasks:
    # JSONファイルの中身を表示(再現用の簡易的なもの)
    - name: Get JSON Text
      ansible.builtin.command:
        cmd: cat out.json
      changed_when: false
      register: json_text

    # デバッグ用に一旦 stduout 全体を表示
    - name: Debug1 stduout
      ansible.builtin.debug:
        msg: "{{ json_text.stdout }}"

    # JSON 内の key1 の値を表示
    - name: Debug2 key specified
      ansible.builtin.debug:
        msg: "{{ json_text.stdout.key1 }}"

なお、 out.json の内容は以下のとおりです。今回は簡単に再現させるために用意しました。

{
    "key1": "val1",
    "key2": "val2",
    "key3": "val3"
}

つまり最後のタスクは、 val1 が表示されることを期待しています。

再現

先ほどの Playbook を実行して再現します。

 % ansible-playbook -i localhost, dict_test.yml

...(略)...
TASK [Debug] ***************************************************************************************************************
fatal: [localhost]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'key1'. 'ansible.utils.unsafe_proxy.AnsibleUnsafeText object' has no attribute 'key1'\n\nThe error appears to be in '/Users/sakana/ansible/qa/dict_test.yml': line 14, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: Debug\n      ^ here\n"}
...(略)...

このように、key1 というアトリビュート(キー)がないよ、というエラーになりました。

stdout 全体を表示したタスクでは、いかにも JSON らしく表示されいて、key1 もあります。そのため、個人的な経験としてはここでおやおやと思ったことが何度かあります。

調査

エラーメッセージには、ansible.utils.unsafe_proxy.AnsibleUnsafeText object とあるため、json_text.stdoutの中身は dict ではなく、ただのテキストと扱われてることが分かります。

念のため ansible.builtin.type_debug フィルターで型を表示させてみます。

    # デバッグ用に一旦 stduout 全体を表示
    - name: Debug1 stduout
      ansible.builtin.debug:
        msg: "{{ json_text.stdout | ansible.builtin.type_debug }}"

結果は以下のとおり、AnsibleUnsafeText と表示されました。

ok: [localhost] => {
    "msg": "AnsibleUnsafeText"
}

もし本当に dict であれば、dict と表示されるはずです。ということで、おかしいぞ、dict のようで dict じゃないぞと気がつけます。

また、json_text.stdout ではなく、json_text だけを表示させると、stdout は以下のように表示されます。これはこれでテキストだと判断できます。

ok: [localhost] => {
    "msg": {
// ...(略)...
        "stdout": "{\n    \"key1\": \"val1\",\n    \"key2\": \"val2\",\n    \"key3\": \"val3\"\n}",
// ...(略)...
}

対処

今回のようなケースの場合、dict として扱うために、ansible.builtin.from_json フィルターを利用します。

json_text.stdout.key1 だったところを (json_text.stdout | ansible.builtin.from_json).key1 にします。

    - name: Debug2 key specified
      ansible.builtin.debug:
        msg: "{{ (json_text.stdout | ansible.builtin.from_json).key1 }}"

これで .key1 で参照できるようになります。

ok: [localhost] => {
    "msg": "val1"
}

タスク変数でワンクッション設けてもいいかもしれません。

    - name: Debug2 key specified
      ansible.builtin.debug:
        msg: "{{ converted_dict.key1 }}"
      vars:
        converted_dict: "{{ json_text.stdout | ansible.builtin.from_json }}"

参考

書いてる途中で思い出しましたが、過去に似たような記事を書いていました。

tekunabe.hatenablog.jp

最近のバージョンの Ansible でも同じ方法が使えることを示すという意味で、今回の記事はこれはこれで書いておきます。