Ansibleのcallback pluginを使って突然の死(echo-sd)を表現する

[Ansible Advent Calendar 2017 4日目](https://qiita.com/advent-calendar/2017/ansible)が空いていたので入れます。(これを書いているときは11日目です)

# ソースコードをいじらずにAnsibleの出力結果を加工する

Ansible Advent Calendar 2017 10日目の [我が家のAnsibleは突然の死にあこがれる](https://qiita.com/t_nakayama0714/items/11a24ea0ac0d6ce1bc47)を見て思いついたので書きます。

上記記事では、ゴリゴリにソースコードをいじっていますが、出力を加工するのであれば、[Callback plugins](http://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#callback-plugins)を使うのが普通なんじゃないかなと思っています(ソースコードを直接変えるのは怖いですよね)。

* [Callback Plugins](https://docs.ansible.com/ansible/devel/plugins/callback.html)
* [Developing Plugins | Callback Plugins](http://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#callback-plugins)

上記の公式ページでは物凄くさらっと「callback pluginを使って出力を加工できるよ」とあるのですが、要はサンプルを見ろということが書いてあって、一体何をやっているのかろくな説明もなくて分からず、かつ世の中的にもあまりcallback pluginの紹介と書き方の説明がなさそうなの紹介します。

# callback pluginとは

上記の通りなのですが、要はAnsibleのイベント(開始や実行結果など)を受けて呼び出されるpluginを指定できます。
上記リンクではすごく簡単な書き方が紹介されていますが、実際には「イベント」はめちゃくちゃ一杯あります。詳細はソースを見たほうが早いです。

* [ansible/lib/ansible/plugins/callback/__init__.py](https://github.com/ansible/ansible/blob/v2.4.2.0-1/lib/ansible/plugins/callback/__init__.py)

```python:__init__.pyからの抜粋
    def on_any(self, *args, **kwargs):
    def runner_on_failed(self, host, res, ignore_errors=False):
    def runner_on_ok(self, host, res):
    def runner_on_skipped(self, host, item=None):
    def runner_on_unreachable(self, host, res):
    def runner_on_no_hosts(self):
    def runner_on_async_poll(self, host, res, jid, clock):
    def runner_on_async_ok(self, host, res, jid):
    def runner_on_async_failed(self, host, res, jid):
    def playbook_on_start(self):
    def playbook_on_notify(self, host, handler):
    def playbook_on_no_hosts_matched(self):
    def playbook_on_no_hosts_remaining(self):
    def playbook_on_task_start(self, name, is_conditional):
    def playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
    def playbook_on_setup(self):
    def playbook_on_import_for_host(self, host, imported_file):
    def playbook_on_not_import_for_host(self, host, missing_file):
    def playbook_on_play_start(self, name):
    def playbook_on_stats(self, stats):
    def on_file_diff(self, host, diff):
    # V2 METHODS, by default they call v1 counterparts if possible
    def v2_on_any(self, *args, **kwargs):
    def v2_runner_on_failed(self, result, ignore_errors=False):
    def v2_runner_on_ok(self, result):
    def v2_runner_on_skipped(self, result):
    def v2_runner_on_unreachable(self, result):
    # FIXME: not called
    def v2_runner_on_async_poll(self, result):
    # FIXME: not called
    def v2_runner_on_async_ok(self, result):
    # FIXME: not called
    def v2_runner_on_async_failed(self, result):
    def v2_playbook_on_start(self, playbook):
    def v2_playbook_on_notify(self, result, handler):
    def v2_playbook_on_no_hosts_matched(self):
    def v2_playbook_on_no_hosts_remaining(self):
    def v2_playbook_on_task_start(self, task, is_conditional):
    # FIXME: not called
    def v2_playbook_on_cleanup_task_start(self, task):
    def v2_playbook_on_handler_task_start(self, task):
    def v2_playbook_on_vars_prompt(self, varname, private=True, prompt=None, encrypt=None, confirm=False, salt_size=None, salt=None, default=None):
    # FIXME: not called
    def v2_playbook_on_import_for_host(self, result, imported_file):
    # FIXME: not called
    def v2_playbook_on_not_import_for_host(self, result, missing_file):
    def v2_playbook_on_play_start(self, play):
    def v2_playbook_on_stats(self, stats):
    def v2_on_file_diff(self, result):
    def v2_playbook_on_include(self, included_file):
    def v2_runner_item_on_ok(self, result):
    def v2_runner_item_on_failed(self, result):
    def v2_runner_item_on_skipped(self, result):
    def v2_runner_retry(self, result):
```

`FIXME`も一部ありますが、どれだけイベントがたくさんあるかがわかると思います。逆に言うと、これらの単位で出力結果を加工できるということです。(`on_any`とかちゃんと書いたら恐ろしいことになりそう)

`v2_` で始まる関数は、ansible v2系で優先的に呼び出されるもので、今となっては `v2_` 系だけ定義すればよいかと思いますが、もし古い1系を使っているのであれば`v2_`なしの方を定義することになるかと思います。

# callback pluginを作る

[Developing Plugins | Callback Plugins](http://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#callback-plugins)に作り方が載っているのですが、最初に書いた通り要は「サンプルを見ろ」という感じで、大分辛いものがあります…。

サンプルは[ansible/lib/ansible/plugins/callback/](https://github.com/ansible/ansible/tree/v2.4.2.0-1/lib/ansible/plugins/callback)にあります。その中でも、[default.py](https://github.com/ansible/ansible/blob/v2.4.2.0-1/lib/ansible/plugins/callback/default.py)を見るのが最もよいでしょう(というかそれ以外を散々調べていて、あとでこれを見つけてこれにすればよかったと後悔しました)。

作ったプラグインは、自分の動かしたいplaybookの直下に`callback_plugins`ディレクトリを作って、そこに置きます。他にも`role`の配下とかでもよいです。詳しくは以下をご覧ください。

* [Distributing Plugins](http://docs.ansible.com/ansible/latest/dev_guide/developing_plugins.html#distributing-plugins)

> Plugins are automatically loaded when you have one of the following subfolders adjacent to your playbook or inside a role

# 突然の死をcallback pluginとして作る

先に、作成したプラグインを紹介します。(`echo-sd`の実行ファイルは`/usr/local/bin/`配下に置き、実行権を与えてください)

```python:echo-sd.py
# Make coding more python3-ish
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
from ansible.module_utils._text import to_bytes, to_text
import os
import subprocess

class CallbackModule(CallbackModule_default):
    """
    This callback module brings suddenly death.
    """
    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'echo-sd'
    CALLBACK_NEEDS_WHITELIST = True

    b_ECHO_SD_PATHS = (
      b"/usr/local/bin/echo-sd",
    )

    def __init__(self):
        super(CallbackModule, self).__init__()

        self.b_echo_sd = None
        self.set_echo_sd_info()

    def set_echo_sd_info(self):
        for b_echo_sd_path in self.b_ECHO_SD_PATHS:
            if os.path.exists(b_echo_sd_path):
                self.b_echo_sd = b_echo_sd_path

    def print_echo_sd(self, message):
        runcmd = [self.b_echo_sd]
        runcmd.append(message)
        cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = cmd.communicate()
        self._display.display(u"%s\n" % to_text(out))

    def v2_runner_on_ok(self, result):
        if 'msg' in result._result:
          self.print_echo_sd(result._result.pop('msg'))

    v2_runner_on_failed = v2_runner_on_ok
    v2_runner_on_unreachable = v2_runner_on_ok
    v2_runner_on_skipped = v2_runner_on_ok
 ```

 見れば分かる通り、かなりシンプルに見えるのではないでしょうか。それぞれのタスクの実行時に `print_echo_sd` によって `echo-sd`コマンドを呼び出しているだけです。

公式では、インポート対象を以下のようにしていますが、こうすると関数を定義したイベント以外は全て何も表示されなくなります。

```python:echo-sd.py(一から全部作りたい場合)
from ansible.plugins.callback import CallbackBase

class CallbackModule(CallbackBase):
```

最初から全部作る場合はこれでいいと思いますが、既存の出力から一部だけ加工したい場合は以下のように`default`をimportするとよいでしょう。

```python:echo-sd.py(既存の出力結果から一部だけ加工したい場合)
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default

class CallbackModule(CallbackModule_default):
```

# callback pluginを使う

単にプラグインを`callback_plugins`配下に置けば使われるのではなく、`ansible.cfg`配下で以下のように定義する必要があります。

```ansible.cfg
[defaults]
stdout_callback = echo-sd
```

`ansible.cfg`ファイルはplaybookの直下に置けば読み込まれます。

あとは、playbookを実行します。`echo-sd`プラグインを入れる前は以下のような感じです。

```:突然死の前
PLAY [echo-sd-servers] *********************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************
ok: [localhost]

TASK [debug] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "突然の死"
}

TASK [debug] *******************************************************************************************************************************
ok: [localhost] => {
    "msg": "echo-sd"
}

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

PLAY RECAP *********************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

とても健康的です。これが、`echo-sd`プラグインを有効にすると以下のようになります。

```出力結果
$ ansible-playbook -i local local.yml

PLAY [echo-sd-servers] *********************************************************************************************************************

TASK [Gathering Facts] *********************************************************************************************************************

TASK [debug] *******************************************************************************************************************************
＿人人人人人人＿
＞　突然の死　＜
￣Y^Y^Y^Y^Y^Y^￣


TASK [debug] *******************************************************************************************************************************
＿人人人人人＿
＞　echo-sd　＜
￣Y^Y^Y^Y^Y^￣


TASK [debug] *******************************************************************************************************************************
＿人人人人人人＿
＞　localhost　＜
￣Y^Y^Y^Y^Y^Y^￣


PLAY RECAP *********************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

このように、いい感じで突然の死が訪れました。

# 死に方が足りない

まだ死に方が足りませんか？そんな時は、「イベント」をそれぞれ足していけばいいです。

```python:echo-sd.py(抜粋)
    def v2_playbook_on_start(self, playbook):
        from os.path import basename
        self.print_echo_sd(basename(playbook._file_name) + ' playbook!')
```

これで

```python
PLAYBOOK: local.yml ************************************************************************************************************************
```

これが(`ansible-playbook -vv`以上でないと表示されません)

```playbookが死ぬ
＿人人人人人人人人人人人＿
＞　local.yml playbook!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^￣
```

こうなります。

次に

```python:echo-sd.py(抜粋)
    def v2_playbook_on_play_start(self, play):
        self.print_echo_sd(play.get_name() + ' hosts!!')
```

これで

```
PLAY [echo-sd-servers] *********************************************************************************************************************
```

これが

```:inventoryが死ぬ
＿人人人人人人人人人人人人人＿
＞　echo-sd-servers hosts!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^￣
```

こうなります。

最後に

```python:echo-sd.py(抜粋)
    def v2_playbook_on_task_start(self, task, is_conditional):
        self.print_echo_sd(task.get_name() + ' start!!!')
```

これで

```
TASK [debug] *******************************************************************************************************************************
```

これが

```:taskが死ぬ
＿人人人人人人人人人＿
＞　debug start!!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^￣
```

こうなります。

最終的に以下みたいになります。

```
＿人人人人人人人人人人人＿
＞　local.yml playbook!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人人人人人人人人人＿
＞　echo-sd-servers hosts!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人人人人人人人人人人＿
＞　Gathering Facts start!!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人人人人人＿
＞　debug start!!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人人＿
＞　突然の死　＜
￣Y^Y^Y^Y^Y^Y^￣

＿人人人人人人人人人＿
＞　debug start!!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人＿
＞　echo-sd　＜
￣Y^Y^Y^Y^Y^￣

＿人人人人人人人人人＿
＞　debug start!!!　＜
￣Y^Y^Y^Y^Y^Y^Y^Y^Y^￣

＿人人人人人人＿
＞　localhost　＜
￣Y^Y^Y^Y^Y^Y^￣


PLAY RECAP *********************************************************************************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

もう意味が分かりませんね。

# まだ死んでないやつがいる

`def v2_playbook_on_stats(self, stats):`というやつなのですが、結果をいい感じで整形する必要があるのですが、~~面倒で~~諦めました。

ソースを置いておきますので、徹底的に殺したい方はチャレンジしてみてください。
https://github.com/tkit/ansible_callback-plugin_echo-sd

# 終わりに

公式にサンプルがいっぱいありますが、Ansibleの実行結果を加工して別の処理に渡したい場合などに使えそうです。デフォルトだと「`*****************`」みたいなのを外して加工するのは大変そうですし(そもそもナンセンスでしょうし)。