Ansibleのcli_parse
モジュールで、PythonのTTP(Template Text Parser)とテンプレートファイルを使ったテキスト解析を試してみた。
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
入力テキスト
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()
テンプレートファイルの定義は先頭一致ではなく行全体をマッチングするため、取り込みたい部分以降についても記載が必要。
その際、特に処理が不要であれば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="...">
などで囲む。
その上で、以下のように<{{ 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のドキュメントはこちら。
コレクションのGitHubプロジェクトのドキュメントはこちら。
構造化でデータのparseについて
TTP(Template Text Parser)
てくなべcli_parse
回