てくなべ (tekunabe)

ansible / network / automation

[Ansible/AWX] ワークフロー内の一連のジョブの実行ログをファイルで保存するPlaybook

はじめに

Ansible Tower / AWX では、Playbook の実行ログがサーバー内に自動的に保存されます。 いつだれが実行してどうなったかを追跡するのに便利です。

そのログを、ファイルとしてダウンロードすることもできます。

f:id:akira6592:20200418213814p:plain:w500
出力のダウンロード

数個ログだったらよいのですが、ワークフロー内にいくつもジョブがあり、ひとつひとつ実行結果画面を開いてダウンロード、開いてダウンロード、を繰り返すのは少しつらいかもしれません。

この記事では、ワークフロー実行ID を指定すると、そのワークフロー内の一連のジョブの実行ログをファイルとして保存する Playbook を紹介します。

ただし、試作レベルです。たとえば、ワークフローからワークフローを呼ぶケースには対応していません。

  • 検証環境
    • AWX 11.0
    • Ansible 2.9.6

Playbook

さっそくですが、Playbook は以下のとおりです。

実現できそうな tower モジュールがなかったので、uri モジュールを利用します。

---
- hosts: tower
  gather_facts: false
  connection: local

  vars:
    wfjid: 600      # 対象のワークフロージョブID
    tower_username: testuser
    tower_password: testpass  # 必要に応じて暗号化などする
    validate_certs: no

  tasks:
    # (1) ワークフロー内のノードを取得
    - name: get workflow nodes
      uri:
        url: "https://{{ ansible_host }}/api/v2/workflow_jobs/{{ wfjid }}/workflow_nodes/"
        method: GET
        url_username: "{{ tower_username }}"
        url_password: "{{ tower_password }}"
        force_basic_auth: true
        validate_certs: false
      register: res_wf_nodes

    # (2) ワークフロージョブ内の各ジョブの実行ログを取得
    - name: get job stdout
      uri:
        url: "https://{{ ansible_host }}/api/v2/jobs/{{ item.job }}/stdout/?format=txt"
        method: GET
        return_content: true
        url_username: "{{ tower_username }}"
        url_password: "{{ tower_password }}"
        force_basic_auth: true
        validate_certs: false
      register: res_job_stdouts
      loop: "{{ res_wf_nodes.json.results }}"
      loop_control:
        label: "{{ item.job }} - {{ item.summary_fields.unified_job_template.name }}"
      when:
        - item.job != None   # 実行されなかったジョブはスキップ
        - item.summary_fields.job.type == 'job'   # 他の workflow_approval などはスキップ

    # (3) ワークフロージョブIDからワークフロー名を取得(ディレクトリ名に利用)
    - name: get workflow name by id
      uri:
        url: "https://{{ ansible_host }}/api/v2/workflow_jobs/{{ wfjid }}/"
        method: GET
        url_username: "{{ tower_username }}"
        url_password: "{{ tower_password }}"
        force_basic_auth: true
        validate_certs: false
      register: res_wfj
    
    # (4) ワークフロージョブID ごとのログファイル保存用ディレクトリを作成
    - name: make directory
      file:
        path: "log/wf_{{ wfjid }}_{{ res_wfj.json.name }}"
        state: directory
      register: res_dir

    # (5) ワークフロージョブ内の各ジョブの実行ログをファイルとして保存
    - name: save job log to file
      copy:
        content: "{{ item.content }}"
        dest: "{{ res_dir.path }}/job_{{ item.item.job }}_{{ jt_name }}_{{ summary.job.status }}.txt" # 
      vars:
        summary: "{{ item.item.summary_fields }}"
        jt_name: "{{ summary.unified_job_template.name }}"
      loop: "{{ res_job_stdouts.results }}"
      loop_control:
        label: "{{ item.item.job }} - {{ jt_name }}"
      when: 
        - item.item.job != None   # 実行されなかったジョブはスキップ
        - item.item.summary_fields.job.type == 'job'   # 他の workflow_approval などはスキップ

解説

Playbook 内の各タスクを簡単に解説します。

vars 部分

wfjid: 600 で対象のワークフロージョブIDを指定しています。

今回は埋め込んでしまっていますが、可変にするために vars_prompt や、extra_vars を利用するといいかもしれません。

(1) ワークフロー内のノードを取得

