playbook内のtask定義にtagを設定しておくことで、指定tagのtaskのみ実行したり、逆に指定tagのtaskを除外してansible-playbook
を実行することができます。
開発中のtaskのみピンポイントで実行したい場合や、逆に、共有のDBのデータを更新したりするtaskはほかのユーザーやチームと調整してからでないと実行が難しかったり、Blue-Greenデプロイメントの実装で環境Aの機能をオフにしてもう片方の環境Bをオンにするような処理だけど開発中は環境Bだけ確認したかったり、大量データのダウンロードや冪等の確認を伴い処理に時間がかかるため開発中は実行したくないなど特定のtaskは実行したくない場合に利用できます。
また、特殊tagとして、常に実行するalways
と実行しないnever
というtagが予約語として用意されています。
never
は特に「通常は実行したくない場合」にtaskに設定しておくと、簡単に除外設定できます。
ただし、tag設定はtaskの呼び出し元のtag設定も継承されるという特徴があります。
tag設定をちゃんと設計・把握していないと、例えばrole単体で作成してて自前のplaybookから呼んでいるときは問題なかったけど、チームの他の人が作成したplaybookと結合したら想定してなかったtagが追加されて、期待しない動作をすることがあるので注意が必要です。
なお、本文中の「tagの設定」はplaybookに対するtags
ディレクティブを使ったtagの使用を表し、「tagの指定」はansible-playbook
実行時のオプション--tags
や--skip-tags
によるtagの使用を表します。(この記事内でのローカルルール)
また、Ansibleバージョンは2.10.5で確認しています。
TL;DR
本記事の内容は、importでtaskを読み込んだ場合の話です。
- playbookのtag設定は、「tagの継承」によってそのtaskの呼び出し元定義のtag設定も有効
- tag設定によってtaskが実行されるか評価されるのはtask単位
- 呼び出し元のplayやroleのtag設定と除外指定だけで配下の全taskが除外されたりはしない
- 動かしたくないtaskがある場合は
never
設定だけに頼らず--skip-tags
を指定--tags never
のみ指定するとnever
tag設定されたtaskは動作するnever
tagと他のtagを複数設定されたtaskは、他に設定されているtagを--tags
で指定されると動作する--skip-tags never
でnever
tagが設定されたtaskは他の有効なtagがあっても動かない
ansible-playbook
実行時は、まず--tags
で処理対象taskかチェックされてから--skip-tags
で除外対象かをチェックされる--skip-tags
の指定が優先される--skip-tags always
でalways
tag設定されたtaskは動かない
- 実行前に
ansible-playbook --list-tasks
で処理対象taskとそのtag設定を確認しよう
tag設定の継承例
playbook定義
tag設定の継承のされかたは、ちょっとわざとらしい上に一部無意味だけど以下のplaybookを例で説明します。
# playbook --- - hosts: localhost gather_facts: false tags: __play tasks: - import_role: name: other tags: __other - name: sample task debug: msg: "sample tasks" roles: - role: sample tags: __role1 - role: other tags: __role2
- playに
__play
を設定 - playはtasksにtaskを2つ定義
- task1は
__other
tagを設定してother
roleを設定 - task2はtag設定無し
- task1は
- playはrolesも使ってroleを2つ定義
- role1は
__role1
tagを設定してsample
roleを設定 - role2は
__role2
tagを設定して(taskと同じ)other
roleを設定
- role1は
※ tasks
とroles
を同時指定した場合の処理順はroles
->tasks
role定義
上記playbookが参照している一つ目のsample
roleは以下の通り。
# roles/sample/tasks/main.yml --- - name: task1 debug: msg: use __tag1 tags: __tag1 - name: task2 debug: msg: use __tag2 and never tags: - __tag2 - never - name: task3 debug: msg: use never tags: - never
もう一つのother
roleは以下の通り。
# roles/other/tasks/main.yml --- - name: other debug: msg: "other" tags: __other_task - name: import import_tasks: other_sub.yml
そしてさらにother
roleでは、import_tasks
を使って別のtaskファイルを読んでいる。内容は以下の通り。
# roles/other/tasks/other_sub.yml --- - name: other_sub debug: msg: other_sub
構造を整理
上記のplaybookのplay/task/roleの呼び出し構成とtagの設定場所の概要は以下の通り。
playやtaskのように要素に対して設定されてるものはブロックの中へ、roles
やimport_role
・import_tasks
のように別定義の呼び出し時に設定されるものは矢印にtagを記載しています。
ここで、playbookのファイル上でtaskに設定されてるだけのtagを整理すると以下の通り。
No | task | tags |
---|---|---|
(1) | sample task | N/A |
(2) | other | __other_task |
(3) | other_sub | N/A |
(4) | task1 | __tag1 |
(5) | task2 | __tag2 , never |
(6) | task3 | never |
しかし、実際は「tagの継承機能」が働くため、ansible-playbook
実行時のtaskごとのtag設定としては以下の通りになります。(順不同)
No | task | tags |
---|---|---|
(1) | sample task | __play_tag |
(2)-a | other | __other_task , __other , __play_tag |
(2)-b | other | __other_task , __parent_role_tag2 , __play_tag |
(3)-a | other_sub | __other , __play_tag |
(3)-b | other_sub | __parent_role_tag2 , __play_tag |
(4) | task1 | __tag1 , __parent_role_tag1 , __play_tag |
(5) | task2 | __tag2 , never , __parent_role_tag1 , __play_tag |
(6) | task3 | never , __parent_role_tag1 , __play_tag |
ここまで来るとマトリクス表の方がわかりやすいかもなのではみ出るけど併記。(はてなブログのスタイルなんか良いのないかなー)
No | task | __play |
__other |
__role1 |
__role2 |
__other_task |
__tag1 |
__tag2 |
never |
---|---|---|---|---|---|---|---|---|---|
(1) | sample task | ✔ | |||||||
(2)-a | other | ✔ | ✔ | ✔ | |||||
(2)-b | other | ✔ | ✔ | ✔ | |||||
(3)-a | other_sub | ✔ | ✔ | ||||||
(3)-b | other_sub | ✔ | ✔ | ||||||
(4) | task1 | ✔ | ✔ | ✔ | |||||
(5) | task2 | ✔ | ✔ | ✔ | ✔ | ||||
(6) | task3 | ✔ | ✔ | ✔ |
(2)のotherと(3)のother_subは、playで定義されたtasksから呼ばれるパターンと、rolesから呼ばれるパターンで、上位の定義が異なるため、同じファイルでも継承されるtagの情報が異なります。
(tasksからimport_role
で呼ばれるときは__other
が設定され、rolesから呼ばれるときは_role2
が設定される)
呼び出し元の判定だけでは終了しない
大抵のプログラミング言語には制御構文if
を使った条件分岐を実装できます。
以下のようなコードを記述すると、条件A
に合致しなかった場合はcode-a
以下入れ子になっている条件B
のif
は評価されずに処理は進みます。
if (条件A) { // code-a if (条件B) { // code-b if (条件C) { // code-c } } }
それに対してAnsibleのplaybookにおけるroleやimport_tasks
などのimport系の外部taskの呼び出しは、呼び出し元のtag条件に合致しなくても、一旦すべてのtaskの内容をロードし、継承によるtag設定も含めて全tagを評価した上で、そのtaskが処理対象か否かを判定します。
前述の例で、例えばsample
role内に定義されたtask1
(No 4)を見てみると、呼び出し階層は以下のようになります。
コマンドラインオプションの--tags
指定の特徴として、「指定されたtagがplaybookで設定されているtaskを実行する。tagが設定されなければ除外」という動きになりますが、--tags __tag1
を指定してansible-playbook
を実行する場合、「プログラミングの制御構文if
のように呼び出し元のplayのtags: __play
の時点でtagの条件に合致しないため処理を終了する」という動きにはなりません。
オプションは後述しますが、--tags
で__tag1
を指定して--list-tasks
を使ってtaskとtagの一覧を出力すると、以下の通りtask1が処理対象となることが確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __tag1 playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] tasks: sample : task1 TAGS: [__play, __role1, __tag1]
(追記) importとinclude
なおこの動作は、import系を使った静的なtaskの読み込みの場合になります。実行時に動的にtask読み込みを行うincludeは、事前の読み込み処理の時点ではtaskを読み込まないため、include_* を行うtask自体がtag条件に合致しなかった場合、対象の(もしimportだったら処理対象だったとしても)taskは読み込まれないため処理対象になりません。逆に、include_* 自体がtag条件に合致して外部のtask情報を読み込んでも、読み込んだtaskへtag情報は継承されません。
本記事も基本的にすべてimport系のtask読み込みを前提とした説明になります。(task読み込みの前処理段階でincludeの場合はtaskを読み込まないため)
Comparing includes and imports: dynamic and static re-use
taskとtagを確認するオプション
1つのplaybookで完結しているなど小規模で目視できるならともかく、この例のように呼び出し階層も複数あると、tags
で文字列検索しても構造を把握するのは正直難しいです。
ですが、ansible-playbook
コマンドには対象playbookのtaskとtagの構造をリストアップする便利なオプションがあります。
--list-tags: 実行予定taskで使用される全tagを出力
--list-tags
を指定します。
これを使うと「実行対象のtaskに設定されている全てのtag」をまとめて出力できます。
never
などによって対象外となっているtaskについては表示されません。
※ 正確には「実行されるかどうか評価の対象となるtask」であり、例えばwhen
の評価結果で実行されない場合のtaskでも出力の対象となります。
事前処理としてplaybookの構文解析を行い処理対象となるtask一覧を静的にリストアップできた内容になります。
$ ansible-playbook -i localhost, playbook.yml --list-tags playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] TASK TAGS: [__other, __other_task, __play, __role1, __role2, __tag1]
もしnever
設定のtaskを処理するつもりがないのにnever
が含まれている場合は注意してください。
--list-tasks: 実行予定の全taskとそれぞれのtagを出力
実行されるtaskとそのtag設定は、ansible-playbook
のオプション--list-tasks
で確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] tasks: sample : task1 TAGS: [__play, __role1, __tag1] other : other TAGS: [__other_task, __play, __role2] other : other_sub TAGS: [__play, __role2] other : other TAGS: [__other, __other_task, __play] other : other_sub TAGS: [__other, __play] sample task TAGS: [__play]
これらのオプションは、--tag
や--skip-tag
でtag指定した場合は、tag設定によって実行対象となるtaskのみが表示されます。
--check
のさらに1段階前の実行対象のtaskが何になるかを確認するのに利用すると良いです。
例えば--tag
を指定すると以下の通り、指定されたtagが設定されている特定taskのみ実行対象になることを確認できます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role2 playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] tasks: other : other TAGS: [__other_task, __play, __role2] other : other_sub TAGS: [__play, __role2]
neverが設定されたtaskが実行される条件
上記の実行例ではnever
が設定されたtaskはリストアップの対象になりません。
--tags
が未指定(あるいはデフォルトのall
指定)の場合は、never
設定のtaskは処理対象となりません。
never
が設定されたtaskを実行対象にするには以下の通り。
--tags never
を指定する- taskに
never
以外にもtagを設定しておき、そのtagを--tags
で指定する never
とalways
をtaskに同時設定する (--tags
指定は不要)
よってnever
tagを設定して通常実行されないように設定されているtaskでも、--tags
で明示的に指定されたり、併記・あるいは継承されたtagを--tags
で指定しても実行されます。
言い換えると、taskに設定されたnever
を含むtagのうち、どれか一つでも--tags
によって対象と判定されれば(never
設定されていても)処理対象となります。
以下のように--tags __role1
を指定すると、__role1
tagの設定を上位で行ったsample
roleはnever
が設定されていても全て実行対象となります。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1 playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] tasks: sample : task1 TAGS: [__play, __role1, __tag1] sample : task2 TAGS: [__play, __role1, __tag2, never] sample : task3 TAGS: [__play, __role1, never]
ただしこれは--tags
のみが指定された場合。
--skip-tags
の指定がある場合は、--tags
の判定後に除外判定が行われます。
never
設定されたtaskを確実に動かしたくない場合は、--skip-tags never
を指定することで、「taskに設定されたnever
を含むtagのうちどれか一つでも--tags
で処理対象と判定」された結果を、除外対象に上書きできます。
$ ansible-playbook -i localhost, playbook.yml --list-tasks --tags __role1 --skip-tags never playbook: playbook.yml play #1 (localhost): localhost TAGS: [__play] tasks: sample : task1 TAGS: [__play, __role1, __tag1]
実装の調査
というわけで長い前置きでしたが、ここからさらに長くなるおまけコンテンツ(本題)です。
playbookからどうやって設定されているtag情報を読み取って、継承処理を行っているか、その実装を確認してみます。
この辺りはとくにドキュメントで規定されているわけでは(たぶん)ないので、少なくとも現在のAnsible 2.10.5の場合のアーキテクチャの話になります。
今後このあたりの(仕様は変わらなくても)実装が変更されるメジャーバージョンアップなどがあれば話は変わってくるかもしれません。
リスト出力部分
まずは--list-tasks
や--list-tags
が指定された場合の「taskとtag情報を保持しつつ実際には処理せずにその情報のみprint」がどうなってるか確認してみます。
リスト表示のみの実行
--list-tasks
や--list-tags
が指定された場合の出力箇所はPlaybookCLIのrun()以下。
results
変数にplayの情報がセットされており、ループで逐次内容をprintしています。
msg = "\n play #%d (%s): %s" % (idx + 1, ','.join(play.hosts), play.name) mytags = set(play.tags) msg += '\tTAGS: [%s]' % (','.join(mytags))
if context.CLIARGS['listtasks']: cur_tags = list(mytags.union(set(task.tags))) cur_tags.sort() if task.name: taskmsg += " %s" % task.get_name() else: taskmsg += " %s" % task.action taskmsg += "\tTAGS: [%s]\n" % ', '.join(cur_tags)
ここでtask情報のprintを行いreturn
するとこのままreturn 0
でプログラムは終了するので、リスト表示指定を行わず通常実行の場合では、results
変数を取得する前段の以下の時点でターゲットノードに対する各処理は完了していることになります。
results = pbex.run()
通常実行
前述のリスト表示の場合results
変数にはplayの情報がセットされていました。
それに対して--list-tasks
などのリスト表示指定をせずに普通に実行すると、run()
の戻り値はplayの実行結果(正常なら0、異常なら非0のint)となります。
そのため、リスト表示するかどうか判定するif isinstance(results, list)
で処理対象外となり、そのelse
節でその戻り値をreturn
して終了します。
つまり、--list-tasks
など指定してtask一覧を表示する場合、実際に処理を実行するrun()
メソッドはコールされてから(処理が諸々スキップされたのちに)、Ansible実行の最後に出力する実装になっています。
処理分岐の実装箇所
pbex.run()
のコールは以下で、前述の通り--list-tags
などの指定による情報表示の前に行われます。
# create the playbook executor, which manages running the plays via a task queue manager pbex = PlaybookExecutor(playbooks=context.CLIARGS['args'], inventory=inventory, variable_manager=variable_manager, loader=loader, passwords=passwords) results = pbex.run()
コールされているPlaybookExecutor()
の実装は以下で、--list-tasks
などの指定が行われている場合、コンストラクタの時点でTaskQueueManager
の初期化を行わずに、このオブジェクトをNone
にセットしています。
if context.CLIARGS.get('listhosts') or context.CLIARGS.get('listtasks') or \ context.CLIARGS.get('listtags') or context.CLIARGS.get('syntax'): self._tqm = None
この後の実処理(run()
)では、このself._tqm
の値がNone
であればリストアップのみとして処理されていきます。
playbookの実行時の処理
playbookファイルのリストを1個ずつ処理していく処理の開始場所は以下の部分。
for playbook_path in self._playbooks: pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)
ただし、リスト表示のみの動作の場合は、以下の部分になります。
if self._tqm is None: # we are doing a listing entry = {'playbook': playbook_path} entry['plays'] = []
entry
という辞書に、playbook
とplays
というキーを持つ変数をセットしています。
この辞書に変数が追加されるのは上記と、以下のもう1か所。
これはplayごとの処理のループ内で、対象playの情報を追加しています。
(このelse
節で実処理が走るため、リスト表示時はtaskの実処理が行われない)
if self._tqm is None: # we are just doing a listing entry['plays'].append(play)
play毎の処理のループを抜けたら以下の部分で配列entrylist
へentry
を追加しています。
if entry: entrylist.append(entry) # per playbook
そしてplaybook毎のループの処理が完了したら、リスト表示時は処理完了。play情報の一覧になっているentrylist
をreturn
します。
(リスト表示ではなく実処理の場合はこのentrylist
は空のままになっているため、ここではreturn
されずに処理続行し、最終的にはtask実行が成功した0かエラー時の非0がreturn
されるため型が異なる)
if entrylist: return entrylist
よって呼び出し元であるPlaybookCLI#run()は、このreturn
値がlist
形式か否かで「処理済み」か「処理予定の情報を収集しただけ」かを、isinstance()
で判断できます。
tag情報は、playのtagはここで取り出しているplay.tags
で参照しています。
mytags = set(play.tags) msg += '\tTAGS: [%s]' % (','.join(mytags))
同じようにtaskのtagは以下。
(list tasksと list tagsで表示形式が異なるので取得方法も若干異なる)
all_tags.update(task.tags) if context.CLIARGS['listtasks']: cur_tags = list(mytags.union(set(task.tags)))
tag情報取得箇所
tag情報のprint箇所はわかったので、playbookで設定されているtagをどこから読み取ってるのか調べてみます。
そうすると、そもそもtagの前にどうやってplayやtaskの情報をロードしているのか、という話になるわけで、そこから追ってみます。
playbookをソースからロード
エントリポイントは、playbook_executor.py
の前述の以下の部分でplaybookの情報をロード。
pb = Playbook.load(playbook_path, variable_manager=self._variable_manager, loader=self._loader)
ここからコールされるのは、以下のplaybook/__init__.py
で実装してあるload()
スタティックメソッド。
@staticmethod def load(file_name, variable_manager=None, loader=None): pb = Playbook(loader=loader) pb._load_playbook_data(file_name=file_name, variable_manager=variable_manager) return pb
ここでPlaybook
インスタンスを作成し_load_playbook_data()
メソッドをコール、そこからplaybookの内容をロードしているのは以下の箇所。
try: ds = self._loader.load_from_file(os.path.basename(file_name)) except UnicodeDecodeError as e: raise AnsibleParserError("Could not read playbook (%s) due to encoding issues: %s" % (file_name, to_native(e)))
ここでコールしているのは、呼び出し元で指定しているloader
インスタンスのload_from_file()
メソッド。
これが何かと言うと、playbook_executor.pyのPlaybook.load()
コール時の引数のloader=self._loader
の部分。
さらにこれはPlaybookExecutor
インスタンス作成時の引数で指定されています。
def __init__(self, playbooks, inventory, variable_manager, loader, passwords): self._playbooks = playbooks self._inventory = inventory self._variable_manager = variable_manager self._loader = loader
じゃあさらに呼び出し元のcli/playbook.py
を見ると、laoder
インスタンスは_play_prereqs()
で取得しています。
loader, inventory, variable_manager = self._play_prereqs()
_play_prereqs()
の実装はここ。
この中でloader
はDataLoader()
をコールして取得しています。
def _play_prereqs(): options = context.CLIARGS # all needs loader loader = DataLoader()
そしてこのDataLoader()
はこちら。
斜め読みする限り、Ansibleとしての処理ではなくYAMLを読んでいます。
上位からコールされるメソッドのload_from_file()
を追っていくと、self.load()
-> from_yaml()
-> json.loads()
と呼ばれて、Pythonの標準パッケージのjson
で処理されています。
(※ YAML形式のplaybookもjson
で処理している)
なので、この時点ではYAMLとして文法が合っていれば、例えば存在しないモジュールや未定義のディレクティブを書いてたりしても読み込み処理までは動作します。
逆に、YAMLとして文法誤りがあれば、例えばインデント誤りなどあればこの時点でエラーになります。
ただし、Ansibleとしては処理されていないため、roles
やimport_playbook
のようにAnsibleとしての外部ファイル読み込みもまだ処理されないため、外部ファイルに文法誤りがあってもここではエラー判定は発生しません。
そのままplaybook
の処理に戻って内容を追っていくと、呼び出し元で使えるようにplay情報としてリストへ追加しているのはPlaybook
クラスの_load_playbook_data()
メソッド内下記の2か所で行われます。
pb = PlaybookInclude.load(entry, basedir=self._basedir, variable_manager=variable_manager, loader=self._loader) if pb is not None: self._entries.extend(pb._entries)
entry_obj = Play.load(entry, variable_manager=variable_manager, loader=self._loader, vars=vars) self._entries.append(entry_obj)
前者はimport_playbook
でplaybookをさらに読み込んでいる場合で、最終的にplaybook内に記述されているplayの読み込みは後者のPlay.load()
で、ここで読み取った一つのplay分のYAML情報(entry
)をentry_obj
として取得し、self._entries
リストに追加されていきます。
playのロード
Play#load()
は以下のスタティックメソッドで、Play
インスタンスを生成し、load_data()
をコールしています。
p = Play() if vars: p.vars = vars.copy() return p.load_data(data, variable_manager=variable_manager, loader=loader)
load_data()
の実装はplaybook/base.py
のFieldAttributeBase
クラス。
play以下全taskのロード
load_data()
メソッドの下記コードで、全てのplayの情報が再帰的に処理され読み込まれます。
for name, attr in sorted(iteritems(self._valid_attrs), key=operator.itemgetter(1)): # copy the value over unless a _load_field method is defined target_name = name if name in self._alias_attrs: target_name = self._alias_attrs[name] if name in ds: method = getattr(self, '_load_%s' % name, None) if method: self._attributes[target_name] = method(name, ds[name]) else: self._attributes[target_name] = ds[name]
name
にはtasks
やroles
・そしてtags
などの属性値が順番にセットされ、_load_tasks()
や_load_roles()
・_load_tasks()
というメソッドが定義されていればそれがコールされ、その戻り値をself._attributes
にセットしていきます。
この処理で、taskやroleあるいやimport系の定義による子要素も読み込まれていきます。
よって、この時点で保持されるtag情報は、まだ該当のtaskに設定されているもののみがセットされます。
(呼び出し元のtag情報は継承されていない)
そして実は、全playのデータロードが完了しても、保持されているデータ構造はこのままで、taskに紐づくtag情報はそのtaskに設定されたtagのみとなっています。
(呼び出し元のtag情報をマージした情報が保持される構成にはならない)
継承の仕組み
前述の通り、読み込まれた全taskの情報はtask単位でオブジェクトとして保持されます。
そしてこの個々のtaskオブジェクトは、そのtaskが持つ使用モジュールやwhen
やuntil
などの条件、register
など、各ディレクティブ設定の情報によって構成されています。
(前述の通りself._attributes[key]
で参照できる)
設定が継承される動作は、これらの値を参照するタイミングで親情報と結合された内容となる処理によって実装されています。
taskのtag情報などは前述の通りself._attributes['tags']
に保持されていますが、実際のコードは.tags
というプロパティ値でアクセスしています。
参照箇所は例えば、--list-tasks
で実行した際には以下の通り。
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L143 mytags = set(play.tags)
# https://github.com/ansible/ansible/blob/stable-2.10/lib/ansible/cli/playbook.py#L171 cur_tags = list(mytags.union(set(task.tags)))
この.tags
というプロパティアクセスは、Pythonの組み込み関数property()
を使ったメンバアクセス関数設定とfunctools#partial()
を使った引数固定の関数指定を使用し、getterのメソッドとして_generic_g_parent()
をコールするように、tags
に限らず各属性に対する定義をメタクラスで行っています。
method = "_get_attr_%s" % attr_name if method in src_dict or method in dst_dict: getter = partial(_generic_g_method, attr_name) elif ('_get_parent_attribute' in dst_dict or '_get_parent_attribute' in src_dict) and value.inherit: getter = partial(_generic_g_parent, attr_name) else: getter = partial(_generic_g, attr_name) setter = partial(_generic_s, attr_name) deleter = partial(_generic_d, attr_name) dst_dict[attr_name] = property(getter, setter, deleter)
このアクセサメソッド設定を、Taggableクラスで定義されている以下の_tags
という名前のプロパティ名を使用し、
class Taggable: untagged = frozenset(['untagged']) _tags = FieldAttribute(isa='list', default=list, listof=(string_types, int), extend=True)
メタクラスの以下のコード部分でtags
という先頭の_
を除いた名前でプロパティを動的に設定しています。
keys = list(src_dict.keys()) for attr_name in keys: value = src_dict[attr_name] if isinstance(value, Attribute): if attr_name.startswith('_'): attr_name = attr_name[1:]
これでattr_name
には_tags
の先頭_
が取り除かれtags
がセットされ、前述の_generic_g_parent
+partial
+property
の設定をdst_dict[attr_name]
にセットすることで、.tags
というプロパティアクセスを実装しています。
value.inherit:
についてはAtrribute
クラスのデフォルト値でTrue
となるように実装されます。
※ なので、ここを改造してFalse
にセットして動作させると、tagの継承は発生しません。
def __init__( self, isa=None, private=False, default=None, required=False, listof=None, priority=0, class_type=None, always_post_validate=False, inherit=True, alias=None, extend=False, prepend=False, static=False, ):
以上の実装によって、.tags
プロパティに参照すると、_generic_g_parent(prop_name=tags, self)
がコールされるようになります。
_generic_g_parent()
の内容についてはメタクラスと同じbase.pyにて実装されています。
def _generic_g_parent(prop_name, self): try: if self._squashed or self._finalized: value = self._attributes[prop_name] else: try: value = self._get_parent_attribute(prop_name) except AttributeError: value = self._attributes[prop_name] # _snip_
(条件によって異なりますが基本的には)_get_parent_attribute()
をコールして戻り値を取り出しています。
ではこのメソッドがどこにあるかと言うと、これは呼び出し元(self
)が実装しているもので、以下の2クラスで現状は実装されています。
Blockクラス側はRoleの場合の実装の違いなどはありますが、「親情報(_parent
)がある場合は、_get_parent_attribute()
を再帰的に呼び出す」という処理によって、そのtaskが持つ呼び出し元の設定も含めた情報が得られるようになっています。
playbookにおけるtag設定が呼び出し元の設定も継承するのはこの仕組みによって実装されています。
実行対象のtagかどうかを評価する箇所
ansible-playbook
の実行時、playbookにおけるtags
の設定に加え、コマンドラインオプションの--tags
および--skip-tags
の指定によって、対象taskが実行されるかどうかが決定します。
指定したオプションは以下のPlay#__init__
の実装の通り、only_tags
が--tags
指定のもの、skip_tags
が--skip_tags
指定のものとして処理されます。
そして、--tags
指定を行わなかった場合は、自動的にall
が指定されます。
self.only_tags = set(context.CLIARGS.get('tags', [])) or frozenset(('all',)) self.skip_tags = set(context.CLIARGS.get('skip_tags', []))
判定のロジックは以下のTaggable#evaluate_tags()
の通りで、まず--tags
によって処理対象かどうかが決定し、その後に--skip-tags
で除外設定かを判定します。(--skip-tags
の指定が優先される)
should_run = True # default, tasks to run if only_tags: if 'always' in tags: should_run = True elif ('all' in only_tags and 'never' not in tags): should_run = True elif not tags.isdisjoint(only_tags): should_run = True elif 'tagged' in only_tags and tags != self.untagged and 'never' not in tags: should_run = True else: should_run = False if should_run and skip_tags: # Check for tags that we need to skip if 'all' in skip_tags: if 'always' not in tags or 'always' in skip_tags: should_run = False elif not tags.isdisjoint(skip_tags): should_run = False elif 'tagged' in skip_tags and tags != self.untagged: should_run = False return should_run
大原則としては以下の通り。
--tags
で指定されなくても動かしたければalways
を設定 (ただし--skip-tags
指定で上書きされる)- tagが設定されたtaskを動かしたければ
--tags
で指定 (ただし--skip-tags
指定で上書きされる) - tagが設定されたtaskを動かしたくなければ
--skip-tags
で指定
気を付けるべきは、指定tag以外のtagが設定されたtaskや、--tags
や--skip-tags
で指定しなかったtagのtaskがどうなるか。
単純に「never
tagを設定しているから動かないだろう」「--tags
で指定してないから動かないだろう」は継承によって認識外のtagが付与される可能性もあるので危険。
tag以外 (クイズ)
例えばwhen
による条件も継承されます。
playbook.yml
--- - hosts: localhost gather_facts: false vars: cond1: true cond2: true roles: - role: sample when: - cond1 is defined and cond1|bool - cond2 is defined and cond2|bool - other
roles/sample/tasks/main.yml
--- - name: roled task 1 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}" - name: set cond2 to false set_fact: cond2: 0 - name: roled task 2 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}" - name: set cond2 to true set_fact: cond2: 1 - name: roled task 3 debug: msg: "cond1 {{cond1}} , cond2 {{cond2}}"
どうでしょう、5つのうちどのtaskが動作するかわかりますか?
正解は以下の通り。
$ ansible-playbook -i localhost, playbook.yml PLAY [localhost] ***************************************************************************************************************** TASK [sample : roled task 1] ***************************************************************************************************** ok: [localhost] => msg: cond1 True , cond2 True TASK [sample : set cond2 to false] *********************************************************************************************** ok: [localhost] TASK [sample : roled task 2] ***************************************************************************************************** skipping: [localhost] TASK [sample : set cond2 to true] ************************************************************************************************ skipping: [localhost] TASK [sample : roled task 3] ***************************************************************************************************** skipping: [localhost] PLAY RECAP *********************************************************************************************************************** localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=3 rescued=0 ignored=0
まとまった情報が見当たらないのですが、多くのディレクティブ/playbookキーワードは継承されます。
例えばこのあたりの指定は呼び出し先のtaskに継承されます。
- base.pyで定義
- conditional.pyで定義 (blockなど)
以下のtask.pyのloop_control
のアクセサメソッドの実装のように、inherit=False
というパラメタを指定しているものは機能は継承されませんが、前述のリストのようにそうでないものは機能が継承される実装になっているようです。
_loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)
まとめ
- playbookにおける
tag
の設定は、そのtaskに設定されてるものが全てではないので気を付けましょう。 --list-tasks
や--list-tags
を活用しましょう。