Ansibleのモジュール開発(実践編)
はじめに
こんにちは、昨日もサバゲーでたくさん撃たれてきた藤本です。
前々回のAnsibleのモジュール開発(基礎編)、前回のAnsibleのモジュール開発(Python編)とAnsibleのモジュール開発する上でのルールや便利なユーティリティライブラリの使い方をご紹介しました。
今回のエントリでは実際にユースケースに則って、一つのAnsibleモジュールを作成してみます。
その他のAnsibleのモジュール開発シリーズは以下をご参照ください。
ユースケース
AWS環境では標準AMIにSwap領域が含まれていないことが多くあります。もちろんSwapさせないメモリ設計が一番ですが、安全を取ってSwap領域は確保しておきたいこともありますよね。そんなわけでSwap領域を作成するSwapモジュールを作成しました。
まずは一般的なLinuxにおけるSwap領域の作成手順です。
- Swap領域として確保したいサイズの空のファイル作成
- 作成したファイルのSwap領域化
- Mount設定ファイルへの追記
- SwapファイルのMount
続いて、要件です。
- 対応OS
- Amazon Linux
- CentOS 5 / 6 / 7
- Ubuntu 12.04 / 14.04
- 対応OS以外はエラーで返す
- 引数
- Swapファイルサイズ(指定がない場合、OSメモリサイズと同値)
- Swapファイルパス(必須)
- 冪等性を担保する
- ファイル作成コマンド
- CentOS 6、Ubuntu 12.04 / 14.04、およびAmazon Linuxは
fallocate
コマンドでSwapファイルを作成する - CentOS 5 / 7は
dd
コマンドでSwapファイルを作成する
- CentOS 6、Ubuntu 12.04 / 14.04、およびAmazon Linuxは
それでは実装していきましょう。ソースコードは長くなったので、ページ最下部に貼っています。
メイン処理
初期処理
初期処理としてAnsibleModuleクラスで引数を受け取ります。今回は要件に挙げたSwapファイルサイズ(size)、Swapファイルパス(filepath)を引数に定義します。
from distutils.version import LooseVersion from ansible.module_utils.basic import * import os import re : def main(): module = AnsibleModule( argument_spec = dict( size=dict(required=False, type='str'), ### (1.1) filepath=dict(required=True, type='str') ### (1.2) ) ) : main()
(1.1) 引数:ファイスサイズ
ファイルサイズは必須ではないため、requiredにFalseを指定します。サイズの指定方法はfallocate
、dd
などと互換性を持たせて、Unitを使えるようにしています。(100Mや1Gなど)
(1.2) 引数:ファイルパス
ファイルパスは必須項目でrequiredにTrueを指定します。
Factoryクラスによる処理インスタンス生成
def main(): : swap = Creator.createSwap(module=module) ### (2.1) ### (2.2) -> changed = False if swap.create_swapfile(): changed = True if swap.make_swap(): changed = True if swap.set_fstab(): changed = True if swap.mount_swap(): changed = True ### <- (2.2) module.exit_json(changed=changed) ### (2.3)
(2.1) Factory Method呼び出しによる処理インスタンス生成
詳細な処理内容は後述しますが、Factoryクラスから処理インスタンスを受け取ります。
(2.2) 処理実行・実行結果判定
作成手順の一つ一つで定義したメソッドを実行します。メソッドは一つ一つ変更の有無を受け取り、既に設定済みであれば、changed
にfalse
を、一つでも変更が加われば、changed
にtrue
を渡して、冪等性を担保します。
(2.3) 結果出力
エラーなく各処理を終えた場合、正常終了を表すexit_json
メソッドで変更有無の結果が入ったjsonを返します。
Factoryクラス
今回のユースケースは要件に記載した通り、OSバージョンによってファイル作成コマンドを使い分けます。前回のエントリのOSバージョンを判定するパターンを利用し、OSを判定するFactoryクラスと、処理を記述するクラスを分けます。Factoryクラスは処理するOSディストリビューション、OSバージョンに対応する処理クラスのインスタンスを生成します。
ベースFactoryクラス
まずベースとなるFactoryクラスを定義します。
class Creator(object): platform = 'Generic' ### (3.1) distribution = None ### (3.2) swap_class = UnimplementedSwap ### (3.3) @classmethod def createSwap(cls, module): return load_platform_subclass(cls, args, kwargs).swap_class(module) ### (3.4)
(3.1) platformの定義
(3.2) distributionの定義
platform
、distribution
の説明は前回のAnsibleのモジュール開発(Python編)を参照してください。
(3.3) 処理クラスの定義
Factoryクラスとして生成したい処理クラスを設定します。ベースFactoryクラスはOS個別Factoryクラスに一致しない場合に呼び出されます。今回のSwapモジュールでは対応OS以外はエラーとしたいため、未実装のメッセージとともにエラーで結果を返す処理クラスを設定します。
(3.4) 処理クラスの呼び出し
load_platform_subclass()
で実行対象となるOSに合わせたFactoryクラスのインスタンスが生成され、インスタンスが持つ処理クラスがインスタンスとして返されます。
OS個別Factoryクラス
次にOS毎に用意するFactoryクラスを定義します。
パターンは2つです。1つはバージョンによらず処理クラスが同じパターン。もう一つはバージョンによって処理クラスが異なるパターン。
バージョンによらず処理クラスが同じパターン
実装はシンプルです。ベースFactoryクラスを継承し、処理クラスのインスタンス化処理はベースFactoryクラスに任せます。OS個別Factoryクラスではload_platform_subclass()
で適切な呼び出しが行われるようにplatform
、distribution
を定義します。また該当した場合に行いたい処理が実装された処理クラスを設定します。
class AmazonLinuxCreator(Creator): platform = 'Linux' distribution = 'Amazon' swap_class = GenericSwap
バージョンによって処理クラスが異なるパターン
from distutils.version import LooseVersion : def _check_version(min=None, max=None): version = get_distribution_version() ### (4.1) if not version or \ (isinstance(min, str) and LooseVersion(version) < LooseVersion(min)) or \ (isinstance(max, str) and LooseVersion(version) >= LooseVersion(max)): ### (4.2) return False return True : class CentosCreator(Creator): platform = 'Linux' distribution = 'Centos' if _check_version(min='6', max='7'): ### (4.3) swap_class = GenericSwap elif _check_version(min='5', max='6'): swap_class = CentOS5Swap
(4.1) OSバージョン取得
get_distribution_version()
の説明は前回のAnsibleのモジュール開発(Python編)を参照してください。
(4.2) バージョン判定
引数のmin
、max
で受け取ったバージョンの範囲にget_distribution_version()
が該当するか否かを判定します。Python標準ライブラリのLooseVersion
を利用することで複数の.
で句切られるようなバージョンを大なり小なりで比較することができます。
(4.3) OSバージョンチェック
OSバージョンチェック関数を呼び出し、該当するバージョンの処理クラスを設定します。上記の場合、CentOS 6系であれば、6.xxといったバージョン番号が返るため、一つ目のif条件句に合致(6 <= 6.xx < 7)し、fallocate
コマンドで実装されたGenericSwapクラスを処理クラスに設定します。CentOS 5系であれば、二つ目のelif条件句に合致(5 <= 5.xx < 6)し、dd
コマンドで実装されたCentOS5Swapクラスを処理クラスに設定します。それによりベースFactoryクラスにより設定した処理クラスがインスタンス化され、mainメソッドに適切な処理クラスが返ります。
処理クラス
Factoryクラスにより実行対象のOSに応じて返される処理クラスを実装します。処理クラスは以下の3パターンを作成します。
- fallocateコマンドパターン(ベース) : CentOS 6、Amazon Linux、Ubuntu 12.04 / 14.04
- ddコマンドパターン : CentOS 5 / 7
- 未対応OSパターン : OSX
fallocateコマンドパターン(ベース)
CentOS 6やAmazon Linux、Ubuntuに対応した処理クラスを実装します。ベースと書いたのはfallocateコマンドパターン、ddコマンドパターンはファイル作成のコマンドが異なるだけで、Swap領域化やマウント処理は同じ処理となるため、ddコマンドの処理クラスはファイル作成のメソッドのみオーバーライドするように実装します。そこそこ長くなってしまったのでポイントを絞って説明します。
class GenericSwap(object): CREATE_SWAPFILE_CMD = '/usr/bin/fallocate' MAKE_SWAP_CMD = '/sbin/mkswap' FILE_CMD = '/usr/bin/file' SWAPON_CMD = '/sbin/swapon' FSTAB_PATH = '/etc/fstab' def __init__(self, module): ### (5.1) -> self.module = module self.size = module.params['size'] self.filepath = module.params['filepath'] ### <- (5.1) if not self.size: self.size = self._get_memsize() ### (5.2) def create_swapfile(self): if os.path.exists(self.filepath): ### (5.3) return False ### (5.4) -> cmd = [self.CREATE_SWAPFILE_CMD, '-l', self.size, self.filepath] rc, out, err = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg='%s command failed. rc=%d, out=%s, err=%s' % (cmd[0], rc, out, err)) ### <- (5.4) os.chmod(self.filepath, 0600) return True def make_swap(self): : def set_fstab(self): : def mount_swap(self): : def _get_memsize(self): with open('/proc/meminfo') as f: for line in filter(lambda line: line.startswith('MemTotal:'), f.readlines()): return re.split('\s+', line)[1] + 'k'
(5.1) 初期化メソッド
AnsibleModule、引数はインスタンスに渡しておきます。
(5.2) Swapファイルサイズデフォルト値設定
Swapファイルサイズを指定しない場合、OSのメモリ値に合わせるといった初期設定を行います。処理内容としては/proc/meminfo
のMemTotal
の値を取得しています。
(5.3) 設定ステータスの確認
メイン処理で説明した通り、モジュールは冪等性を担保するためにchanged
にtrue
or false
を指定して実行結果を返します。各処理の前に変更の必要性を確認し、changed
の値を決定します。Swapファイル作成処理の場合、指定されたパスにファイルが存在すればファイルを作成する必要がないため、変更無しと判定するFalse
を返します。指定されたファイルがなければ、作成処理を行い、変更ありと判定するTrue
を返します。
(5.4) OSコマンド実行
OSコマンドを実行する場合、AnsibleModuleのrun_command()
を利用しましょう。また成功以外を想定しない場合、リターンコードのチェックを行い、エラーだった場合はfail_json()
で失敗の結果出力をしましょう。
ddコマンドパターン
CentOS 5 / 7に対応した処理クラスを実装します。上記ベースクラスを継承し、create_swapfile()
だけをオーバーライドしたクラスとなります。
class CentOS5Swap(GenericSwap): CREATE_SWAPFILE_CMD = '/bin/dd' BLOCK_SIZE = 1024 * 1024 def create_swapfile(self): if os.path.exists(self.filepath): return False if self.size.endswith(('k', 'K')): self.size = float(self.size[:-1]) * 1024 elif self.size.endswith(('m', 'M')): self.size = float(self.size[:-1]) * 1024 * 1024 elif self.size.endswith(('g', 'G')): self.size = float(self.size[:-1]) * 1024 * 1024 * 1024 else: self.size = float(self.size) count = int(self.size / self.BLOCK_SIZE) cmd = [self.CREATE_SWAPFILE_CMD, 'if=/dev/zero', 'of=%s' % self.filepath, 'bs=%d' % self.BLOCK_SIZE, 'count=%d' % count] rc, out, err = self.module.run_command(cmd) if rc != 0: self.module.fail_json(msg='%s command failed. rc=%d, out=%s, err=%s' % (cmd[0], rc, out, err)) os.chmod(self.filepath, 0600) return True
未対応OSパターン
対応していないOSの処理クラスです。初期化メソッドでfail_json()
を呼び出し、処理を行うことなく、モジュールを終了します。
class UnimplementedSwap(object): def __init__(self, module): distribution = get_distribution() module.fail_json( msg='swap module cannot be used on %s' % (distribution if distribution else get_platform()) )
動作確認
今回動作確認したOSは以下の7パターンになります。
- fallocateコマンドパターン(ベース) : CentOS 6、Amazon Linux、Ubuntu 12.04 / 14.04
- CentOS 6
- Ubuntu 12.04
- Ubuntu 14.04
- Amazon Linux
- ddコマンドパターン
- CentOS 5
- CentOS 7
- 未対応OSパターン
- OSX (ローカル端末)
playbook.yml
- hosts: all become: yes tasks: - swap: filepath=/swap
変更あり(未設定)
まずは未設定の状態でansible-playbook
を実行します。
# ansible-playbook playbook.yml -v Using /Users/fujimoto.shinji/Techs/ansible/dev-module/ansible.cfg as config file PLAY [all] ********************************************************************* TASK [setup] ******************************************************************* ok: [amazn] ok: [localhost] ok: [centos7] ok: [ubuntu12] ok: [ubuntu14] ok: [centos6] ok: [centos5] TASK [swap] ******************************************************************** fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "swap module cannot be used on Darwin"} changed: [amazn] => {"changed": true} changed: [ubuntu14] => {"changed": true} changed: [ubuntu12] => {"changed": true} changed: [centos5] => {"changed": true} changed: [centos7] => {"changed": true} changed: [centos6] => {"changed": true} NO MORE HOSTS LEFT ************************************************************* PLAY RECAP ********************************************************************* amazn : ok=2 changed=1 unreachable=0 failed=0 centos5 : ok=2 changed=1 unreachable=0 failed=0 centos6 : ok=2 changed=1 unreachable=0 failed=0 centos7 : ok=2 changed=1 unreachable=0 failed=0 localhost : ok=1 changed=0 unreachable=0 failed=1 ubuntu12 : ok=2 changed=1 unreachable=0 failed=0 ubuntu14 : ok=2 changed=1 unreachable=0 failed=0
localhostのOSXは未対応OSパターンのため、failed
にtrue
が返され、失敗となりました。その他OSはchanged
にtrue
が設定されていて、swapモジュールによって変更があったことを表現しています。それでは実際に設定されているのか確認してみます。
# ssh -F ssh_config amazn /sbin/swapon -s Filename Type Size Used Priority /swap file 503264 0 -1 # ssh -F ssh_config centos5 /sbin/swapon -s Filename Type Size Used Priority /dev/mapper/VolGroup00-LogVol01 partition 1048568 148 -1 /swap file 508920 0 -5 # ssh -F ssh_config centos6 /sbin/swapon -s Filename Type Size Used Priority /dev/dm-1 partition 1015804 0 -1 /swap file 502044 0 -2 # ssh -F ssh_config centos7 /sbin/swapon -s Filename Type Size Used Priority /dev/dm-1 partition 1048572 0 -1 /swap file 500732 0 -2 # ssh -F ssh_config ubuntu12 /sbin/swapon -s Filename Type Size Used Priority /dev/mapper/vagrant--vg-swap_1 partition 524284 0 -1 /swap file 501360 0 -2 # ssh -F ssh_config ubuntu14 /sbin/swapon -s Filename Type Size Used Priority /dev/mapper/vagrant--vg-swap_1 partition 524284 0 -1 /swap file 500768 0 -2
/swap
というファイルでSwap領域を認識していることが分かります。
変更なし(設定済み)
続いて、既に設定済みの状態でもう一度、ansible-playbook
を実行します。
# ansible-playbook playbook.yml -v Using /Users/fujimoto.shinji/Techs/ansible/dev-module/ansible.cfg as config file PLAY [all] ********************************************************************* TASK [setup] ******************************************************************* ok: [amazn] ok: [localhost] ok: [centos7] ok: [ubuntu12] ok: [ubuntu14] ok: [centos6] ok: [centos5] TASK [swap] ******************************************************************** fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "msg": "swap module cannot be used on Darwin"} ok: [amazn] => {"changed": false} ok: [centos5] => {"changed": false} ok: [ubuntu14] => {"changed": false} ok: [ubuntu12] => {"changed": false} ok: [centos7] => {"changed": false} ok: [centos6] => {"changed": false} NO MORE HOSTS LEFT ************************************************************* PLAY RECAP ********************************************************************* amazn : ok=2 changed=0 unreachable=0 failed=0 centos5 : ok=2 changed=0 unreachable=0 failed=0 centos6 : ok=2 changed=0 unreachable=0 failed=0 centos7 : ok=2 changed=0 unreachable=0 failed=0 localhost : ok=1 changed=0 unreachable=0 failed=1 ubuntu12 : ok=2 changed=0 unreachable=0 failed=0 ubuntu14 : ok=2 changed=0 unreachable=0 failed=0
既に設定済みなのでchanged
にfalse
を返しています。ただし、この返り値はあくまでモジュールの実装上のことなのでモジュール開発した場合は、テストツールを利用するなどして設定が行われているか、想定しない設定が行われていないか動作確認しましょう。
まとめ
いかがでしたでしょうか?
3回に分けて、Ansibleのモジュール開発の基礎から実践までご紹介しました。これからも色々と試してみてTipsをご紹介できればと思います。
今回のAnsibleのモジュール開発シリーズで作成した一連のソースコードをGithubにアップロードしました。色々試してみたい方はcloneして試してみてください。
s-fujimoto/develop-ansible-module