これはアルファ時点の古い情報です。Ansible 2.0.0正式版では、旧スタイルのモジュールも動く様に対応されています。
ただし、これから作る自作モジュールのスタイルとしてはnew
, non_native_want_json
が推奨される点は変わりませんので、記事は残しておきます。
Ansible 2.0.0のalpha版が出たので、そろそろ本腰入れて使ってみようと思ったら、bashスクリプトで書いた自作モジュールが動かなくなっていたので、対応方法のメモ。
自作モジュール: parrot
ここでは、以下の様な与えられた引数をオウム返しするだけのparrot
と言うカスタム・モジュールを例にして話を進めます。
(ちなみに、このまま使うと値にスペースが入る場合には正しく動かないです)
#!/bin/bash
# 引数ファイルが与えられなかったらエラー
if [ -z $1 ]; then
echo "{\"failed\": true, \"msg\": \"no arguments provided.\"}"
exit 1
fi
result="{\"changed\": false"
# 引数をkey, valueに分解してresultに突っ込む
for kv in $(cat $1); do
echo $kv
escaped=$(echo $kv | sed -e 's/"/\\\"/g')
key=${escaped%%=*}
value=${escaped#*=}
if [ $key ]; then
result+=", \"$key\": \"$value\""
fi
done
result+="}"
echo $result
exit 0
現象
1.9.2では動く
このparrot
モジュールをAnsible 1.9.2で実行すると、下の様に入力した引数がそのまま実行結果として出力されます。
$ ansible localhost -m parrot -a 'foo=bar hoge=fuga'
localhost | success >> {
"changed": false,
"foo": "bar",
"hoge": "fuga"
}
2.0.0だと動かない
ところが、Ansible 2.0.0で全く同じ様に実行しても、引数が与えられていないと怒られてしまいした!
ansible localhost -m parrot -a 'hoge=fuga foo=bar'
localhost | FAILED! => {
"changed": false,
"failed": true,
"msg": "no arguments provided."
}
エラーの原因
これは一体どうしたことだろうと、-vvv
を付けて、実行されているコマンドを確認してみると…
# 1.9.2の場合
<localhost> EXEC ['/bin/sh', '-c', u'LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /bin/bash /tmp/ansible-tmp-1441005841.0-58446508593101/parrot /tmp/ansible-tmp-1441005841.0-58446508593101/arguments; rm -rf /tmp/ansible-tmp-1441005841.0-58446508593101/ >/dev/null 2>&1']
# 2.0.0の場合
localhost EXEC LANG=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /bin/bash /tmp/ansible-tmp-1441005817.19-202560708428255/parrot; rm -rf "/tmp/ansible-tmp-1441005817.19-202560708428255/" >/dev/null 2>&1
上の実行結果をよく見てみると、1.9.2ではparrot
への第一引数としてarguments
と言うファイルが渡されているのに対して、2.0.0ではparrot
が引数なしで実行されているのがわかります。
parrot
内では第一引数で渡されたパスのファイルを読みに行ってansible引数を取得しているので、argumentsファイルが存在しないと当然エラーになってしまいます。
ファイル経由の引数渡しはv2では使えない!
では、なぜv2ではarguments
ファイルが渡されていないのでしょうか?
実はAnsible v1のモジュールには3つの異なるスタイルが存在し、それぞれ異なる形式で引数情報が渡されています。
Ansibleモジュールの3つのスタイル
- old
- 1.4未満で使われていた形式
- モジュールへの引数はスペース区切りの
arguments
ファイルで渡される- e.g.
hoge=fuga foo=bar
- e.g.
- スペース入りの値などの扱いが困難
-
parrot
モジュールはこの形式
- non_native_want_json
-
old
と同様に引数はarguments
ファイル経由で渡される -
old
と違って、arguments
ファイルの中身はJSON- e.g.
{"foo": "bar", "hoge": "fuga"}
- e.g.
- モジュール内に
WANT_JSON
と書かれている場合にこのスタイルになる。
-
- new
- 1.4以降の組み込みモジュールで使われている形式
- 変数系の情報は下で説明する仕様でモジュールファイル内に展開される
- 引数用の外部ファイルは使われない。
- モジュール内に
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
と書かれている場合にこのスタイルになる。
ソースを確認したところ、v2ではarguments
ファイル経由での引数渡しが使えなくなっているようです。
具体的な後方互換ポリシーについては未確認ですが、一度削除されたファイル経由での引数渡しが今後v2で再実装される見込みは薄いでしょう。
v1でのモジュール内文字列展開の仕様
-
"<<ANSIBLE_VERSION>>"
: モジュールを実行するAnsibleのバージョン文字列 -
"<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
: モジュールに与えられた名前なし引数文字列 -
"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
: モジュールに与えられた名前付き引数のpythonコード用JSON文字列(reprを使っている) -
<<SELINUX_SPECIAL_FILESYSTEMS>>
: SELinux情報
v2でのモジュール内文字列展開の仕様変更点
なお、v2では展開の仕様にもいくつか変更点がありました。
- 追加された展開パターン
-
<<INCLUDE_ANSIBLE_MODULE_WINDOWS_ARGS>>
- PowerShell用の名前付き引数のJSON文字列
-
"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
との違いは、引用符が含まれないだけ -
"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
はpythonコード用だが、こちらは単純にダンプされたJSON
-
- 削除された展開パターン
-
"<<INCLUDE_ANSIBLE_MODULE_ARGS>>"
- そもそもv2では
command
系などの一部の組み込みモジュール以外では、名前なし引数の利用が許可されていません
- そもそもv2では
<<SELINUX_SPECIAL_FILESYSTEMS>>
-
自作モジュールを新方式に対応させる
ということで、parrot
をv2でも使えるようにするには1ファイル内で引数を受け取れるようにモジュールを修正するしかなさそうです。
モジュール内の文字列展開はバージョン1.4以降であればv1でも使えますので、v1/v2間での互換性についてはそんなに気にする必要はなさそうです。
嘘でした。
"<<INCLUDE_ANSIBLE_MODULE_COMPLEX_ARGS>>"
はpython内でそのまま使えるようにrepr
を使っているので、{"foo": "bar\"baz"}
の様なJSONの場合、'{"foo": "bar\\"baz"}'
の様にエスケープされてしまいます。
そのため、v2用には<<INCLUDE_ANSIBLE_MODULE_WINDOWS_ARGS>>
を使い、v1用にnon_native_want_json
を使う、と言う対応が必要になりそうです。
Windows用じゃないモジュールに<<INCLUDE_ANSIBLE_MODULE_WINDOWS_ARGS>>
を入れるというのは気持ちが悪いけれど…
2015/09/11時点のdevelブランチ最新では代わりに<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>
という表記も可能になっています。
修正されたparrot
という訳で、修正後のparrot
はこんな感じになりました。
#!/bin/bash
# WANT_JSON
ANSIBLE_VERSION="<<ANSIBLE_VERSION>>"
# jqがなかったらエラー
if ! type jq >/dev/null 2>&1; then
echo "{\"failed\": true, \"msg\": \"jq not found.\"}"
exit 1
fi
# v1の場合は$1に引数ファイルのパスが入る
if [[ $ANSIBLE_VERSION == 1.* ]]; then
MODULE_COMPLEX_ARGS=$(cat $1)
# v2の場合はINCLUDE_ANSIBLE_MODULE_JSON_ARGSを使う
else
MODULE_COMPLEX_ARGS=$(cat << 'EOF'
<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>
EOF
)
fi
echo $MODULE_COMPLEX_ARGS | jq '.changed=false'
exit 0
JSONの操作については、jqを使っちゃってます。
自分の場合、Ansibleモジュールをわざわざbashで書きたいのって、CoreOSの操作くらいなんですが、CoreOSには幸いjq
がインストールされているので楽しました。
ポイントとしては
-
WANT_JSON
コメントでnon_native_want_json
スタイルを適用 -
ANSIBLE_VERSION
の値によって、引数の取得方法を場合分け
と言ったあたりでしょうか。
まとめ
今回はbashスクリプトを例にとって自作モジュールのv2対応法を試してみましたが、このやり方は組み込みでヘルパーが用意されているPythonやPowerShell以外でモジュールを作る場合にはそのまま適用可能なものです。
v2への移行は時期尚早と言う場合でも、WANT_JSON
コメントを使って引数をJSONとして取得する方法は、JSONパーサーが使える言語/環境であれば是非とも導入すべきかと思います。
参考
https://gist.github.com/mildred/9ce28b926b567548037d (モジュール・スタイルごとの挙動の違いについて)
追記(2015/09/11)
<<INCLUDE_ANSIBLE_MODULE_WINDOWS_ARGS>>
がイケていなかったので、<<INCLUDE_ANSIBLE_MODULE_JSON_ARGS>>
と言うリプレイサーを足したら、取り込んでくれました。
https://github.com/ansible/ansible/pull/12267