Ansibleモジュールを自作したらsanityテストをしてみよう

自作のAnsibleモジュールを作成した時のテスト手法をまとめます。
ここではいくつかあるテストの中で `sanity` テストのやり方について書いておきます。
`sanity` テストは静的解析をしてくれるテストです。例えばPEP8に反していないか、必要なモジュールがロードされているかなど確認してくれます。

# 1. 環境

|   項目  | バージョン |
|---------|------------|
| OS      | CentOS7.5  |
| Python  | 3.6.6      |
| Ansible | devel      |

# 2. テスト用ドキュメント

以下は、Ansibleモジュールのテスト用ドキュメントです。

* [https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html](https://docs.ansible.com/ansible/latest/dev_guide/developing_modules_general.html)
* [https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html](https://docs.ansible.com/ansible/latest/dev_guide/testing_sanity.html)

# 3. 作成したモジュール

テストを実行するために簡単なモジュールを作ってみました。
以下は `name` に指定した文字列を辞書で返すだけのモジュールです。

```python:output_name.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (c) 2018, sky_joker
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

from __future__ import absolute_import, division, print_function
__metaclass__ = type


ANSIBLE_METADATA = {
    'metadata_version': '1.1',
    'status': ['preview'],
    'supported_by': 'community'
}


DOCUMENTATION = '''
module: output_name
short_description: module that outputs a name.
author:
    - sky_joker (@sky-joker)
version_added: 2.x
description:
    - Output the specified name.
requirements:
    - python >= 2.7
options:
    name:
        description:
            - Specify name.
        required: True
'''

EXAMPLES = '''
---
- output_name:
    name: taro
  register: r

- debug: msg="{{ r }}"
'''

from ansible.module_utils.basic import AnsibleModule


def main():
    argument_spec = dict(
        name=dict(type="str", required=True)
    )

    module = AnsibleModule(argument_spec, supports_check_mode=True)

    result = dict(changed=False)

    name = module.params['name']
    if(name):
        result['name'] = name
        module.exit_json(**result)
    else:
        module.fail_json(msg="Please specify name.")


if __name__ == "__main__":
    main()
```

# 4. テスト準備

## 4-1. リポジトリクローン

テストツールはAnsibleのリポジトリにあるのでクローンします。

```shell-session
[example@localhost ~]$ git clone https://github.com/ansible/ansible.git
```

## 4-2. venv作成

テスト用にモジュールをインストールするのでvenvを作成します。

```shell-session
[example@localhost ~]$ cd ansible/
[example@localhost ansible]$ python36 -m venv venv
```

## 4-3. 必要なモジュールのインストール

~~`requirements.txt` に記述されているものだけでは足りないので追加でモジュールをインストールします。~~
`sanity` 用は別で分けているので `test/runner/requirements/sanity.txt` に記述されているモジュールをインストールします。

```shell-session
[example@localhost ansible]$ source venv/bin/activate
(venv) [example@localhost ansible]# pip install -r test/runner/requirements/sanity.txt
```

# 5. テスト実施

テストを実施してみます。
現状のパスです。

```shell-session
(venv) [example@localhost ansible]$ pwd
/home/example/ansible
(venv) [example@localhost ansible]$ ls
CODING_GUIDELINES.md  MANIFEST.in           Makefile    bin         contrib  examples  lib       packaging         setup.py       test     venv
COPYING               MODULE_GUIDELINES.md  README.rst  changelogs  docs     hacking   licenses  requirements.txt  shippable.yml  tox.ini
```

~~`ansible` ディレクトリ内でテストを実行します。~~
~~理由は `ansible-test(テストツール)` が相対パスでカレントの `lib` モジュールを読み込んでいるためです。~~
~~試しに `output_name.py` でファイルを作成します。~~

`env-setup` を実行すればパスが自動で設定されます。

```
(venv) [example@localhost ansible]# . hacking/env-setup
```

テスト用モジュールを作成します。

```
(venv) [example@localhost ansible]# mkdir lib/ansible/modules/salf-made
(venv) [example@localhost ansible]# touch lib/ansible/modules/salf-made/__init__.py
(venv) [example@localhost ansible]$ vi lib/ansible/modules/salf-made/output_name.py
コードを貼り付け
```

テストを実行します。
Pythonは3.6を使っているのでバージョンを指定します。
<font color="#FC0107">※symlinksをskipしている理由はシンボリックリンクがあると処理が失敗するためです。venv配下にはlib64というシンボリックリンクがあるため必ず処理が失敗します。特に問題ないと思われるため無効化します。</font>

```
(venv) [example@localhost ansible]# ansible-test sanity --skip-test symlinks --python 3.6 lib/ansible/modules/salf-made/output_name.py
WARNING: Skipping tests disabled by default without --allow-disabled: docs-build
Sanity check using ansible-doc with Python 3.6
Sanity check using azure-requirements
Sanity check using boilerplate
Sanity check using botmeta
Sanity check using changelog
Sanity check using compile with Python 3.6
Sanity check using configure-remoting-ps1
Sanity check using empty-init
Sanity check using import with Python 3.6
Sanity check using integration-aliases
Sanity check using line-endings
Sanity check using no-assert
Sanity check using no-basestring
Sanity check using no-dict-iteritems
Sanity check using no-dict-iterkeys
Sanity check using no-dict-itervalues
Sanity check using no-get-exception
Sanity check using no-illegal-filenames
Sanity check using no-smart-quotes
Sanity check using no-tests-as-filters
Sanity check using no-underscore-variable
Sanity check using no-unicode-literals
Sanity check using pep8
Sanity check using pslint
Sanity check using pylint
Sanity check using replace-urlopen
Sanity check using required-and-default-attributes
Sanity check using rstcheck
Sanity check using sanity-docs
Sanity check using shebang
Sanity check using shellcheck
Sanity check using test-constraints
Sanity check using use-argspec-type-path
Sanity check using use-compat-six
Sanity check using validate-modules
WARNING: Cannot perform module comparison against the base branch. Base branch not detected when running locally.
Sanity check using yamllint
WARNING: Reviewing previous 2 warning(s):
WARNING: Skipping tests disabled by default without --allow-disabled: docs-build
WARNING: Cannot perform module comparison against the base branch. Base branch not detected when running locally.
```

`ansible-doc` は以下のようにテストすればいいです。

```shell-session
(venv) [example@localhost ansible]# ansible-doc -v output_name
No config file found; using defaults
> OUTPUT_NAME    (/root/ansible/lib/ansible/modules/salf-made/output_name.py)

        Output the specified name.

OPTIONS (= is mandatory):

= name
        Specify name.



REQUIREMENTS:  python >= 2.7

AUTHOR: sky_jokerxx
        METADATA:
          status:
          - preview
          supported_by: community


EXAMPLES:
---
- output_name:
    name: taro
  register: r

- debug: msg="{{ r }}"
```

これで、自作Ansibleモジュールの静的なチェックはできました。
エラーが起きていないので問題ないですね :)
dockerでやるやり方なども公式に書いてあるのでSCMと組み合わせたCIが出来そうです。
CIが出来たら、また `sanity` の記事を書いてみようと思います。
今回はいくつかあるテストの中で `sanity` についてまとめてみました。

# 6. 補足

`sanity` で出来るテストは以下のように一覧で表示できます。

```shell-session
(venv) [example@localhost ansible]$ ansible-test sanity --list-test
WARNING: Skipping tests disabled by default without --allow-disabled: docs-build
ansible-doc
azure-requirements
boilerplate
botmeta
changelog
compile
configure-remoting-ps1
empty-init
import
integration-aliases
line-endings
no-assert
no-basestring
no-dict-iteritems
no-dict-iterkeys
no-dict-itervalues
no-get-exception
no-illegal-filenames
no-smart-quotes
no-tests-as-filters
no-underscore-variable
no-unicode-literals
pep8
pslint
pylint
replace-urlopen
required-and-default-attributes
rstcheck
sanity-docs
shebang
shellcheck
symlinks
test-constraints
use-argspec-type-path
use-compat-six
validate-modules
yamllint
WARNING: Reviewing previous 1 warning(s):
WARNING: Skipping tests disabled by default without --allow-disabled: docs-build
```