配列や、辞書の配列の中から特定の条件の要素を抜き出す。
配列操作はselect()
で要素に対してフィルター処理を行い、辞書操作はselectattr()
でkey指定することでvalueにフィルター処理を行う。
ここでいうフィルター処理は、AnsibleやJinja2のフィルターのことではなく、AnsibleやJinja2のTest(演算子/operatorと言うと個人的にはわかりやすいんだけど、「演算子」という表記は特に無く、Jinja2だとis
を使った被演算子の方で関数的に使用)を使って要素をフィルタリングすることを指す。
Ansibleで変数名 is HogeHoge(...)
という書式で使われているHogeHoge()
の部分。
これをselect()
やselectattr()
で使って配列・辞書に対してまとめて処理可能になっている (はず)。
select()
とselectattr()
は、どちらもJinja2テンプレートのサイトにBuiltin Filtersとして記載があるのでそちらを参照。
jinja.palletsprojects.com
この記事では、以下のバージョンを前提としている。
- Ansible: 2.10.5
- Python: 3.6.8
- Jinja2: 2.11.2
- OS: CentOS 7.9.2009
配列 (select)
指定したTestの演算結果がtrueの場合にその要素を返す。
って書くとなんか分かりづらいけど、要は条件に合致した配列要素の場合のみ、その要素を返す。
数値 (一致・大小)
比較演算子が使える。
vars:
integer_values:
- 0
- 1
- 2
- 3
- 4
- name: select integer values
debug:
msg:
- '{{ integer_values | select("eq", 2) }}'
- '{{ integer_values | select("==", 2) }}'
- '{{ integer_values | select("le", 3) }}'
- '{{ integer_values | select("<=", 3) }}'
- '{{ integer_values | select("lt", 3) }}'
- '{{ integer_values | select("<", 3) }}'
文字列 (一致・大小)
数値の場合と同様、eq
または==
で一致した場合。
(intは==
、strはeq
みたいに、どっちかに統一して欲しかったな、これ。。)
vars:
hosts:
- web1
- web2
- web3
- db1
- db2
tasks:
- name: split filter sample
debug:
msg:
- '{{ hosts | select("eq", "web1") }}'
文字列に対してはあまり使う頻度は無いと思うが、gt
/>
, ge
/>=
, lt
/<
, le
/<=
も使える。
match
、search
を使った正規表現も使える。
正規表現はJinja2のTestでなく、Ansibleで用意されている。
処理内容はPythonのPattern#match()
とPattern#search()
となっている。
Testing strings
vars:
hosts:
- web1
- web2
- web3
- db1
- db2
- web1
- dns
tasks:
- name: split filter sample
debug:
msg:
- '{{ hosts | select("match", "web") }}'
- '{{ hosts | select("match", "b") }}'
- '{{ hosts | select("search", "b") }}'
- '{{ hosts | select("regex", "b") }}'
また、regex
もあり、これはmatch_type
という名前付き引数を指定することで、Pythonのre
で使用するメソッドを指定できる。
'{{ hosts | select("regex", "d.*\d+", match_type="fullmatch") }}'
docs.python.org
型でフィルタリング
配列要素として数値や文字列・booleanなどバラバラの型の値が入っている場合に、型でフィルタリングもできる。
型名の文字列のみを引数に指定する。
type_mix:
- 1
- 12
- a
- ab
- curry
- 755
- true
- false
- 3.14
- "true"
- "false"
- "123"
- "3.14"
- - subitem1
- subitem2
- key: 1
value: 2
- name: select values type
debug:
msg:
- '{{ type_mix | select("string") }}'
- '{{ type_mix | select("integer") }}'
- '{{ type_mix | select("number") }}'
- '{{ type_mix | select("boolean") }}'
- '{{ type_mix | select("float") }}'
- '{{ type_mix | select("sequence") }}'
- '{{ type_mix | select("mapping") }}'
sequence
のみ思ってたのと違った。。
(iterateとして処理できるとtrueになるため、辞書だけでなく文字列もヒットする)
また、number
はboolean値もヒットするので注意。
型というより、数値として処理できる変数であれば対象という感じ?(未確認)
ファイルパス
実行ホスト上に指定されたパスが存在するかどうかでフィルタリングできる。
Testing paths
- name: file path test
vars:
path_list:
- /
- /bin
- /usr/bin/perl
- /usr/bin/python3
- /not-found-path
debug:
msg:
- '{{ path_list | select("directory") }}'
- '{{ path_list | select("file") }}'
- '{{ path_list | select("link") }}'
- '{{ path_list | select("exists") }}'
- '{{ path_list | select("mount") }}'
バージョン文字列
「〇〇が未インストール、または、インストール済みでバージョン□□以下の場合」みたいなことができる。
Comparing versions
- name: version sample
vars:
version_string:
- 1.0.0
- 1.0.99999999999999999
- 1.1.0
- 1.1.1
- 1.01.0
- 1.01.1
- 1.9.0
- 1.10.0
- '1.0.1'
- '1.1.1'
- '1.1'
debug:
msg:
- '{{ version_string | select("version", "1.1.0", "gt") }}'
1.1.0
との比較で、1.a.0
を入力するとエラーになる。
fatal: [localhost]: FAILED! =>
msg: 'Version comparison: ''<'' not supported between instances of ''str'' and ''int'''
versonについてはPythonのドキュメントに2021.02.16時点で記載がなぜか抜けているため、詳細はソースを見るしかない?
docs.python.org
StrictVersionにすると、<数字>.<数字>
のような形式のバージョン文字列で無いとエラーになる。
デフォルトのLooseVersionであればもう少しユルい書式でバージョン比較できる。
Ansibleで実装されているversoin()
/ version_compare()
についてはこちら。
また、テスト(Test Pluginでなくソフトウェアテストの方のテスト)の内容についてはこちら
PythonのVersion()
は別途もうちょっと調べたいところ。。
含む (in)
Pythonのin
演算子と同じで、別途用意したリスト内に含まれる要素かどうかを、入力としてのリストの全要素に対して検査する。
vars:
hosts:
- web1
- web2
- web3
- db1
- db2
- dns
contain_list:
- db1
- web1
- mail1
tasks:
- name: contain list
debug:
msg: '{{ hosts | select("in", contain_list) }}'
in
と似たものにcontains
もあるが、配列に対するselect
フィルターに使用する場合、contains
の引数に一致する要素をフィルタリング…しそうだけどそうはならず、配列要素を更にリストへ分解してその要素に含まれていればtrueという判定となっている。(書いててよくわからん)
まぁ一致する要素を抜き出したければeq
を使えばいいので配列要素に対するcontains
はナンセンスのような気もする。(使い方の認識を誤ってる可能性もあるけど…)
下記の場合、"b"
を含む文字列であればtrueになる。
vars:
contain_list:
- db1
- web1
- mail1
tasks:
- name: contain list
debug:
msg:
- '{{ contain_list | select("contains", "b") }}'
配列要素がiterableである必要があるため、数値の配列だと型エラーとなる。
動作する例としては以下のような感じ。(見やすいようにインラインリストにしている)
vars:
list_in_list:
- [1,2,3]
- [4,5,6]
- [7,8,9]
tasks:
- name: contain list
debug:
msg:
- '{{ list_in_list | select("contains", 5) }}'
辞書 (selectattr)
辞書の配列(辞書型の変数を要素に持つ配列)に対するselectattr()
は、「辞書のkey名とTest名を指定することで、そのvalueがtrueかどうか」という動作をする。
指定のTestを行い、結果がtrueの場合に要素を返すという機能自体はselect()
と同じ。
配列と若干異なる点としては「指定されたkeyが存在しない場合」に備えてdefined
を使ってガード処理を行ったり、配列の場合の動作がいまいちだったcontains
を使った「配列を要素として持つ場合に、その配列中に指定の要素があるか」などがある。
Test例は配列で一通りやってるので簡単に。。
以下のような、IPアドレスやパッケージ情報のホスト情報っぽい辞書型変数を要素に持つ配列を対象にselectattr()
を使ったフィルタリングを行ってみる。
- hosts: localhost
gather_facts: no
vars:
hosts:
- name: web1
addr: 192.168.2.10
mask: 255.255.255.0
pkg:
- httpd
- php
- python
- name: web2
addr: 192.168.2.11
mask: 255.255.255.0
pkg:
- httpd
- ruby
- python
- addr: 192.168.2.0
mask: 255.255.255.0
pkg:
- php
- name: db1
addr: 192.168.2.20
mask: 255.255.255.0
pkg:
- mysql
- postgres
- php
tasks:
- name: selectattr filter sample
debug:
msg:
- '{{ hosts | selectattr("name", "defined") }}'
- '{{ hosts | selectattr("name", "defined") | selectattr("name", "eq", "web1") }}'
- '{{ hosts | selectattr("name", "defined") | selectattr("name", "match", ".*1") }}'
- '{{ hosts | selectattr("pkg", "contains", "php") }}'
キーが存在するか
'{{ hosts | selectattr("name", "defined") }}'
defined
で、指定のkeyが存在する要素のみを返す。
前述hosts
の例であれば、結果は以下の通り。
ok: [localhost] =>
msg:
- - addr: 192.168.2.10
mask: 255.255.255.0
name: web1
pkg:
- httpd
- php
- python
- addr: 192.168.2.11
mask: 255.255.255.0
name: web2
pkg:
- httpd
- ruby
- python
- addr: 192.168.2.20
mask: 255.255.255.0
name: db1
pkg:
- mysql
- postgres
- php
キーname
の値との一致
'{{ hosts | selectattr("name", "defined") | selectattr("name", "eq", "web1") }}'
前述のdefined
でname
が存在するもののみをフィルタリングした結果に対し、更にname
の値がweb1
のものを返す。
ok: [localhost] =>
msg:
:
:
- - addr: 192.168.2.10
mask: 255.255.255.0
name: web1
pkg:
- httpd
- php
- python
'{{ hosts | selectattr("name", "defined") | selectattr("name", "match", ".*1") }}'
配列の場合と同様、match
やsearch
を使った正規表現も可能。
結果は以下の通り。
ok: [localhost] =>
msg:
:
:
- - addr: 192.168.2.10
mask: 255.255.255.0
name: web1
pkg:
- httpd
- php
- python
- addr: 192.168.2.20
mask: 255.255.255.0
name: db1
pkg:
- mysql
- postgres
- php
含む (contains)
'{{ hosts | selectattr("pkg", "contains", "php") }}'
指定keyの値がリスト形式の場合、その配列要素に指定した文字列や数値を含む場合の要素を返す。
例の場合、valueに配列を持つkey名pkg
に対して、その配列要素にphp
を含む場合のホスト情報がフィルタリングされる。
ok: [localhost] =>
msg:
:
:
- - addr: 192.168.2.10
mask: 255.255.255.0
name: web1
pkg:
- httpd
- php
- python
- addr: 192.168.2.0
mask: 255.255.255.0
pkg:
- php
- addr: 192.168.2.20
mask: 255.255.255.0
name: db1
pkg:
- mysql
- postgres
- php
前述のselect()
の例で使用したin
は、使い方が悪いのか空リストになった。
- name: selectattr filter sample
vars:
contain_list:
- php
- mysql
debug:
msg:
- '{{ hosts | selectattr("pkg", "in", contain_list) }}'
↑の内容のplayで実行すると以下の通り。
ok: [localhost] =>
msg:
- []
(おまけ) reject() / rejectattr()
条件に合致しない要素が欲しい場合は、逆の処理となるreject()
とrejectattr()
を使用する。
見るべきドキュメント等
select() / selectattr() フィルター本体
ソースも確認すると理解が深まる。
実は中身はどちらも(さらに言うとreject()とrejectattr()も)select_or_reject()
をコールしてるのよね。
Jinja2 Tests
まずJinja2 Templateのページ。
ここの「List of Builtin Tests」のリストにあるTestsが使用できる。
一致不一致や型チェックなどの基本的なTestはここで用意されている。
jinja.palletsprojects.com
ページ右下でバージョンを選べるが、これはAnsibleで使用しているPythonパッケージのJinja2のバージョンに合わせればよい。
Ansible Tests
Jinja2 TemplateのTest以外にAnsibleで内蔵されているTestもある。
Filterと同じくコレクション等のpluginリストにはないが、このページで一通り確認することができる。
docs.ansible.com
Testsのソースはcallbackやlookupなどのほかのpluginと同様に、testというディレクトリ以下にある。
まとめ
selectattr()
+ map()
で指定キー配下の情報のみ抽出の例はよく見かけるけど、select()
とselectattr()
についての情報があまりなかった気がするのでまとめてみた。
どちらのFilterもAnsibleではなくJinja2の機能で、使用する主要なTest(演算子)もJinja2のドキュメントに記載があるのがポイント。
また、「select()
・selectattr()
の引数で指定するのはFilterでなくTest」であることをおさえておけば、やりたいことを探しやすいと思う。(自分はここが曖昧だった)
※ 現状FilterとTestはCollection Indexから辿れず、AnsibleのFilterとTest、Jinja2のドキュメントを見るしかないと思う。
select()
・selectattr()
は、Perlだとgrep()
相当でPowerShellだとWhere-Object
相当かな。
今回は扱ってないmap
は、Perlのmap()
相当でPowerShellだとForEach-Object
相当のはず。
比較的単純なフィルタリングであればselect()
+selectattr()
で済ませて良いと思うが、抽出条件が複雑(深い位置にある値でフィルタリングして別要素の値を抜き出す、など)になってくると標準機能だけだとかえって分かりづらくなるため、json_query()
なんかを使った方が良かったりするケースも出てくるので、要件によってやり方は考慮した方がよい。
(疑問) builtin 以外の Filter / Test は…
Collection IndexからFilterとTestを辿れないということは、FilterとTestに載っていない(特にbuiltin以外の)Filter / Testの情報はどこから知ればいいか不明…
例えばcommunity.generalにあるjc
Filterとか。
zaki-hmkc.hatenablog.com
リポジトリのプラグインのディレクトリを見るしかないのかな。
サンプルplaybook (on GitHub)