Ansible: RECAPの総計を表示させる

# はじめに

Anslbleを使うと複数のホストにまとめて設定を叩き込むことができます。便利ですね。でもホスト数が増えると結果を確認するのも一苦労。なんとかならないか調べたらなんとかできたので、記録を残します。

# RACAP表示がてんこもり

`ansible-playbook`を実行すると、最後に要約(RECAP)が表示されます。


```
PLAY RECAP ********************************************************************************************************
hoatA                      : ok=7    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hoatB                      : ok=7    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hoatC                      : ok=15   changed=3    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hoatD                      : ok=5    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hostE                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostF                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostG                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostH                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostI                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostJ                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostK                      : ok=12   changed=1    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hostL                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostM                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostN                      : ok=10   changed=0    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hostO                      : ok=10   changed=0    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hsotP                      : ok=16   changed=1    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
```

実行ログ全体からすれば要約なのはわかりますが、それでもホスト数がそれなりだと量が多いです。まだ1画面におさまる範囲ならなんとか見渡せますが、もっと数が増えるとひととおり確認するのも大変です。うっかり見落としてしまいそうな気もします。

できれば、全ホストのトータルの値を見てまず`failed`がないことを確認するなどしたいと思うのですが、はたして方法はあるのでしょうか。

# 探してみた

なんとかまとめられる方法がないか検索したら、わりとあっさり見つかりました。

https://github.com/ansible/ansible/issues/14367

読んでいくと、callback pluginを用意してどうにかした→callback pluginがAnsible collectionのひとつcommunity.generalに追加された、ということがわかります。

# さっそくやってみた

ざっくりやったことは4つ。

- `community.general`のインストール
- `ansible.cfg`への追加
- 変数定義
- Playbookへの追加

ひとつずつみていきます。

## `community.general`のインストール

```
ansible-galaxy collection install community.general
```

## `ansible.cfg`への追加

まず、`ansible.cfg`にコールバックを追加します。Playbookと同じ場所に置いておくと読まれます。

```ini:ansible.cfg
[defaults]
stdout_callback = community.general.diy
```

## 変数定義

変数定義がてんこもりです。`diy.py`というだけあって、自分で全部どうにかするという感じです。これをPlaybookに直接書くのは微妙なので、ファイルを分けました。内容はissueに書かれていたサンプルほぼそのままです。(`cyan`の定義がなくて怒られたのでそこだけ追加しています)

