【Ansible】自作モジュール作ってみる
はじめに
この記事はAPC Advente Calendar 2019の14日目向けに作成しています。
PythonでAnsibleの自作モジュールを作ってみました。
以下のようなモチベーションです。
- Pythonのお勉強(モジュールのソースをちょっと読んでみる)
- モジュールを作る人の気持ちを少し理解する
- モジュールって作れるんだ!っていう感動を得たい
公式のモジュールのソース
Ansible公式のモジュールのソースは、以下のGitHubリポジトリにあります。
手始めに、copyモジュールとか、眺めてみました。
完全に理解した。(してない)
お題
さて、何を作ろうか。ということで、
下記のお題に決めました。
srcで指定したファイルから、キーワードが含まれる行のみ抽出して、新しいファイルを生成する
過去の記事で、公式のモジュールをこねくり回して実現した、
「registerに格納したリストを、条件で絞って新たなリストに格納する」
という処理を作ってみようってとこからきています。
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とか使ったのを作ってみようかな。