uri モジュールで、指定されたワークフロージョブ内のノードを取得します。

(2) ワークフロー内の各ジョブの実行ログを取得

各ジョブの実行ログを取得します。GUI で実行ログをダウンロードする場合は format=txt_download ですが、ここでは format=txt とします。

(3) ワークフロージョブIDからワークフロー名を取得(ディレクトリ名に利用)

あとでワークフロージョブIDごとにディレクトリを作成しますが、その際にワークフロー名もつけたかったので、このタイミングで取得します。

(4) ワークフロージョブID ごとのログファイル保存用ディレクトリを作成

わかりやすいように、log/ワークフローID_ワークフロー名ディレクトリを作成します。

(5) ワークフロージョブ内の各ジョブの実行ログをファイルとして保存

実際に実行ログをファイルとして保存します。

GUI で実行ログをダウンロードすると、job_ジョブID.txt というファイル名になりますが、ここでは、job_ジョブID_ジョブ名_ステータス.txt とします。 ステータスには、successcanceled など、そのジョブの状態が入ります。


■ 実行

今回対象のワークフロージョブは以下のものです。中央の jt_show9jt_show1 が失敗したときのみ実行するジョブなのでスキップされてます。

f:id:akira6592:20200418214127p:plain
対象のワークフロージョブ(結果)

Playbook を実行します。

$ ansible-playbook -i inventory.ini log_downlod.yml 

PLAY [tower] ******************************************************************************************************

TASK [get workflow nodes] *****************************************************************************************
ok: [awx1]

TASK [get job stdout] *********************************************************************************************
ok: [awx1] => (item=601 - jt_show1)
ok: [awx1] => (item=606 - jt_show4)
ok: [awx1] => (item=603 - jt_show2)
skipping: [awx1] => (item= - jt_show9) 
ok: [awx1] => (item=604 - jt_show3)

TASK [get workflow name by id] ************************************************************************************
ok: [awx1]

TASK [make directory] *********************************************************************************************
changed: [awx1]

TASK [save job log to file] ***************************************************************************************
changed: [awx1] => (item=601 - jt_show1)
changed: [awx1] => (item=606 - jt_show4)
changed: [awx1] => (item=603 - jt_show2)
skipping: [awx1] => (item= - jt_show9) 
changed: [awx1] => (item=604 - jt_show3)

PLAY RECAP ********************************************************************************************************
awx1                       : ok=5    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

確認

以下のように実行ログが保存されます。

$ tree log
log
└── wf_600_wf_show
    ├── job_601_jt_show1_successful.txt
    ├── job_603_jt_show2_successful.txt
    ├── job_604_jt_show3_successful.txt
    └── job_606_jt_show4_successful.txt

結果から以下のことが分かります。

  • 今回の対象のワークフロージョブID 600 のワークフロー名は wf_show
  • ワークフロー内には以下のジョブが含まれ、すべて成功した
    • ジョブID 601、ジョブ名 jt_show1
    • ジョブID 603、ジョブ名 jt_show2
    • ジョブID 604、ジョブ名 jt_show3
    • ジョブID 605、ジョブ名 jt_show4
  • jt_show9 のジョブは実行されずにスキップされた
    • 条件的に実行されないノードのため

代表して一つ、ログの中身を確認します。見慣れた Playbook 実行ログです。

$ cat log/wf_600_wf_show/job_601_jt_show1_successful.txt 
SSH ********: 

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

TASK [show verion] *************************************************************
ok: [iosao_latest]
ok: [iosao]
ok: [iosao_latest] => {
    "msg": {
        "ansible_facts": {
            "discovered_interpreter_python": "/usr/libexec/platform-python"
        },
        "changed": false,
        "failed": false,
        "stdout": [
            "Cisco IOS XE Software, Version 16.11.01a\\nCisco IOS Software [Gibraltar], ...(略)..."
        ],
        "stdout_lines": [
            [
                "Cisco IOS XE Software, Version 16.11.01a",
                ...(略)...
                "",
                "Configuration register is 0x2102"
            ]
        ]
    }
}
ok: [iosao] => {
    ...(略)...
    }
}
iosao                      : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
iosao_latest               : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   


■ おわりに

少々手間ではありますが、専用のモジュールがなくても、API 経由でやりたいことができました。