Ansibleのcli_parse
モジュールで、PythonのTTP(Template Text Parser)とテンプレートファイルを使ったテキスト解析を試してみた。
ttp.readthedocs.io
cli_parse
は、2.10時点で標準のansible.netcommon.cli_parse
と、Ansible 3(3.0.0b1)に新たに標準に含まれているansible.utils.cli_parse
がある。
現状では中身は同じだが、ansible.utils
コレクション側でメンテされる(こっちに移管される?)のかな?
今回はひとまず標準で使用可能なansible.netcommon.cli_parse
を使ってみる。
ansible.utils.cli_parse
の場合はnetcommon
の場所を読み替えればOK。(のハズ)
環境
- Ansible: 2.10.5
- ansible.netcommon 1.4.1
- ttp: 0.6.0
お題
ip a
の結果をparseしてみる。
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
inet 127.0.0.1/8 scope host lo
valid_lft forever preferred_lft forever
inet6 ::1/128 scope host
valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
link/ether 00:0c:29:22:86:2f brd ff:ff:ff:ff:ff:ff
inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192
valid_lft forever preferred_lft forever
inet6 fe80::20c:29ff:fe22:862f/64 scope link
valid_lft forever preferred_lft forever
3: br-7a62608ea9c3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:78:cd:77:68 brd ff:ff:ff:ff:ff:ff
inet 172.18.0.1/16 brd 172.18.255.255 scope global br-7a62608ea9c3
valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
link/ether 02:42:12:db:d5:fd brd ff:ff:ff:ff:ff:ff
inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
valid_lft forever preferred_lft forever
inet6 fe80::42:12ff:fedb:d5fd/64 scope link
valid_lft forever preferred_lft forever
5: br-ec4e8609788c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:a0:58:03:b3 brd ff:ff:ff:ff:ff:ff
inet 172.26.0.1/16 brd 172.26.255.255 scope global br-ec4e8609788c
valid_lft forever preferred_lft forever
6: br-f28d1c1cb87b: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:03:19:f1:f8 brd ff:ff:ff:ff:ff:ff
inet 172.19.0.1/16 brd 172.19.255.255 scope global br-f28d1c1cb87b
valid_lft forever preferred_lft forever
7: br-11a5b9bc6a2c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:aa:9c:c7:a8 brd ff:ff:ff:ff:ff:ff
inet 172.20.0.1/16 brd 172.20.255.255 scope global br-11a5b9bc6a2c
valid_lft forever preferred_lft forever
inet6 fc00:f853:ccd:e793::1/64 scope global
valid_lft forever preferred_lft forever
inet6 fe80::42:aaff:fe9c:c7a8/64 scope link
valid_lft forever preferred_lft forever
inet6 fe80::1/64 scope link
valid_lft forever preferred_lft forever
8: br-1e7a6f36bebc: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:5f:6e:38:28 brd ff:ff:ff:ff:ff:ff
inet 172.21.0.1/16 brd 172.21.255.255 scope global br-1e7a6f36bebc
valid_lft forever preferred_lft forever
9: br-4221181b1c51: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:de:a0:d1:67 brd ff:ff:ff:ff:ff:ff
inet 172.22.0.1/16 brd 172.22.255.255 scope global br-4221181b1c51
valid_lft forever preferred_lft forever
44: br-d51584ee1064: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:83:4a:1d:18 brd ff:ff:ff:ff:ff:ff
inet 172.23.0.1/16 brd 172.23.255.255 scope global br-d51584ee1064
valid_lft forever preferred_lft forever
inet6 fe80::42:83ff:fe4a:1d18/64 scope link
valid_lft forever preferred_lft forever
71: br-378c264dc6ee: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
link/ether 02:42:21:be:06:4f brd ff:ff:ff:ff:ff:ff
inet 172.24.0.1/16 brd 172.24.255.255 scope global br-378c264dc6ee
valid_lft forever preferred_lft forever
inet6 fe80::42:21ff:febe:64f/64 scope link
valid_lft forever preferred_lft forever
117: vethc853031@if116: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default
link/ether ea:d7:90:4c:62:be brd ff:ff:ff:ff:ff:ff link-netnsid 0
inet6 fe80::e8d7:90ff:fe4c:62be/64 scope link
valid_lft forever preferred_lft forever
準備
ttp
TTPの利用にはPythonモジュールのttp
が必要。
無いとエラーになる。
TASK [cli parse] **************************************************************
fatal: [localhost]: FAILED! => changed=false
msg: Failed to import the required Python library (ttp) on cloud-dev's Python /home/zaki/src/ansible-sample/venv/a2.10/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter
$ pip install ttp
する。
ansible.utils
ansible.netcommon.cli_parse
でなくansible.utils.cli_parse
を使用する場合は追加でコレクションをインストールする。
$ ansible-galaxy collection install ansible.utils
pip install ansible
でインストールしたAnsible環境であればansible.netcommon.cli_parse
は標準で利用可能。
入力テキスト
cli_parse
でテキストをparseする上で、よくあるケースとしては「ネットワークコマンドの実行結果のparse」で、これはcommand
パラメタでコマンドを指定することで実現できる。
今回のip a
についても同様にcommand
で指定する。
- name: cli parse
ansible.netcommon.cli_parse:
command: ip a
なお、すでにコマンド結果をテキストで保持しており、その変数を対象にparseしたい場合はtext
で指定することもできる。
ansible.netcommon.cli_parse:
text: "{{ ip_a_result }}"
TTPの指定
parseのエンジンにTTPを指定する。
それ加えて、parse用のテンプレートファイルを指定する。
parser:
name: ansible.netcommon.ttp
template_path: "./ttp-template/ip-a.ttp"
ここでは./ttp-template/ip-a.ttp
にあるファイルを指定。
TTPテンプレートファイル
ip a
の結果に対して何をピックアップしたいかをテンプレートファイルに定義する。
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
このテンプレートを使うことで、ip a
の結果の以下の部分をparseすることができる。
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
inet 192.168.0.18/24 brd 192.168.0.255 ...
parseした結果は、{{ hogehoge }}
で定義した箇所をhogehoge: 元テキストの値
という構造化データとして扱うことができる。
ignore()
ttp.readthedocs.io
テンプレートファイルの定義は先頭一致ではなく行全体をマッチングするため、取り込みたい部分以降についても記載が必要。
その際、特に処理が不要であればignore
を指定すれば無視される。
ignore
はデフォルトで正規表現の\S
にマッチでスペースは含まれないため、「行の残り全てを無視」であれば、正規表現で.*
を指定する。
サンプル実行
- name: cli parse
ansible.netcommon.cli_parse:
command: ip a
parser:
name: ansible.netcommon.ttp
template_path: "./ttp-template/ip-a.ttp"
register: result
- debug: print result
debug:
msg: "{{ result.parsed }}"
register
を使って結果を参照している。
これを実行すると以下の通り。
ok: [localhost] =>
msg:
- - - interface: lo
mode: <LOOPBACK,UP,LOWER_UP>
mtu: '65536'
number: '1'
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
interface: ens192
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '2'
- addr: 172.18.0.1/16
broadcast: 172.18.255.255
interface: br-7a62608ea9c3
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '3'
- addr: 172.17.0.1/16
broadcast: 172.17.255.255
interface: docker0
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '4'
- addr: 172.26.0.1/16
broadcast: 172.26.255.255
interface: br-ec4e8609788c
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '5'
- addr: 172.19.0.1/16
broadcast: 172.19.255.255
interface: br-f28d1c1cb87b
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '6'
- addr: 172.20.0.1/16
broadcast: 172.20.255.255
interface: br-11a5b9bc6a2c
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '7'
- addr: 172.21.0.1/16
broadcast: 172.21.255.255
interface: br-1e7a6f36bebc
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '8'
- addr: 172.22.0.1/16
broadcast: 172.22.255.255
interface: br-4221181b1c51
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '9'
- addr: 172.23.0.1/16
broadcast: 172.23.255.255
interface: br-d51584ee1064
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '44'
- addr: 172.24.0.1/16
broadcast: 172.24.255.255
interface: br-378c264dc6ee
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '71'
- interface: vethc853031@if116
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '117'
何となくいい感じに構造化データにできた。
テンプレートの書き方いろいろ
行内の項目が有ったり無かったり
例えばinet
の行、ループバック(lo
)の場合は以下の通り。
inet 127.0.0.1/8 scope host lo
対して、ループバック以外のインタフェースについては以下の通り。
inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192
この通り、brd ブロードキャストアドレス
の項目が有ったり無かったりする。
この場合は、無理に1行でテンプレートを定義せずに(というかわからなかった)、2行に分けると簡単に定義できる。
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
このテンプレートを使用すれば、parse結果は以下の通り。
ok: [localhost] =>
msg:
- - - addr: 127.0.0.1/8
interface: lo
mode: <LOOPBACK,UP,LOWER_UP>
mtu: '65536'
number: '1'
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
interface: ens192
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '2'
- addr: 172.18.0.1/16
broadcast: 172.18.255.255
interface: br-7a62608ea9c3
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '3'
[...snip...]
この通り、ブロードキャストアドレスの記載の無いループバックのインタフェースも、inet
の行のIPアドレスを取得できている。
重要なのはbrd ブロードキャストアドレス
のテンプレートから先に記述すること。
ブロードキャストアドレスの定義がない行を先に書いてしまうと、全ての項目がこちらにマッチしてしまい、ブロードキャストアドレスのテンプレートまで処理が到達しない。
ただし、先頭行はこの方法は使えないので別の対処が必要。
先頭行の複数定義
テンプレートの先頭行を、
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
と記述すると、ループバックとens192
はマッチするが、残りのインタフェースはqlen
の記載がないためそもそも処理対象にならない。
実行すると以下の通り。
ok: [localhost] =>
msg:
- - - addr: 127.0.0.1/8
group: default
interface: lo
mode: <LOOPBACK,UP,LOWER_UP>
mtu: '65536'
number: '1'
qdisc: noqueue
qlen: '1000'
state: UNKNOWN
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
group: default
interface: ens192
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '2'
qdisc: mq
qlen: '1000'
state: UP
そしてこの場合、前述と同じように以下のように2行安直に書いても動作しない。(2行目の記述が機能しない)
※ ダメな例
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
これは、TTPのテンプレートにおける1行目の記述は「グループの開始」を表す意味も持っており、デフォルトでは1行目のみがその効果を持つ。
そこで、グループの開始の定義を複数行で行いたい場合は_start_
識別子を使う。
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
このように2行目の記述した定義に| _start_
を追加(これは試した限り行内のどの変数に入れても大丈夫そう。ドキュメントへの記載は見当たらず)する。
このテンプレートで実行すると以下の通り、qlen
の有るインタフェースも無いインタフェースもparseできている。
ok: [localhost] =>
msg:
- - - addr: 127.0.0.1/8
group: default
interface: lo
mode: <LOOPBACK,UP,LOWER_UP>
mtu: '65536'
number: '1'
qdisc: noqueue
qlen: '1000'
state: UNKNOWN
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
group: default
interface: ens192
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '2'
qdisc: mq
qlen: '1000'
state: UP
- addr: 172.18.0.1/16
broadcast: 172.18.255.255
group: default
interface: br-7a62608ea9c3
mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
mtu: '1500'
number: '3'
qdisc: noqueue
state: DOWN
- addr: 172.17.0.1/16
broadcast: 172.17.255.255
group: default
interface: docker0
mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
mtu: '1500'
number: '4'
qdisc: noqueue
state: UP
[...snip...]
<
と>
{{ mode }}
で取っている mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
の部分、普通に考えて<
と>
を取り除きたいけど、単純に
{{ number }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} ...
のようにテンプレート内に<
と>
を直接記述すると、
fatal: [localhost]: FAILED! => changed=false
msg: 'Template Text Parser returned an error while parsing. Error: not well-formed (invalid token): line 2, column 32'
とTTPの書式のエラーになってしまう。
これは、TTPのテンプレート内ではXML書式として<
と>
がメタ文字として扱われるためで、たとえば1テンプレートファイルで複数の書式を記述したい場合に
<group name="interfaces">
{{ number }}: {{ interface }}: {{ mode }} ...
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
</group>
このような使い方をするため、TTPの処理で<
と>
が期待する使われ方をしていないというエラーになっている。
回避策としてはHTMLの実体参照で記載する(これ見つけるのに半日かかった)。。。んだけど、これバグじゃね?って感じが少しするが、とりあえず現バージョン(ansible.netcommon 1.4.1 + ttp 0.6.0)だと以下の通りの動作。
ただし、HTML実体参照で動くというのもドキュメントには記載を見つけられなかったので、正しい使い方なのか未対応の処理を貫通した動作なのかは不明。
<group>
で囲む
全体を<group name="...">
などで囲む。
ttp.readthedocs.io
その上で、以下のように<{{ mode }}>
と記載する。
<group name="interfaces">
{{ number }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
</group>
そうすると、結果の構造がname
で指定したキーの辞書型になるが、HTMLの実体参照を使って<
と>
を指定したものが<
と>
にマッチして正しく動く。
結果は以下の通り。
ok: [localhost] =>
msg:
- - interfaces:
- addr: 127.0.0.1/8
group: default
interface: lo
mode: LOOPBACK,UP,LOWER_UP
mtu: '65536'
number: '1'
qdisc: noqueue
qlen: '1000'
state: UNKNOWN
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
group: default
interface: ens192
mode: BROADCAST,MULTICAST,UP,LOWER_UP
mtu: '1500'
number: '2'
qdisc: mq
qlen: '1000'
state: UP
- addr: 172.18.0.1/16
broadcast: 172.18.255.255
group: default
interface: br-7a62608ea9c3
mode: NO-CARRIER,BROADCAST,MULTICAST,UP
mtu: '1500'
number: '3'
qdisc: noqueue
state: DOWN
[...snip...]
直接HTML実体参照を記述
以下のように<group>
などで囲まずに直接HTML実体参照を記述すると、
※ ダメな例
{{ number }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
<
と>
を直接使用した場合と同じくinvalid tokenのエラーとなる。
ここでちょっと小細工して、&
をさらに実体参照で記述して、
{{ number }}: {{ interface }}: &lt;{{ mode }}> ...
と記述すると正しく動作する。
TTPの処理のソース見ればわかるかもしれないけどそこまではチェックしてないが、どうやら&
を見つけたところから処理が変わってると思われ。
ちなみに>
の方はそのままでも動作するし、試した感じでは以下のように>
と直接書いても大丈夫だった。
{{ number }}: {{ interface }}: &lt;{{ mode }}> ...
紛らわしいので、現状では<group>
で囲んだ方が良さそう。
部分的にsplit
<
と>
を除いたらこの内部の,
区切りの文字列をバラシてリストにしたいですが、はい、split
も利用可。
<group name="interfaces">
{{ number }}: {{ interface }}: <{{ mode | split(",") }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: <{{ mode | split(",") }}> mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
inet {{ addr }} {{ ignore(".*") }}
</group>
これ実行すれば以下の通り。
ok: [localhost] =>
msg:
- - interfaces:
- addr: 127.0.0.1/8
group: default
interface: lo
mode:
- LOOPBACK
- UP
- LOWER_UP
mtu: '65536'
number: '1'
qdisc: noqueue
qlen: '1000'
state: UNKNOWN
- addr: 192.168.0.18/24
broadcast: 192.168.0.255
group: default
interface: ens192
mode:
- BROADCAST
- MULTICAST
- UP
- LOWER_UP
mtu: '1500'
number: '2'
qdisc: mq
qlen: '1000'
state: UP
- addr: 172.18.0.1/16
broadcast: 172.18.255.255
group: default
interface: br-7a62608ea9c3
mode:
- NO-CARRIER
- BROADCAST
- MULTICAST
- UP
mtu: '1500'
number: '3'
qdisc: noqueue
state: DOWN
[...snip...]
split
はドキュメントに記載がない。
join
はあるのに何故だろう。。
まとめ
ということで、自前のテンプレートを用意することで、Ansibleのcli_parse
とTTPを使って任意の書式の解析と構造化データとしての取り込みはできそう。
なお、cli_parse
はTTP以外にもtextfsmなどほかのparserを使用することができるので、そのうち試したい。
関連ドキュメント
netcommon.cli_parseのドキュメントはこちら。
docs.ansible.com
コレクションのGitHubプロジェクトのドキュメントはこちら。
github.com
github.com
構造化でデータのparseについて
docs.ansible.com
TTP(Template Text Parser)
ttp.readthedocs.io
てくなべcli_parse
回
tekunabe.hatenablog.jp
サンプルコード
github.com