概要
Ansible公式のテストフレームワークであるMoleculeは,Ansibleによる構築の後に,デフォルトではTestinfraによるテストを実行します.
Testinfraにはhost.ansible.get_variables()
というAPIがあり,これを介してAnsibleの変数を取得できるように見えます.が,実際にはinventoryやhost_vars,group_varsに記載されているもの以外の値を取ることができません.
ここでは,Testinfraによるテストコードから,gather_facts
によって収集される変数やRoleのdefaultsやvars内で定義されている変数を参照するハックを紹介します.ただし,これは完全にAnsibleの挙動と同じであるとは言えないかもしれないという点に注意してください.
環境
ここでは単体のRoleのテストのみを取り扱います.Roleのディレクトリの構造は以下の通りです.
$ tree -L 2 /path/to/role /path/to/role ├── LICENSE ├── Pipfile ├── Pipfile.lock ├── README.md ├── defaults │ └── main.yml ├── meta │ └── main.yml ├── molecule │ └── default ├── tasks │ └── main.yml ├── tests │ ├── inventory │ └── test.yml └── vars └── main.yml
Testinfraにおける問題点
TestinfraはAnsibleと完全に統合されているわけではないため,構築の実行中に使用される変数を取得することができません.これにより何が起こるかというと,複数のプラットフォームに跨ったテストを書くときにifを大量に並べることになります.
テストコード中では以下のようにOSの情報を取得できます*1
def test_hoge(host): dist = host.system_info.distribution if dist == 'debian': # some action elif dist == 'ubuntu': # some action elif dist == 'darwin': # some action elif dist == 'centos': # some action
正直めちゃくちゃ面倒臭い……インストールするパッケージのバージョンチェックなども,このままではバージョンをハードコードすることになり,Role内の値を変更するときにテストコード内も全て変更したりする必要が出てきます.
さて,ここで救世主のようにみえるTestinfraのAPIHost.ansible.get_variables()
の実際の実装をみてみましょう.
https://github.com/philpep/testinfra/blob/3.4.0/testinfra/utils/ansible_runner.py#L172-L189
ansible-inventoryコマンドを叩いてインベントリの情報を収集している以外は過去のバージョンとの互換性を保つためのコードのようです.つまり,get_variables()
とは言いつつも,実際にはインベントリに記述されている情報しか取っていません.
既存のアプローチ
いくつかのIssueで議論されています.
これらの中でも直接YAMLとして読み込んだり,include_vars
やsetup
を使用した方法が紹介されていますが,これらは誤った用法によりうまく変数を解釈できていません.
- YAMLとして読み取る
- これは論外
include_vars
を使う- 一見うまく動作するように思えるが,これも実際には変数の解釈をしない
- 例)
hoge: "{{ ansible_python_interpreter }}"
のようなYAMLをロードしても{"hoge": "{{ ansible_python_interpreter }}"}
が返ってくるだけ
include_vars
+setup
+ansible.template.Templar
- https://github.com/philpep/testinfra/issues/345#issuecomment-468814489
- かなり惜しい
- 使い方が間違っている
Testinfraから変数を参照する
ようやく本題です.変数を取得する方法は雑にいうとAnsibleのinclude_vars
とsetup
モジュールを使い,自分でファイル読み込みの優先順位を定義して順に読み取っていきます.読み込んだ変数を優先度順に ansible.template.Templar
で解決していきます.
Testinfraが内部で採用しているテストフレームワークであるpytestの作法に則って,fixtureとして実装します.
setup
まず,setup
モジュールを使用してAnsibleのFactsを収集します.
import pytest @pytest.fixture(scope='module') def ansible_facts(host): return host.ansible("setup", "gather_subset=min")["ansible_facts"]
これはそれなりに重いので,
gather_subset
やfilter
などの引数を使って必要な変数のみ取得*2- fixtureのスコープをmoduleにすることで,複数回の取得を防ぐ
等で多少早くなります(たぶん).
このansible_facts
というfixtureは単にいつも使っているFacts*3がそのまま返ってきます.
include_vars
include_vars
を使って各変数定義ファイルを読んでいきます.
from ansible.template import Templar from ansible.parsing.dataloader import DataLoader @pytest.fixture(scope='module') def ansible_vars(host, ansible_facts): # リストの後にいくほど優先度が高い # host.system_infoを利用すれば,OSごとに読み取るvarsを変更している場合にも対応可能 var_files = [ "../../defaults/main.yml", host.ansible.get_variables(), "../../vars/main.yml", ] templar = Templar(loader=DataLoader()) # 優先度順に読んでいく all_vars = {} all_vars.update(ansible_facts) for f in var_files: if isinstance(f, str): var_from_f = host.ansible( "include_vars", f"file={f}")["ansible_facts"] else: var_from_f = f # 変数の出現順に埋め込みを解決 for key, val in var_from_f.items(): templar.available_variables = all_vars all_vars[key] = templar.template(val) return all_vars
ディクショナリの順序が保証されているPython 3.6以降でしかうまく動作しない気はしています.
templar.template()
は引数にディクショナリも取ることができるのですが,その際,解決すべき変数がそのディクショナリ内に含まれている場合にうまく動作しません.
例えば以下のようなファイルを読み込み,そのまま渡すとうまく解決することができずにエラーになります.
--- hoge: "hoge" hogefuga: "{{ hoge }}fuga"
そのため,まずはhoge
という変数を解決してTemplar
の持つ変数のディクショナリを更新し,次にhogefuga
を解決するようにしました.
テストで使う
実際のテストコードへの実装は非常に容易です.例えばインストールされているべきパッケージの一覧をinstalled_packages
として定義しているなら,以下のようにします.
def test_hoge(host, ansible_vars): expected_packages = ansible_vars.get("installed_packages") for expected_pkg in expected_packages: actual_pkg = host.package(expected_pkg) assert actual_pkg.is_installed
メリット
- 煩わしいOSの判別を一部不要にできる可能性がある
- Role側がデフォルト値を変えただけなどの場合に,テストを手動で修正する必要が無くなる
デメリット
- そもそもRoleのデフォルト値が間違っていたりするとテストの意味がなくなる
setup
が重いため,内容次第ではテストが遅くなる可能性がある- 完全に
ansible-playbook
コマンドによる実行と同等とはいえず,また,内部のAPIを直接参照しているため,今後も互換性が担保されるかわからない
現時点での制限
- Role中のタスクで
set_fact
したりregister
している変数は参照できない - 変数の優先順位を人が解決する必要がある
などなど.現時点での実装があまり賢いとは思っていないので,もしより良い方法を知っている人がおられれば教えていただけると幸いです.