こんにちは、cloudpackSebastian です。

はじめに

Ansibleのmoduleは基本的にはpythonで記述を行います。
実はmodule自体は所定の形式でレスポンスを返せばpythopnで無くても記載は可能なのですが、公式が pythonで書かれている事と、ansibleがpythonの使用を前提に記載されている事からpythonを用いないと公式にcommitは出来ません。
また、pythonの方が相性は良いので今回はそのままpythonで書きます。

moduleが実行されるまで

ansibleのmoduleを書く前に、まずansibleのmoduleがどのように実行されるのかを知らねば話になりません。
そういう訳でmoduleがどの様に実行されていくのかを追いかけてみます。

ansibleの導入後、サーバーに対してpingモジュールを用いてremote serverへの疎通応答を求めて見ます。
-vvvvはデバッグ出力用のオプションです。

ansible hogehoge -m ping -vvvv

するとレスポンスが以下の様に返ります。

 ESTABLISH CONNECTION FOR USER: huga
 REMOTE_MODULE ping
 EXEC ['ssh', '-C', '-tt', '-vvv', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=60s', '-o', 'ControlPath=/Users/foo/.ansible/cp/ansible-ssh-%h-%p-%r', '-o', 'StrictHostKeyChecking=no', '-o', 'Port=22', '-o', 'IdentityFile="/etc/ansible/key/bar.pem"', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'User=huga', '-o', 'ConnectTimeout=10', 'xxx.xxx.xxx.xxx', "/bin/sh -c 'mkdir -p $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && chmod a+rx $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && echo $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901'"]
 PUT /var/folders/0x/g4lkrc4528x_rqpzr47d2k0r0000gp/T/tmpxqYhf1 TO /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/ping
 EXEC ['ssh', '-C', '-tt', '-vvv', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=60s', '-o', 'ControlPath=/Users/foo/.ansible/cp/ansible-ssh-%h-%p-%r', '-o', 'StrictHostKeyChecking=no', '-o', 'Port=22', '-o', 'IdentityFile="/etc/ansible/key/bar.pem"', '-o', 'KbdInteractiveAuthentication=no', '-o', 'PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'User=huga', '-o', 'ConnectTimeout=10', 'xxx.xxx.xxx.xxx', u"/bin/sh -c 'LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/ping; rm -rf /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/ >/dev/null 2>&1'"]
hogehuga | success >> {
    "changed": false,
    "ping": "pong"
}

1行目の ESTABLISH CONNECTION FOR USER: hugaはxxx.xxx.xxx.xxxに接続しhugaユーザーにてログインを行う事を明示しています。
2行目の REMOTE_MODULE pingはpingモジュールを用いる事を明示しています。

問題は3行目以降です。

 EXEC ['ssh', '-C', '-tt', '-vvv', '-o', 'ControlMaster=auto', '-o', 'ControlPersist=60s', '-o', 'ControlPath=/Users/foo/.ansible/cp/ansible-ssh-%h-%p-%r','-o', 'StrictHostKeyChecking=no', '-o', 'Port=22', '-o', 'IdentityFile="/etc/ansible/key/bar.pem"', '-o', 'KbdInteractiveAuthentication=no', '-o','PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey', '-o', 'PasswordAuthentication=no', '-o', 'User=huga', '-o', 'ConnectTimeout=10','xxx.xxx.xxx.xxx', "/bin/sh -c 'mkdir -p $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && chmod a+rx $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && echo $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901'"]

EXECと書いてあるからには何らかのexecuteだと判断出来ます。
[]で囲まれている範囲を見てみるとsshにて何らかの実行を行っています。

見やすく整えると以下のようになります。

ssh
  -C
  -tt
  -vvv
  -o ControlMaster=auto
  -o ControlPersist=60s
  -o ControlPath=/Users/foo/.ansible/cp/ansible-ssh-%h-%p-%r
  -o StrictHostKeyChecking=no
  -o Port=22
  -o IdentityFile="/etc/ansible/key/bar.pem"
  -o KbdInteractiveAuthentication=no
  -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey
  -o PasswordAuthentication=no
  -o User=huga
  -o ConnectTimeout=10
  xxx.xxx.xxx.xxx
  /bin/sh -c 'mkdir -p $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && chmod a+rx $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901 && echo $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901'

幾つかのsshのオプションが並び最後に/bin/shがあります。
つまりssh経由でリモートホストに対してcommandを投げている訳です。
内容は以下の様になります。

mkdir -p $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901
chmod a+rx $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901
echo $HOME/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901

見るとユーザーホーム以下に一時directoryを作成しています。

4行目は PUT /var/folders/0x/g4lkrc4528x_rqpzr47d2k0r0000gp/T/tmpxqYhf1 TO /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/pingとなっています。
PUTと在るようにremote server内の先ほど作成した一時directory内にpingをputしています。

ここまでで分かると思いますが、ansibleのmoduleはremote serverにmodule自身が転送されてremote server上で実行されます。

実際に5行目にはEXECの指定で実行commandが記載されています。

u"/bin/sh -c 'LANG=en_US.UTF-8 LC_CTYPE=en_US.UTF-8 /usr/bin/python /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/ping; rm -rf /home/huga/.ansible/tmp/ansible-tmp-1419504192.56-262299774391901/ >/dev/null 2>&1'"

内容を追うと、言語環境が英語環境で文字コードがutf-8にてpythonを用いて先ほどのpingを実行しています。
また、その後rmにてpingファイルの削除が実行されています。

最後にjsonにて文字列が返って来ています。

hogehuga | success >> {
    "changed": false,
    "ping": "pong"
}

これはansibleのmoduleが吐き出すレスポンスの内容です。

  • ansibleはmoduleをremote serverに転送して実行を行う
  • 転送先は一時directoryとして日付付きで作成される
  • us-US.UTF-8が指定されたpythonにて実行される
  • moduleは実行後、速やかに削除される
  • moduleのresponseはjsonで返却される
    ※ この為、ansibleはsimple json moduleが必須になる
補足

当然ながらこれらの挙動はansibleの設定次第で変更されるものです。

転送先の一時directoryはansible.cfgに定義されるremote_tmpによります。
EXECにて用いられるshellはexecute設定で変更されるのでzshやcshに変更する事も可能でしょう。
inventoryのhost指定でansible_*_interpreterの指定を行えばpythonでは無くruby等の実行も可能ですが基本はpythonで行うことが好ましいです。

ansible module作る

tutorial

では、ansible module実際に作ってみます。
ansibleのTutorialを見るとexampleが書いてあります。

#!/usr/bin/python

import datetime
import json

date = str(datetime.datetime.now())
print json.dumps({
    "time" : date
})

はい、かなりシンプルですね。
jsonを用いてdateを返すだけのmoduleです。
実行してみると以下の様に返ります。

ansible hogehuga -m time -v
hogehuga | success >> {
    "time": "2014-12-25 12:02:59.370913"
}

ただのpythonファイルなので単純に実行してみても構いません

chmod +x  time
./time
{"time": "2014-12-25 21:04:14.466017"}

ansible_module class library

tutorialではjsonを自分で記載していましたが、実は最初からansible用のlibraryが用意されています。
このlibraryを用いると引数を処理したりjsonにてresponseを返したりはそのまま行えます。

試しに、必須引数messageを指定すると指定した引数messageの内容をそのまま返却するだけのmoduleを作成してみます。

#!/usr/bin/python
# -*- coding: utf-8 -*-
from ansible.module_utils.basic import *
DOCUMENTATION = '''
---
module: echo
short_description: This is a sample echo module
description :
   - This module is module which just returns the message just as it is.
version_added: "1.0"
options:
  message:
    description:
      - echo message.
    required: true
    default: null
'''
EXAMPLES = '''
- action: echo message=arg1
'''
###############################################################################
#
# ansible module main function
#
###############################################################################
def main():
    module = AnsibleModule(
        argument_spec=dict(
                message=dict(required=True) ,
        ) ,
        supports_check_mode=True
    )
    try :
        #
        # 受け取ったパラメータのチェック
        msg_param = module.params['message']
        if module.check_mode:
            module.exit_json(changed=False , msg=msg_param , mode='check mode')
        else :
            module.exit_json(changed=False , msg=msg_param)
    except Exception as e:
        module.fail_json(msg=str(e))

###############################################################################
#
# call main
#
###############################################################################
main()

はい、こんな感じです。
これをechoとして保存しmoduleとして実行してみます。

ansible hogehuga -m echo -a "message='hello ansible world'"
hogehuga | success >> {
    "changed": false,
    "msg": "hello ansible world"
}
引数の処理 arguments_spec

内部でinstance化しているClass AnsibleModuleがansibleのmoduleを記載する為に用いるclassになります。

    module = AnsibleModule(
        argument_spec=dict(
                message=dict(required=True) ,
        )
    )

arguments_specにてdictにて指定されているmessageが引数名を表しています。
更に、messageにイコールで指定されるdictは引数messageに関しての詳細を示しています。
ここではrequiredTrueが指定されているため、必須引数である旨を定義しています。
以下に引数の制御に用いれる内容の一覧を示します。

指定内容 意味
require 必須指定 required=True
default 指定引数が無指定の場合に用いる値を指定します default=None
choices 配列にて指定して指定配列の中から値を指定させる様に指定します choices=[‘start’ . ‘stop’ , ‘restart’]
type 引数の型を指定します type=’int’
aliases 引数名のエイリアスを作成します aliases[‘name’]
responseの処理

AnsibleModule classはレスポンスはdict指定で連想配列を渡すと自動的にjsonで返してくれます。
echoスクリプトで指定されているmodule.exit_json(changed=False , msg=msg_param)が実際にresponseを返している行です。
changedはmoduleが実行された結果、変更されたか否かの指定を行います。
特に何も指定しない場合はFalseが返されるようになっています。

ただし、何らかの要因により処理に失敗した場合は別です。
exit_jsonは出力結果をsuccessとして返すため、failの際は別途処理が必用です。
echoスクリプト内ではmodule.fail_json(msg=str(e))がfailでの返却を行うmethodの指定です。

check modeとsupports_check_mode

実はansibleにはcheck mode、つまりdry runモードがあります。
しかし、module側でcheck modeに対応を行い、check modeが指定された際にcheckのみを行い、変更を行わない処理の実装を行わなければなりません。
check_modeに対応するためにはまず、AnsibleModuleの引数にてsupports_check_mode=Trueの指定を行うつ様はあります。

Falseが指定されていたり指定がない場合は以下の様に返ります。

ansible hogehuga -m echo -a "message='hello ansible world'" --check
hogehuga | skipped

また、実際にsupports_check_mode=Trueの指定があってもそのまま実行しては変更されてしまいます。
よって、check modeに対応する際はAnsibleModuleのcheck_mode methodにて実行時にcheck modeであるかの確認を行います。
テスト用のスクリプトではcheck modeであった場合はresponseのmodeに「check mode」と返る様にしています。

よって、check modeで実行すると以下の様に返ります。

ansible hogehuga -m echo -a "message='hello ansible world'" --check
hogehuga | success >> {
    "changed": false,
    "mode": "check mode",
    "msg": "hello ansible world"
}
documentation

ansibleにはansible-docと言うcommandがあります。
このcommandを用いるとansibleでは各moduleのdocumentが表示されるようになっています。
折角なのでdocumentも書いておきます。
因みに、ここでmoduleにdocumentが書かれていないと漏れ無く、ansible-docでエラーが吐かれます。
ansibleのdocumentationはそのままpythonのご作法に従って書かれているので
DOCUMENTATION = ''' 〜〜〜〜 '''で書けばOKです。
注意点としてはansible上でのpythonの実行環境はあくまで英語環境のutf-8が指定されているので日本語での指定はNGです。

DOCUMENTATION = '''
---
module: echo
short_description: This is a sample echo module
description :
   - This module is module which just returns the message just as it is.
version_added: "1.0"
options:
  message:
    description:
      - echo message.
    required: true
    default: null
'''

以上の様に記載しておき、ansible-docにて表示を行うと整形されたdocumentが出力されます。

ansible-doc echo
> ECHO

  This module is module which just returns the message just as it is.

Options (= is mandatory):

= message
        echo message. [Default: None]

- action: echo message=arg1

元記事はこちらです。
ansibleのmoduleの作成