ぴよ丸水産

週末ファゴッティストによる技術ブログ

【Ansible】自作モジュール作ってみる

はじめに

この記事はAPC Advente Calendar 2019の14日目向けに作成しています。

qiita.com

PythonでAnsibleの自作モジュールを作ってみました。
以下のようなモチベーションです。

  • Pythonのお勉強(モジュールのソースをちょっと読んでみる)
  • モジュールを作る人の気持ちを少し理解する
  • モジュールって作れるんだ!っていう感動を得たい

公式のモジュールのソース

Ansible公式のモジュールのソースは、以下のGitHubリポジトリにあります。

github.com

手始めに、copyモジュールとか、眺めてみました。
完全に理解した。(してない)

お題

さて、何を作ろうか。ということで、
下記のお題に決めました。
srcで指定したファイルから、キーワードが含まれる行のみ抽出して、新しいファイルを生成する

過去の記事で、公式のモジュールをこねくり回して実現した、
「registerに格納したリストを、条件で絞って新たなリストに格納する」
という処理を作ってみようってとこからきています。

blue-38.hatenablog.com

set_factで変数でやりくりしていますが、
せっかくなんで、ファイルでやりくりする感じにしました。

設計(的な何か)

設計なのかわからないですけど、
これをこうすると、こうなるんじゃ!
っていうイメージは明確にしたいので、
入出力ファイルのサンプルと、
Playbookを先に書いてみました。

このファイルを準備して、

  • /root/src/sample.txt
yokohama kanagawa
tachikawa tokyo
yamato kanagawa
fujisawa kanagawa
oomiya saitama
kawaguchi saitama

このPlaybookを流すと、

  • /root/ansible/site.yml
---
- hosts: all
  tasks:
  - name: try extract module
    extract:
      src: /root/src/sample.txt
      dest: /root/dest/sample_result.txt
      word: "kanagawa"
      
  - name: display result
    debug:
      msg: "{{ result.extracted_data }}"

こうなる!

  • /root/dest/sample_result.txt
yokohama kanagawa
yamato kanagawa
fujisawa kanagawa

実装

完成品

完成品はこちらです。
だいぶcopyモジュールを参考にしています。

ansibleをたたくディレクトリの配下にlibraryディレクトリを作成して、
そこにソースを格納します。

  • /root/ansible/library/extract.py
#!/usr/bin/python
# -*- coding: utf-8 -*-

from ansible.module_utils.basic import *

def main():
    module = AnsibleModule(
        argument_spec = dict(
            src = dict(required=True, type='path'),
            dest = dict(required=True, type='path'),
            word = dict(required=True, type='str'),
        ),
        supports_check_mode=True
    )
    src = module.params['src']
    b_src = to_bytes(src, errors='surrogate_or_strict')
    dest = module.params['dest']
    b_dest = to_bytes(dest, errors='surrogate_or_strict')
    word = module.params['word']
    flag = 0

# ファイルが存在しない場合は、異常終了
    if not os.path.exists(b_src):
        module.fail_json(msg="Source %s not found" % (src))

# 冪等性担保のために、destのファイルの有無を事前調査
    if os.path.exists(b_dest):
        old_file = open(b_dest)
        old_file_lines = old_file.readlines()
        old_file.close()
        flag = 1

# srcのファイルを読み出しして、キーワードを含む行を抽出
    org_file = open(b_src,'r')
    lines = org_file.readlines()
    org_file.close()
    extracted_lines = []
    for line in lines:
        if word in line:
            extracted_lines.append(line)
        else:
            pass
# destのファイルが既に存在する場合、内容を比較
# 既に存在するファイルの内容と、書き出そうとしている内容が一致する場合は、changed=Falseで終了させる
    if flag == 1:
        if old_file_lines == extracted_lines:
            module.exit_json(changed=False,extracted_data=extracted_lines)

# 抽出したファイルをdestのファイルに書き出す
# 抽出結果がない場合は、空のファイルが生成される
    extract_file = open(b_dest,'w')
    if len(extracted_lines) > 0:
        for line in extracted_lines:
            extract_file.write(line)
    else:
        pass
    extract_file.close()

# changed=Trueで終了させる
    module.exit_json(changed=True,extracted_data=extracted_lines)

if __name__ == '__main__':
    main()

ポイント

from ansible.module_utils.basic import *

モジュール作る時のおまじないです。

argument_spec = dict()

引数を定義できます。
必須か否かはrequired=で。
引数の型はtype=で指定できるみたいです。

module.exit_json()

