Ansible Advent Calendar 2017 4日目が空いていたので入れます。(これを書いているときは11日目です)
ソースコードをいじらずにAnsibleの出力結果を加工する
Ansible Advent Calendar 2017 10日目の 我が家のAnsibleは突然の死にあこがれるを見て思いついたので書きます。
上記記事では、ゴリゴリにソースコードをいじっていますが、出力を加工するのであれば、Callback pluginsを使うのが普通なんじゃないかなと思っています(ソースコードを直接変えるのは怖いですよね)。
上記の公式ページでは物凄くさらっと「callback pluginを使って出力を加工できるよ」とあるのですが、要はサンプルを見ろということが書いてあって、一体何をやっているのかろくな説明もなくて分からず、かつ世の中的にもあまりcallback pluginの紹介と書き方の説明がなさそうなの紹介します。
callback pluginとは
上記の通りなのですが、要はAnsibleのイベント(開始や実行結果など)を受けて呼び出されるpluginを指定できます。
上記リンクではすごく簡単な書き方が紹介されていますが、実際には「イベント」はめちゃくちゃ一杯あります。詳細はソースを見たほうが早いです。
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に作り方が載っているのですが、最初に書いた通り要は「サンプルを見ろ」という感じで、大分辛いものがあります…。
サンプルはansible/lib/ansible/plugins/callback/にあります。その中でも、default.pyを見るのが最もよいでしょう(というかそれ以外を散々調べていて、あとでこれを見つけてこれにすればよかったと後悔しました)。
作ったプラグインは、自分の動かしたいplaybookの直下にcallback_plugins
ディレクトリを作って、そこに置きます。他にもrole
の配下とかでもよいです。詳しくは以下をご覧ください。
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/
配下に置き、実行権を与えてください)
# 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
コマンドを呼び出しているだけです。
公式では、インポート対象を以下のようにしていますが、こうすると関数を定義したイベント以外は全て何も表示されなくなります。
from ansible.plugins.callback import CallbackBase
class CallbackModule(CallbackBase):
最初から全部作る場合はこれでいいと思いますが、既存の出力から一部だけ加工したい場合は以下のようにdefault
をimportするとよいでしょう。
from ansible.plugins.callback.default import CallbackModule as CallbackModule_default
class CallbackModule(CallbackModule_default):
callback pluginを使う
単にプラグインをcallback_plugins
配下に置けば使われるのではなく、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
このように、いい感じで突然の死が訪れました。
死に方が足りない
まだ死に方が足りませんか?そんな時は、「イベント」をそれぞれ足していけばいいです。
def v2_playbook_on_start(self, playbook):
from os.path import basename
self.print_echo_sd(basename(playbook._file_name) + ' playbook!')
これで
PLAYBOOK: local.yml ************************************************************************************************************************
これが(ansible-playbook -vv
以上でないと表示されません)
_人人人人人人人人人人人_
> local.yml playbook! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
こうなります。
次に
def v2_playbook_on_play_start(self, play):
self.print_echo_sd(play.get_name() + ' hosts!!')
これで
PLAY [echo-sd-servers] *********************************************************************************************************************
これが
_人人人人人人人人人人人人人_
> echo-sd-servers hosts!! <
 ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^ ̄
こうなります。
最後に
def v2_playbook_on_task_start(self, task, is_conditional):
self.print_echo_sd(task.get_name() + ' start!!!')
これで
TASK [debug] *******************************************************************************************************************************
これが
_人人人人人人人人人_
> 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の実行結果を加工して別の処理に渡したい場合などに使えそうです。デフォルトだと「*****************
」みたいなのを外して加工するのは大変そうですし(そもそもナンセンスでしょうし)。