初めてのAnsible（10章：カスタムモジュール）


[「初めてのAnsible」](https://www.oreilly.co.jp/books/9784873117652/)を読み進めながらのメモ

前掲
[初めてのAnsible（1章：イントロダクション）](http://qiita.com/c0tt0n-candy/items/512049586f5a1f1279f3)
[初めてのAnsible（2章：Playbook:始めてみよう）](http://qiita.com/c0tt0n-candy/items/36b4eb2717447071ad21)
[初めてのAnsible（3章：インベントリ:サーバーの記述）](http://qiita.com/c0tt0n-candy/items/1b109c53d8709717a1bb)
[初めてのAnsible（4章：変数とファクト）](http://qiita.com/c0tt0n-candy/items/7144d49636a30433365c)
[初めてのAnsible（7章：複雑なPlaybook）](http://qiita.com/c0tt0n-candy/items/9e9ea1f745bd904e4a5e)
[初めてのAnsible（8章：ロール:プレイブックのスケールアップ）](http://qiita.com/c0tt0n-candy/items/d94131e481fa5f0e4db2)
[初めてのAnsible（9章：Ansibleの高速化）](http://qiita.com/c0tt0n-candy/items/83dfbb158f4520bba7e2)


必要なら自分でモジュールかいてプルリクエストなげてみたらって章

* Ansibleモジュール群のGitHub
	* [ansible/ansible-modules-core](https://github.com/ansible/ansible-modules-core)
	* [ansible/ansible-modules-extras](https://github.com/ansible/ansible-modules-extras)

# カスタムモジュール

Ansibleのモジュールを記述する手段として2通りを挙げている

* Pythonで記述する場合
* Python以外（bashやRubyなど）で記述する場合

AnsibleがPythonでモジュールを記述するためのヘルパークラスを提供していることから、Pythonで記述することを推奨

ここでも詳細はPythonモジュールに関してのみまとめる

## カスタムモジュールの置き場所

playbookが `playbooks/playbook` に置かれていれば
カスタムモジュールは `playbooks/library/custom_module` に置く


# モジュール呼び出し時の挙動

Ansibleがモジュールを呼び出す際には以下のような挙動をとる

1. (Pythonモジュールの場合} スタンドアロンのPythonスクリプトを引数付きで生成
	* ヘルパークラスのコードや引数などを埋め込み1つにまとめたPythonスクリプトを生成

2. モジュールをホストにコピー
	* (Pythonモジュールの場合) 生成したスクリプトをコピー
	* (Pythonモジュールでない場合) `playbooks/library/custom_module` をコピー

3. (Pythonモジュールでない場合) 引数ファイルをホスト上で生成
	* モジュールに渡された引数をそのまま書き込んだ引数ファイルを生成
	* カスタムモジュール内に `# WANT_JSON` を記述することで引数ファイルをJSONとすることも可能

4. 引数ファイルを引数として渡しホスト上でモジュールを呼び出し


# Pythonモジュール

Ansibleが提供する **AnsibleModule** を使うことで以下が容易になる

* 入力のパース
* JSONフォーマットでの出力
* 外部プログラムの呼び出し

以下のサンプルコードを見ながらまとめる([ansiblebook/ch10/playbooks/library/can_reach](https://github.com/lorin/ansiblebook/blob/master/ch10/playbooks/library/can_reach)より)

```python:python:playbooks/library/can_reach
#!/usr/bin/python

def can_reach(module, host, port, timeout):
    nc_path = module.get_bin_path('nc', required=True)
    args = [nc_path, "-z", "-w", str(timeout), host, str(port)]
    (rc, stdout, stderr) = module.run_command(args)
    return rc == 0

def main():
    module = AnsibleModule(
        argument_spec=dict(
            host=dict(required=True),
            port=dict(required=True, type='int'),
            timeout=dict(required=False, type='int', default=3)
        ),
        supports_check_mode=True
    )

    # In check mode, we take no action
    # Since this module never changes system state, we just
    # return changed=False
    if module.check_mode:
        module.exit_json(changed=False)

    host = module.params['host']
    port = module.params['port']
    timeout = module.params['timeout']

    if can_reach(module, host, port, timeout):
        module.exit_json(changed=False)
    else:
        msg = "Could not reach %s:%s" % (host, port)
        module.fail_json(msg=msg)

from ansible.module_utils.basic import *
main()
```


## `can_reach`

```python:
def can_reach(module, host, port, timeout):
    nc_path = module.get_bin_path('nc', required=True)
    args = [nc_path, "-z", "-w", str(timeout), host, str(port)]
    (rc, stdout, stderr) = module.run_command(args)
    return rc == 0
```

### [get_bin_path](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/basic.py#L1815)

* 外部プログラムのパスを取得
	* ここでは nc(netcat) コマンドのパスを取得
* `required=True` が指定されている場合、実行ファイルが見つからないと `fail_json`メソッド(後述)をなげる

### [run_command](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/basic.py#L2136)

* 外部プログラムを呼び出し
* いくつかの引数をとるけどもここでは `args` についてのみまとめる
	* 細かいことはリンク先のコード追えばいいかな

#### `run_command` の引数 : `args`

* 型 : 文字列 or リスト
	* 上記の例では、引数をリストで渡している
* 実行するコマンドとそのオプション等
* 返り値は rc, stdout, stderr

## AnsibleModule

```python:
module = AnsibleModule(
    argument_spec=dict(
        host=dict(required=True),
        port=dict(required=True, type='int'),
        timeout=dict(required=False, type='int', default=3)
    ),
    supports_check_mode=True
)
```

### `argument_spec`

* AnsibleModuleのとる引数のうち、唯一必須
* モジュールで使用できる引数に関する辞書
	上記の例では host, port, timeout を引数としている

### 引数のオプション

上記の例でも引数(host, port, timeout)にオプションが指定されている

* `required` 
	* Trueであれば引数は必須

* `type`
	* 引数の型を指定（デフォルトでは文字列）
	* str, list, dict, bool, int, float が指定可能 

* `default`
	* デフォルト値
	* `required=False` とする場合には指定しておいた方がよい

### [`チェックモード`](http://docs.ansible.com/ansible/dev_guide/developing_modules.html#check-mode)


* AnsibleModuleの引数の一つ
	* Trueの場合にはチェックモードをサポートする
* `ansible-playbook` に *-C* or *--check* フラグを渡すことで有効化
* チェックモードでは各タスクがホストの状態を変化させるか、変化させずに成功するか、エラーを返すかといったことのみを知らせる

## `exit_json` / `fail_json`

```python:
if module.check_mode:
    module.exit_json(changed=False)

host = module.params['host']
port = module.params['port']
timeout = module.params['timeout']

if can_reach(module, host, port, timeout):
    module.exit_json(changed=False)
else:
    msg = "Could not reach %s:%s" % (host, port)
    module.fail_json(msg=msg)
```

### `exit_json`

* 成功したことを示す
* 引数 changed は必須 : ホストの状態を変化させたかどうか
* 引数 msg を利用し、メッセージを返すことも可能

### `fail_json`

* 失敗したことを示す
* 引数 msg で失敗の理由を返すべき

### モジュールの引数へのアクセス

* `argument_spec` で定義した各引数へ params によりアクセス可能

## AnsibleModuleヘルパークラスのインポート

```
from ansible.module_utils.basic import *
```

* カスタムモジュールでは末尾にimport文を書いた方がよい
	* 前述した通りPythonモジュールの場合、 **ヘルパークラスのコードや引数などを埋め込み1つにまとめたPythonスクリプトを生成** するため、モジュール内の行番号は元のファイルと生成されたものとで異なる
	* 先頭にimport文をもってきてしまうと、xx行でエラーが起きてるよーみたいな時にわかりづらくなる