［Ansible小ネタ］ にゃんしぶるを支える技術 ［cowsay/callback plugin］

Ansible界隈の人たちとの会話で何気なく「にゃんしぶる」という単語を使ったら、Ansible実行時にﾈｺﾁｬﾝが出てくるモジュールとか作りそうな流れになってきたので、ひとまずメッセージ出力でにゃんしぶるできないかどうか試してみましたにゃーん。。。

![2019-11-04_14h05_26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/256859/3a9f476f-6bf1-2ccf-4ff6-d8e0b7ceb80a.png)

環境

```
[zaki@manager nyan]$ ansible --version
ansible 2.8.5
  config file = /home/zaki/work/ansible/nyan/ansible.cfg
  configured module search path = [u'/home/zaki/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Aug  7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
[zaki@manager nyan]$
[zaki@manager nyan]$ cat /etc/redhat-release
CentOS Linux release 7.7.1908 (Core)
[zaki@manager nyan]$
```

なお、本記事の内容は、`cowsay`の使い方・アレンジ方法と、Ansible Callback Pluginの簡単な作り方という構成になってます。

# 解説はいいからちょっと試したい

[詳細は後述](https://qiita.com/zaki-lknr/items/5de3f11389090f38bf6e#%E5%AE%9F%E8%A3%85%E4%BE%8B)してるけど、`/usr/local/bin/cowsay`に以下のスクリプトをコピペ。(スクリプトの引数を元に整形してascii artを`print`してるだけです)

```perl:/usr/local/bin/cowsay
#!/usr/bin/perl

my $border = "-" x ((length $ARGV[-1]) + 2);
print <<"__EOL__";
 $border
< $ARGV[-1] >
 $border
　\\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ
__EOL__
```

そして`chmod 755 /usr/local/bin/cowsay`で実行権限を付与して、Ansibleを実行すればOK

元に戻したくなったら、`/usr/local/bin/cowsay`を削除すればOKです。

# cowsay でにゃんしぶる

Ansible実行時に、システムに`cowsay`コマンドがインストールされている場合、このコマンドを使ってtask名を出力する機能が備わっている。

Ansibleから`cowsay`を呼び出している部分はこの箇所

https://github.com/ansible/ansible/blob/stable-2.8/lib/ansible/utils/display.py

## 自前の cowsay を用意してにゃんしぶる

Ansible実行時に`cowsay`がインストールされているかどうかは、[以下のリスト](https://github.com/ansible/ansible/blob/stable-2.8/lib/ansible/utils/display.py#L74)に実行ファイルがあるかどうかを見て判断している。

```python
b_COW_PATHS = (
    b"/usr/bin/cowsay",
    b"/usr/games/cowsay",
    b"/usr/local/bin/cowsay",  # BSD path for cowsay
    b"/opt/local/bin/cowsay",  # MacPorts path for cowsay
)
```

これらのパスのどこかに、実行可能な`cowsay`スクリプト等を配置すればOK。
逆に言えば、Ansible実行時にパスが通っている場所(`~/local/bin/cowsay`とか)に`cowsay`を置いていても、これらのパス以外であれば反応しない。

なので、`yum`などで`cowsay`をインストールしなくても、`/usr/local/bin/cowsay`を自分で用意してしまえば、Ansibleでにゃんしぶるが可能。
(※ `chmod 755`で実行権限を与えるのをお忘れなく)

### 実装例

例えばPerlスクリプト

```perl:/usr/local/bin/cowsay
#!/usr/bin/perl

my $border = "-" x ((length $ARGV[-1]) + 2);
print <<"__EOL__";
 $border
< $ARGV[-1] >
 $border
　\\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ
__EOL__
```

オリジナルの`cowsay`がインストール済みで、それ以外は特に追加設定を行わない場合、Ansibleからは`cowsay -W 60 -f default "メッセージ"`という形式で`cowsay`が実行される。

```python
    def banner_cowsay(self, msg, color=None):
        if u": [" in msg:
            msg = msg.replace(u"[", u"")
            if msg.endswith(u"]"):
                msg = msg[:-1]
        runcmd = [self.b_cowsay, b"-W", b"60"]
        if self.noncow:
            thecow = self.noncow
            if thecow == 'random':
                thecow = random.choice(list(self.cows_available))
            runcmd.append(b'-f')
            runcmd.append(to_bytes(thecow))
        runcmd.append(to_bytes(msg))
        cmd = subprocess.Popen(runcmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        (out, err) = cmd.communicate()
        self.display(u"%s\n" % to_text(out), color=color)
```

この関数の

```python
        runcmd = [self.b_cowsay, b"-W", b"60"]

[...]

            runcmd.append(b'-f')
            runcmd.append(to_bytes(thecow))
```

の部分。

なので、自作の`cowsay`を作る場合は、この呼び出し仕様を踏まえて、引数の一番最後を取り出して出力すれば、にゃんしぶるが実行できる。
(Perlにおいてスクリプトの引数は`@ARGV`配列で受け取ることができ、配列の末尾を参照するには配列インデックスに`-1`を指定すれば取り出せる)

### 実行例

```
[zaki@manager nyan]$ ansible-playbook playbook.yml 
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'

 ------------------
< PLAY [localhost] >
 ------------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

 ------------------------
< TASK [Gathering Facts] >
 ------------------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

ok: [localhost]
 -------------
< TASK [ping] >
 -------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

ok: [localhost]
 ------------
< PLAY RECAP >
 ------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[zaki@manager nyan]$
```

## cowsay に自前のテンプレートを追加してにゃんしぶる

`cowsay`自体はパッケージインストールし、`cowsay`の出力テンプレートを用意することでもにゃんしぶるできる。
`cowsay`はデフォルトで牛のAAでメッセージを表示するが、表示形式はいくつかサンプルがあり、オプションで表示を変えることができる。

[女子エンジニアはcowsayで女子力をupさせよう - Qiita](https://qiita.com/daisukeoda/items/fc2c0c5208697bad1f26)

### cowsay の設定

```
[zaki@manager nyan]$ cowsay Ansible
 _________
< Ansible >
 ---------
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||
```

選択できるテンプレート

```
[zaki@manager nyan]$ cowsay -l
Cow files in /usr/share/cowsay:
beavis.zen blowfish bong bud-frogs bunny cheese cower default dragon
dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters
head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk
moofasa moose mutilated ren satanic sheep skeleton small sodomized
stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle
tux udder vader vader-koala www
[zaki@manager nyan]$ 
```

`tux`を選んでみる

```
[zaki@manager nyan]$ cowsay -f tux Ansible
 _________
< Ansible >
 ---------
   \
    \
        .--.
       |o_o |
       |:_/ |
      //   \ \
     (|     | )
    /'\_   _/`\
    \___)=(___/

[zaki@manager nyan]$
```

じゃあこのテンプレートってどこにあるのかというと、リストの1行目`Cow files in /usr/share/cowsay:`の通り、`/usr/share/cowsay`以下にある。

```
[zaki@manager nyan]$ ls -F /usr/share/cowsay/
DragonAndCow.pm  default.cow            luke-koala.cow    stimpy.cow
Example.pm       dragon-and-cow.cow     mech-and-cow.cow  supermilker.cow
Frogs.pm         dragon.cow             meow.cow          surgery.cow
MechAndCow.pm    elephant-in-snake.cow  milk.cow          telebears.cow
Stegosaurus.pm   elephant.cow           moofasa.cow       three-eyes.cow
TextBalloon.pm   eyes.cow               moose.cow         turkey.cow
TuxStab.pm       flaming-sheep.cow      mutilated.cow     turtle.cow
beavis.zen.cow   ghostbusters.cow       ren.cow           tux.cow
blowfish.cow     head-in.cow            satanic.cow       udder.cow
bong.cow         hellokitty.cow         sheep.cow         vader-koala.cow
bud-frogs.cow    kiss.cow               skeleton.cow      vader.cow
bunny.cow        kitty.cow              small.cow         www.cow
cheese.cow       koala.cow              sodomized.cow
cower.cow        kosh.cow               stegosaurus.cow
```

中身はPerlスクリプトで、`$the_cow`変数に出力するAAをセットしている。

```perl:/usr/share/cowsay/tux.cow
##
## TuX
## (c) pborys@p-soft.silesia.linux.org.pl
##
$the_cow = <<EOC;
   $thoughts
    $thoughts
        .--.
       |o_o |
       |:_/ |
      //   \\ \\
     (|     | )
    /'\\_   _/`\\
    \\___)=(___/

EOC
[zaki@manager nyan]$
```

ということは、あとはわかるな？

```perl:/usr/share/cowsay/cat.cow
##
## A cow wadvertising the World Wide Web, from lim@csua.berkeley.edu
##
$the_cow = <<EOC;
　\\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ
EOC
```

ちなみに、日本語(多バイト文字)を含む場合、UTF-8では文字化けするため、UTF-16 LEで保存してある。

```
[zaki@manager ~]$ cowsay -l
Cow files in /usr/share/cowsay:
beavis.zen blowfish bong bud-frogs bunny cat cheese cower default dragon
dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters
head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk
moofasa moose mutilated ren satanic sheep skeleton small sodomized
stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle
tux udder vader vader-koala www
```

↑`cat`が追加されている。
`cat`を指定して実行。

```
[zaki@manager ~]$ cowsay -f cat Ansible
 _________
< Ansible >
 ---------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ
[zaki@manager ~]$
```

なお、パッケージで配置されるファイルの置き場である`/usr/share/cowsay`に自前のファイルを紛れさせるのが宗教上の理由等により不可能な場合は、`/usr/local/share/cowsay/cat.cow`に保存し、実行時に環境変数`COWPATH`で指定しても動く。(詳細はmanか`cowsay`のソース)

```
[zaki@manager ~]$ COWPATH=/usr/local/share/cowsay cowsay -l
Cow files in /usr/local/share/cowsay:
cat
[zaki@manager ~]$ COWPATH=/usr/local/share/cowsay cowsay -f cat aaa
 _____
< aaa >
 -----
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ
[zaki@manager ~]$
```

複数パス設定

```
[zaki@manager ~]$ COWPATH=/usr/local/share/cowsay:/usr/share/cowsay cowsay -l
Cow files in /usr/local/share/cowsay:
cat
Cow files in /usr/share/cowsay:
beavis.zen blowfish bong bud-frogs bunny cheese cower default dragon
dragon-and-cow elephant elephant-in-snake eyes flaming-sheep ghostbusters
head-in hellokitty kiss kitty koala kosh luke-koala mech-and-cow meow milk
moofasa moose mutilated ren satanic sheep skeleton small sodomized
stegosaurus stimpy supermilker surgery telebears three-eyes turkey turtle
tux udder vader vader-koala www
[zaki@manager ~]$
```

### Ansible の設定

`cowsay`で自前のテンプレートでメッセージ出力する準備ができたので、Ansibleから指定のテンプレートを使う設定を行う。
といっても、`ansible.cfg`に記述するだけ。
(自前のテンプレートが`/usr/local/share/cowsay`などデフォルト以外にある場合は`COWPATH=/usr/local/share/cowsay`が必要)

[ANSIBLE_COW_SELECTION](https://docs.ansible.com/ansible/latest/reference_appendices/config.html#ansible-cow-selection)

```ini:ansible.cfg
[defaults]
cow_selection = cat
```

ちなみにここに`tux`などの、`cowsay -l`で表示されるリストの項目を記述すれば、その内容でAnsibleの出力が行われる。
また、`random`にすれば、使用可能なテンプレートから出力のたびにランダムで一つ選ばれる。

### 実行例

```
[zaki@manager nyan]$ ansible-playbook playbook.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'

 __________________
< PLAY [localhost] >
 ------------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

 ________________________
< TASK [Gathering Facts] >
 ------------------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

ok: [localhost]
 _____________
< TASK [ping] >
 -------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

ok: [localhost]
 ____________
< PLAY RECAP >
 ------------
　\ ∧_∧ 　
 .ﾐ,,・_・ﾐ
ヾ(,_ｕｕﾉ

localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

[zaki@manager nyan]$
```

### cowsay を無効にする

```ini:ansible.cfg
[defaults]
nocows = True
```

あるいは`yum erase cowsay`で削除する。

# callback plugin でにゃんしぶる

ここまでの`cowsay`を使ったにゃんしぶるだと、play名やtask名を出力しているだけなので、正常時はかわいいﾈｺﾁｬﾝ、エラー時は毛が逆立ったﾈｺﾁｬﾝ、みたいなことはできない。

この辺りをイジる場合は、callback plugin(普段使う時も「デフォルトのエラー見づらいわー、あ、でもyamlの`stdout_callback`使うと見やすくなるらしいで」ってアレ)を使えば可能。

「にゃんしぶる用のcallback pluginを自作」することになるが、「何も設定しないときに使用される`default`callback plugin」をベースに作れば、表示をイジる程度であれば、コピペで簡単にできる。

[Ansibleのcallback pluginを使って突然の死(echo-sd)を表現する - Qiita](https://qiita.com/tkit/items/985635f4840492bddf28)

callback pluginはAnsible本体と同じくPython製。
CentOS7でepelからパッケージインストールしたansible 2.8の場合は`/usr/lib/python2.7/site-packages/ansible/plugins/callback/*.py`にプラグインのソースがある。

## にゃんしぶる callback plugin を作る

まずはベースとなる`default.py`を、プレイブックのあるディレクトリに`callback_plugins`ディレクトリを作成し、その下へ`nyansible.py`という名前でコピーする。

```
[zaki@manager nyan]$ mkdir callback_plugins/
[zaki@manager nyan]$ cp /usr/lib/python2.7/site-packages/ansible/plugins/callba
ck/default.py callback_plugins/nyansible.py
[zaki@manager nyan]$ ll callback_plugins/
合計 20
-rw-r--r--. 1 zaki zaki 17488 11月  4 12:36 nyansible.py
[zaki@manager nyan]$
```

### defaultをベースにした最も簡単なコピペコード

default callback pluginの、表示に部分のメッセージを変更してあげればOK.
ただし、日本語(多バイトコード)をそのまま書いてしまうと文字化けしてしまうので、対応が必要。

#### 日本語対応

```
ERROR! Unexpected Exception, this is probably a bug: Non-ASCII character '\xce' in file /home/zaki/work/ansible/nyan/callback_plugins/nyansible.py on line 89, but no encoding declared; see http://www.python.org/peps/pep-0263.html for details (nyansible.py, line 89)
```

スクリプト内に日本語を使用する場合、スクリプトの1行目(普段のPythonスクリプトでshebang(`#!/usr/bin/env python3`とか書くアレ)が1行目にある場合は2行目)にエンコードを指定する。
UTF-8であれば

```python
# -*- coding: utf-8 -*-
```

と書く。

```
 [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin
(<ansible.plugins.callback.nyansible.CallbackModule object at 0x7fda2cacc410>):
'ascii' codec can't decode byte 0xe0 in position 0: ordinal not in range(128)
```

また、日本語を使用する各箇所でUnicode指定を表す`u" ... "`という記述が必要。(`ok`や`changed`のメッセージ出力は`u`がないため、付け加える)

```python
self._display.display("fatal: [%s]: FAILED! => %s" ...
```

を

```python
self._display.display(u"エラー: [%s]: FAILED! => %s" ...
```

と書く。

#### 変更点サンプル

```diff
[zaki@manager nyan]$ diff -u /usr/lib/python2.7/site-packages/ansible/plugins/c
allback/default.py callback_plugins/nyansible.py
--- /usr/lib/python2.7/site-packages/ansible/plugins/callback/default.py       2019-09-13 06:12:55.000000000 +0900
+++ callback_plugins/nyansible.py       2019-11-04 12:59:50.520091324 +0900
@@ -1,3 +1,4 @@
+# -*- coding: utf-8 -*-
 # (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com>
 # (c) 2017 Ansible Project
 # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
@@ -48,7 +49,7 @@

     CALLBACK_VERSION = 2.0
     CALLBACK_TYPE = 'stdout'
-    CALLBACK_NAME = 'default'
+    CALLBACK_NAME = 'nyansible'

     def __init__(self):

@@ -86,11 +87,11 @@

         else:
             if delegated_vars:
-                self._display.display("fatal: [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'],
+                self._display.display(u"Σ(;Φ ω Φ): [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'],
                                                                             self._dump_results(result._result)),
                                       color=C.COLOR_ERROR, stderr=self.display_failed_stderr)
             else:
-                self._display.display("fatal: [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)),
+                self._display.display(u"Σ(;Φ ω Φ): [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)),
                                       color=C.COLOR_ERROR, stderr=self.display_failed_stderr)

         if ignore_errors:
@@ -107,9 +108,9 @@
                 self._print_task_banner(result._task)

             if delegated_vars:
-                msg = "changed: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
+                msg = u"ฅ/ᐠ｡ᆽ｡ᐟ \: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
             else:
-                msg = "changed: [%s]" % result._host.get_name()
+                msg = u"ฅ/ᐠ｡ᆽ｡ᐟ \: [%s]" % result._host.get_name()
             color = C.COLOR_CHANGED
         else:
             if not self.display_ok_hosts:
@@ -119,9 +120,9 @@
                 self._print_task_banner(result._task)

             if delegated_vars:
-                msg = "ok: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
+                msg = u"ฅ(＾・ω・＾ฅ): [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
             else:
-                msg = "ok: [%s]" % result._host.get_name()
+                msg = u"ฅ(＾・ω・＾ฅ): [%s]" % result._host.get_name()
             color = C.COLOR_OK

         self._handle_warnings(result._result)
[zaki@manager nyan]$
```

#### 実行例

```yaml:playbook.yml
---
- hosts: localhost
  tasks:
    - ping:
    - shell: date
    - yum:
        name: ruby
```

```
[zaki@manager nyan]$ ansible-playbook playbook.yml  
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ฅ(＾・ω・＾ฅ): [localhost]

TASK [ping] ********************************************************************
ฅ(＾・ω・＾ฅ): [localhost]

TASK [shell] *******************************************************************
ฅ/ᐠ｡ᆽ｡ᐟ \: [localhost]

TASK [yum] *********************************************************************
Σ(;Φ ω Φ): [localhost]: FAILED! => {"changed": false, "changes": {"installed": ["ruby"]}, "msg": "You need to be root to perform this command.\n", "rc": 1, "results": ["Loaded plugins: fastestmirror\n"]}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

[zaki@manager nyan]$
```

### エラーが見やすいyamlプラグインを継承したにゃんしぶる

defaultプラグインでなく、エラー時のメッセージをyaml形式で出力してくれるyamlプラグインをベースに作ってみる。
yamlプラグインは`/usr/lib/python2.7/site-packages/ansible/plugins/ca
llback/yaml.py`にソースがある。ただしこのソースを見ても、okやchanged・fatalを出力しているコードがない。
じゃあどうなっているのかというと、Ansibleのcallback pluginは`CallbackModule`クラスを作るようになっていて、yamlプラグインはdefaultプラグインを継承して作られている。
つまり、基本的な処理はすべてdefaultプラグインの処理をそのまま使用し、出力処理のみ`yaml.dump()`を使うような実装になっている。

```python:/usr/lib/python2.7/site-packages/ansible/plugins/callback/yaml.py
        if abridged_result:
            dumped += '\n'
            dumped += to_text(yaml.dump(abridged_result, allow_unicode=True, width=1000, Dumper=AnsibleDumper, default_flow_style=False))
```

というわけなので、yamlプラグインを継承し、ok/changed/fatalなどを出力する処理をオーバーライドするような実装にしてやればOK

#### yamlプラグインのクラスを継承したにゃんしぶる用CallbackModuleクラスを作成する

まずは自前の処理を何も実装していない、yamlプラグインをそのまま継承しただけのクラスを作成。
こんな感じ。

名前は`nyaml`にしました。

```python:callback_plugins/nyaml.py
# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    callback: nyaml
    type: stdout
    short_description: nyaml-ized Ansible screen output
'''

from ansible.plugins.callback.yaml import CallbackModule as CallbackModule_yaml

class CallbackModule(CallbackModule_yaml):

    """
    nyansible and nyaml /ᐠ｡ꞈ｡ᐟ\
    """

    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'yaml'
```

※ この時点で`ansible.cfg`に`stdout_callback = nyaml`と書けば(ただのyamlプラグインと同じように)動く。

#### メッセージ出力処理を実装する

defaultプラグインのソースからメッセージ出力処理の関数を拝借してくる。
対象は`v2_runner_on_ok()`や`v2_runner_on_failed()`など。
これらの関数をまずはコピペして動かしてみる。

すると、いくつか定義が足りないとwarningが出力される。

```
TASK [Gathering Facts] ********************************************************* [WARNING]: Failure using method (v2_runner_on_ok) in callback plugin
(<ansible.plugins.callback.nyaml.CallbackModule object at 0x7fc66f796c50>):
global name 'TaskInclude' is not defined


TASK [ping] ********************************************************************
TASK [shell] *******************************************************************
TASK [yum] ********************************************************************* [WARNING]: Failure using method (v2_runner_on_failed) in callback plugin
(<ansible.plugins.callback.nyaml.CallbackModule object at 0x7fc66f796c50>):
global name 'C' is not defined
```

`TaskInclude`や`C`が無いと言われている。

ので、これをdefaultプラグインのソースからさらに拝借する。(基本的にこの作業の繰り返し)
というわけで、全体のソースはこんな感じ。

```python:callback_plugins/nyaml.py
# -*- coding: utf-8 -*-

from __future__ import (absolute_import, division, print_function)
__metaclass__ = type

DOCUMENTATION = '''
    callback: nyaml
    type: stdout
    short_description: nyaml-ized Ansible screen output
'''

from ansible.plugins.callback.yaml import CallbackModule as CallbackModule_yaml
from ansible import constants as C
from ansible.playbook.task_include import TaskInclude
from ansible.utils.color import colorize, hostcolor

class CallbackModule(CallbackModule_yaml):

    """
    nyansible and nyaml /ᐠ｡ꞈ｡ᐟ\
    """

    CALLBACK_VERSION = 2.0
    CALLBACK_TYPE = 'stdout'
    CALLBACK_NAME = 'yaml'

    def v2_runner_on_failed(self, result, ignore_errors=False):

        delegated_vars = result._result.get('_ansible_delegated_vars', None)
        self._clean_results(result._result, result._task.action)

        if self._last_task_banner != result._task._uuid:
            self._print_task_banner(result._task)

        self._handle_exception(result._result, use_stderr=self.display_failed_stderr)
        self._handle_warnings(result._result)

        if result._task.loop and 'results' in result._result:
            self._process_items(result)

        else:
            if delegated_vars:
                self._display.display(u"Σ(;Φ ω Φ): [%s -> %s]: FAILED! => %s" % (result._host.get_name(), delegated_vars['ansible_host'],
                                                                            self._dump_results(result._result)),
                                      color=C.COLOR_ERROR, stderr=self.display_failed_stderr)
            else:
                self._display.display(u"Σ(;Φ ω Φ): [%s]: FAILED! => %s" % (result._host.get_name(), self._dump_results(result._result)),
                                      color=C.COLOR_ERROR, stderr=self.display_failed_stderr)

        if ignore_errors:
            self._display.display("...ignoring", color=C.COLOR_SKIP)

    def v2_runner_on_ok(self, result):

        delegated_vars = result._result.get('_ansible_delegated_vars', None)

        if isinstance(result._task, TaskInclude):
            return
        elif result._result.get('changed', False):
            if self._last_task_banner != result._task._uuid:
                self._print_task_banner(result._task)

            if delegated_vars:
                msg = u"ฅ/ᐠ｡ᆽ｡ᐟ\: [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = u"ฅ/ᐠ｡ᆽ｡ᐟ\: [%s]" % result._host.get_name()
            color = C.COLOR_CHANGED
        else:
            if not self.display_ok_hosts:
                return

            if self._last_task_banner != result._task._uuid:
                self._print_task_banner(result._task)

            if delegated_vars:
                msg = u"ฅ(＾・ω・＾ฅ): [%s -> %s]" % (result._host.get_name(), delegated_vars['ansible_host'])
            else:
                msg = u"ฅ(＾・ω・＾ฅ): [%s]" % result._host.get_name()
            color = C.COLOR_OK

        self._handle_warnings(result._result)

        if result._task.loop and 'results' in result._result:
            self._process_items(result)
        else:
            self._clean_results(result._result, result._task.action)

            if (self._display.verbosity > 0 or '_ansible_verbose_always' in result._result) and '_ansible_verbose_override' not in result._result:
                msg += " => %s" % (self._dump_results(result._result),)
            self._display.display(msg, color=color)
```

#### nyamlプラグインでにゃんしぶる

このnyamlプラグインを使って`ansible-playbook`を実行すると

```
[zaki@manager nyan]$ ansible-playbook playbook.yml  
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [localhost] ***************************************************************

TASK [Gathering Facts] *********************************************************
ฅ(＾・ω・＾ฅ): [localhost]

TASK [ping] ********************************************************************
ฅ(＾・ω・＾ฅ): [localhost]

TASK [shell] *******************************************************************
ฅ/ᐠ｡ᆽ｡ᐟ\: [localhost]

TASK [yum] *********************************************************************
Σ(;Φ ω Φ): [localhost]: FAILED! => changed=false 
  changes:
    installed:
    - ruby
  msg: |-
    You need to be root to perform this command.
  rc: 1
  results:
  - |-
    Loaded plugins: fastestmirror

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=1    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

[zaki@manager nyan]$
```

---

参考資料

- ねこのAA
    - [猫 | ねこ - 可愛い顔文字 ｜ 顔文字まとめサイト](https://cutekaomoji.com/jp/)
    - [2chのかわいいAA/顔文字まとめ: 10/10更新 猫](https://2ch-aa.blogspot.com/2017/10/1010.html)

---

ちなみに筆者はアイコンからもわかるかと思いますが、うさぎが好きです🐰