```yaml:callback_vars.yml
    color_reset: "\e[0m"
    green: "\e[0m\e[38;5;2m"
    yellow: "\e[0m\e[38;5;226m"
    red: "\e[0m\e[38;5;1m"
    cyan: "\e[0m\e[38;5;51m"
    ansible_callback_diy_playbook_on_stats_msg: >-
      {%- macro new_message(type, value)                  -%}
      {%-   set types = {"ok": { "color": green }, 
                         "changed": { "color": yellow },
                         "unreachable": { "color": red },
                         "failed": { "color": red },
                         "skipped": { "color": cyan },
                         "rescued": { "color": cyan },
                         "ignored": { "color": cyan } }   -%}
      {%-   set msg = type+"="+("%-5s" | format(value))   -%}
      {%-   if value > 0                                  -%}
      {%-     set msg = types[type].color+msg+color_reset -%}
      {%-   endif                                         -%}
      {{-   msg                                           -}}
      {%- endmacro                                        -%}

      {%- set newline, stats = "\n",ansible_callback_diy.stats                                                                                    -%}
      {{- newline                                                                                                                                 -}}
      {{- "PLAY RECAP ******************************************************************************************************************"+newline -}}
      {{- "-----------------------------------------------------------------------------------------------------------------------------"+newline -}}
      {{- "SERVER RECAP"+newline+newline                                                                                                          -}}
      {%- for server, count in stats.processed.items() | sort                                                                                     -%}
      {%-   set ok = stats.ok[server] | default(0)                                                                                                -%}
      {%-   set ok_msg = new_message("ok", ok)                                                                                                    -%}
      {%-   set changed = stats.changed[server] | default(0)                                                                                      -%}
      {%-   set changed_msg = new_message("changed", changed)                                                                                     -%}
      {%-   set unreachable = stats.dark[server] | default(0)                                                                                     -%}
      {%-   set unreachable_msg = new_message("unreachable", unreachable)                                                                         -%}
      {%-   set failed = stats.failures[server] | default(0)                                                                                      -%}
      {%-   set failed_msg = new_message("failed", failed)                                                                                        -%}
      {%-   set skipped = stats.skipped[server] | default(0)                                                                                      -%}
      {%-   set skipped_msg = new_message("skipped", skipped)                                                                                     -%}
      {%-   set rescued = stats.rescued[server] | default(0)                                                                                      -%}
      {%-   set rescued_msg = new_message("rescued", rescued)                                                                                     -%}
      {%-   set ignored = stats.ignored[server] | default(0)                                                                                      -%}
      {%-   set ignored_msg = new_message("ignored", ignored)                                                                                     -%}
      {%-   set server_msg = "%-26s" | format(server)                                                                                             -%}
      {{-   server_msg+" : "+ok_msg+" "+changed_msg+" "+unreachable_msg+" "+failed_msg+" "+skipped_msg+" "+rescued_msg+" "+ignored_msg+newline    -}}
      {%- endfor                                                                                                                                  -%}
      {{- newline                                                                                                                                 -}}

      {%- set ok = stats.ok | dict2items | map(attribute='value') | sum                                                                           -%}
      {%- set ok_msg = new_message("ok", ok)                                                                                                      -%}
      {%- set changed = stats.changed | dict2items | map(attribute='value') | sum                                                                 -%}
      {%- set changed_msg = new_message("changed", changed)                                                                                       -%}
      {%- set unreachable = stats.dark | dict2items | map(attribute='value') | sum                                                                -%}
      {%- set unreachable_msg = new_message("unreachable", unreachable)                                                                           -%}
      {%- set failed = stats.failures | dict2items | map(attribute='value') | sum                                                                 -%}
      {%- set failed_msg = new_message("failed", failed)                                                                                          -%}
      {%- set skipped = stats.skipped | dict2items | map(attribute='value') | sum                                                                 -%}
      {%- set skipped_msg = new_message("skipped", skipped)                                                                                       -%}
      {%- set rescued = stats.rescued | dict2items | map(attribute='value') | sum                                                                 -%}
      {%- set rescued_msg = new_message("rescued", rescued)                                                                                       -%}
      {%- set ignored = stats.ignored | dict2items | map(attribute='value') | sum                                                                 -%}
      {%- set ignored_msg = new_message("ignored", ignored)                                                                                       -%}
      {{- "-----------------------------------------------------------------------------------------------------------------------------"+newline -}}
      {%- set task_msg = "%-26s" | format("TASK RECAP")                                                                                           -%}
      {{ task_msg+" : "+ok_msg+" "+changed_msg+" "+unreachable_msg+" "+failed_msg+" "+skipped_msg+" "+rescued_msg+" "+ignored_msg+newline         -}}
      {{- newline                                                                                                                                 -}}

      {%- set ok = stats.ok | length                                                                                                              -%}
      {%- set ok_msg = new_message("ok", ok)                                                                                                      -%}
      {%- set changed = stats.changed | length                                                                                                    -%}
      {%- set changed_msg = new_message("changed", changed)                                                                                       -%}
      {%- set unreachable = stats.dark | length                                                                                                   -%}
      {%- set unreachable_msg = new_message("unreachable", unreachable)                                                                           -%}
      {%- set failed = stats.failures | length                                                                                                    -%}
      {%- set failed_msg = new_message("failed", failed)                                                                                          -%}
      {%- set skipped = stats.skipped | length                                                                                                    -%}
      {%- set skipped_msg = new_message("skipped", skipped)                                                                                       -%}
      {%- set rescued = stats.rescued | length                                                                                                    -%}
      {%- set rescued_msg = new_message("rescued", rescued)                                                                                       -%}
      {%- set ignored = stats.ignored | length                                                                                                    -%}
      {%- set ignored_msg = new_message("ignored", ignored)                                                                                       -%}
      {{- "-----------------------------------------------------------------------------------------------------------------------------"+newline -}}
      {%- set host_msg = "%-26s" | format("HOST RECAP")                                                                                           -%}
      {{ host_msg+" : "+ok_msg+" "+changed_msg+" "+unreachable_msg+" "+failed_msg+" "+skipped_msg+" "+rescued_msg+" "+ignored_msg+newline+newline -}}
```
</details>

## Playbookへの追加

そしてPlaybookの**末尾に**下記を追加します。

```yaml:deploy.yml
- hosts: all
  gather_facts: no
  vars_files:
   - callback_vars.yml
```
# 実行してみた

結果は下記。

```
PLAY RECAP ********************************************************************************************************
-----------------------------------------------------------------------------------------------------------------------------
SERVER RECAP

hoatA                      : ok=7    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hoatB                      : ok=7    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hoatC                      : ok=15   changed=3    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hoatD                      : ok=5    changed=0    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hostE                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostF                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostG                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostH                      : ok=12   changed=1    unreachable=0    failed=0    skipped=6    rescued=0    ignored=0   
hostI                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostJ                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostK                      : ok=12   changed=1    unreachable=0    failed=0    skipped=7    rescued=0    ignored=0   
hostL                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostM                      : ok=10   changed=0    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
hostN                      : ok=10   changed=0    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hostO                      : ok=10   changed=0    unreachable=0    failed=0    skipped=8    rescued=0    ignored=0   
hsotP                      : ok=16   changed=1    unreachable=0    failed=0    skipped=26   rescued=0    ignored=0   
-----------------------------------------------------------------------------------------------------------------------------
TASK RECAP                 : ok=170   changed=9     unreachable=0     failed=0     skipped=206   rescued=0     ignored=0    

-----------------------------------------------------------------------------------------------------------------------------
HOST RECAP                 : ok=16    changed=7     unreachable=0     failed=0     skipped=16    rescued=0     ignored=0    

```
できました。このサンプルではテキストに色がつくようになっています。画像だと下記のような感じです。

![](https://pbs.twimg.com/media/E-K7GbyUUAADDEh?format=jpg&name=medium)

# おわりに

変数定義はPlaybookによらず同じなので、使いまわしするといいように思います。
名前の通りDIYなので、ああしたいこうしたいという場合はレッツチャレンジ！