モジュールをを正常終了させて返り値を返します。
changed=で変更の有無を定義します。
モジュールの返り値を定義できます。
registerに入れて使えるように、
extracted_lines(抽出した行のリスト)を返す仕様にしてみました。

module.fail_json()

モジュールを異常終了させて、エラーメッセージを返します。
msg=で定義できます。

下記のエラー出たらpythonがおかしいので、コードを見直しましょう

# ansible-playbook -i "localhost," -c local site.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
ok: [localhost]

TASK [try extract module] ******************************************************
fatal: [localhost]: FAILED! => {"msg": "Unable to import extract due to invalid syntax"}

PLAY RECAP *********************************************************************
localhost                  : ok=1    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

ちゃんとextract.pyをモジュールとして読み込んでくれてんのかな?
と、ディレクトリ構造など疑いましたが、
そこは問題なかったです。

:が余計についてたり、
,が必要なところになかったり、
ただの凡ミスでした。

実行

実行結果がこちらです。
ちゃんと冪等性も、(浅~く)確認しました。

  • 1回目実行
# ansible-playbook -i "localhost," -c local site.yml -vv
ansible-playbook 2.8.5
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /bin/ansible-playbook
  python version = 3.6.8 (default, Jul  1 2019, 16:43:04) [GCC 8.2.1 20180905 (Red Hat 8.2.1-3)]
Using /etc/ansible/ansible.cfg as config file

PLAYBOOK: site.yml *************************************************************
1 plays in site.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
task path: /root/ansible/site.yml:3
ok: [localhost]
META: ran handlers

TASK [try extract module] ******************************************************
task path: /root/ansible/site.yml:8
changed: [localhost] => {"changed": true, "extracted_data": ["yokohama kanagawa\n", "yamato kanagawa\n", "fujisawa kanagawa\n"]}

TASK [display result] **********************************************************
task path: /root/ansible/site.yml:15
ok: [localhost] => {
    "msg": [
        "yokohama kanagawa\n",
        "yamato kanagawa\n",
        "fujisawa kanagawa\n"
    ]
}
META: ran handlers
META: ran handlers

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
  • 2回目実行
# ansible-playbook -i "localhost," -c local site.yml -vv
ansible-playbook 2.8.5
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /bin/ansible-playbook
  python version = 3.6.8 (default, Jul  1 2019, 16:43:04) [GCC 8.2.1 20180905 (Red Hat 8.2.1-3)]
Using /etc/ansible/ansible.cfg as config file

PLAYBOOK: site.yml *************************************************************
1 plays in site.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
task path: /root/ansible/site.yml:3
ok: [localhost]
META: ran handlers

TASK [try extract module] ******************************************************
task path: /root/ansible/site.yml:8
ok: [localhost] => {"changed": false, "extracted_data": ["yokohama kanagawa\n", "yamato kanagawa\n", "fujisawa kanagawa\n"]}

TASK [display result] **********************************************************
task path: /root/ansible/site.yml:15
ok: [localhost] => {
    "msg": [
        "yokohama kanagawa\n",
        "yamato kanagawa\n",
        "fujisawa kanagawa\n"
    ]
}
META: ran handlers
META: ran handlers

PLAY RECAP *********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0
  • ファイルが見つかりませんエラーを起こしてみる。
# ansible-playbook -i "localhost," -c local site.yml -vv
ansible-playbook 2.8.5
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/root/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /bin/ansible-playbook
  python version = 3.6.8 (default, Jul  1 2019, 16:43:04) [GCC 8.2.1 20180905 (Red Hat 8.2.1-3)]
Using /etc/ansible/ansible.cfg as config file

PLAYBOOK: site.yml *************************************************************
1 plays in site.yml

PLAY [all] *********************************************************************

TASK [Gathering Facts] *********************************************************
task path: /root/ansible/site.yml:3
ok: [localhost]
META: ran handlers

TASK [try extract module] ******************************************************
task path: /root/ansible/site.yml:8
fatal: [localhost]: FAILED! => {"changed": false, "msg": "Source /root/src/saample.txt not found"}

PLAY RECAP *********************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0

おわりに

モジュールは、わりと簡単に作れます!

公式モジュールはもっと複雑ですが、
実際手を動かしてみることで、
冪等性はどうやって仕込んでるのかとか、
考えるきっかけになりました。

基本的には公式のモジュールを駆使したほうがいいんだろうなぁと思いますが、
いい勉強になります。
次はAPIとか使ったのを作ってみようかな。

参考

blog.amedama.jp

dev.classmethod.jp