Contents
ネットワーク機器の非構造な結果テキストをパースできる
network-engine
ロールの command_parser
の使い方について(忘れるので)書いておこうと思います。Ansibleもくもく会で使われている演習にも使い方が書いてあります。
network-engineロール
パーサードキュメント
パースモジュール
現在、network-engineロールで使えるモジュールは以下の3つです。
- command_parser
- YAMLでパース用テンプレートを書いて、それを元にテキストをパースします
- ここでは、これを使います
- textfsm_parser
- Google TextFSM形式でパース用テンプレートを作成して、それを元にテキストをパースします
- net_facts
- ネットワーク機器から基本的なファクト機能を収集します
環境
今回はCSR1000を対象にしてやってみようと思います。
項目 | バージョン |
---|---|
Ansible | 2.8.3 |
CSR1000 | 3.15.0S(ED) |
Python | 3.6.8 |
目的
ここでは、コマンド実行で取得したrawテキスト結果を元にレポートを作成してみようと思います。
レポートを作るまでの各処理を一つずつ説明していこうと思います。
全体の処理について
ここでの全体の処理は以下のようになっています。
- SSHで接続した対象機器上でコマンドを実行
- 取得した値をregisterで変数に格納
- テンプレート用パースに上で取得した結果を渡す
- パース処理が走る
- パース結果を基にレポートを生成する
ロールのインストール
ansible-galaxy
コマンドを実行して network-engine
ロールをインストールします。
1 2 3 4 |
(venv) [root@3600ad0bdbd6 ~]# mkdir cisco (venv) [root@3600ad0bdbd6 ~]# cd cisco/ (venv) [root@3600ad0bdbd6 cisco]# ansible-galaxy install -f ansible-network.network-engine -p roles/ |
パースについて
パースは以下のディレクティブ順番で処理します。
parser_metadata
ではメタ情報を設定しますpattern_match
ではパースする対象の文字列を取得しますpattern_group
ではpattern_matchで取得した文字列をループして解析しますjson_template
ではpattern_groupで解析した結果を元に任意のjson形式にします
それでは、一つずつやってみましょう。
inventoryについて
inventoryは以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
(venv) [root@3600ad0bdbd6 cisco]# vi inventory [cisco] csr1000 ansible_host=192.168.0.68 [cisco:vars] ansible_user=admin ansible_password=secret ansible_connection=network_cli ansible_network_os=ios ansible_become=yes ansible_become_password=secret ansible_become_method=enable |
対象rawテキストデータ
ここでは show run
で取得できたinterfaceを元にレポートを生成します。
そのため、以下の範囲が対象です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
csr1000#show running-config (snip) ! interface GigabitEthernet1 ip address 192.168.0.68 255.255.255.0 negotiation auto ! interface GigabitEthernet2 no ip address shutdown negotiation auto vlan-id dot1q 10 ! vlan-id dot1q 20 ! vlan-id dot1q 30 ! ! interface GigabitEthernet3 no ip address shutdown negotiation auto ! (snip) |
parser_matchについて
まずは、ロールとパース用テンプレートを呼び出すPlaybookを作成します。
Playbookでは shor run
コマンドを実行して取得した結果をパースします。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
--- - name: ansible-network.network-engine role test hosts: cisco gather_facts: no roles: - ansible-network.network-engine tasks: - name: execute command 'show run' on csr1000 ios_command: commands: - show run register: show_run_result - name: parse the results of 'show run' command command_parser: file: "parsers/main.yml" content: "{{ show_run_result.stdout[0] }}" - debug: var=section |
次にパース用のテンプレートファイルを作成します。
1 2 3 |
(venv) [root@3600ad0bdbd6 cisco]# mkdir parsers (venv) [root@3600ad0bdbd6 cisco]# vi parsers/main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
--- - name: parser meta data parser_metadata: version: 1.0 command: show run network_os: ios - name: match sections pattern_match: regex: "^interface" match_all: true match_greedy: true export: true register: section |
parser_metadataオプション
オプション | 説明 |
---|---|
version | バージョンを指定 |
command | 実行したコマンドを指定 |
network_os | ネットワーク機器のOSタイプを指定 |
pattern_matchオプション
オプション | 説明 |
---|---|
regex | コマンド結果から抜き出したい対象の文字列部分の正規表現を指定 |
match_all | 正規表現にマッチしたものをすべて取得する場合は true |
match_greedy | 正規表現にマッチした部分ではなく、それ以降も取得する場合は true |
match_until | 正規表現にマッチした部分で処理を終了させる場合は true |
export | 呼び出したPlaybookでパース用テンプレート内のregisterに登録した変数を呼び出せるようにする場合は true |
今回はinterface情報をパースしたいので正規表現では ^interface
を指定しています。
parser_matchの動きを確認する
それでは parser_match
の動きを確認してみましょう。
作成したPlaybookを実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 |
(venv) [root@3600ad0bdbd6 cisco]# ansible-playbook main.yml -i inventory (snip) TASK [debug] ********************************************************************************************************************************************************************* ok: [csr1000] => { "section": [ "interface GigabitEthernet1\n ip address 192.168.0.68 255.255.255.0\n negotiation auto\n!\n", "interface GigabitEthernet2\n no ip address\n shutdown\n negotiation auto\n vlan-id dot1q 10\n !\n vlan-id dot1q 20\n !\n vlan-id dot1q 30\n !\n!\n", "interface GigabitEthernet3\n no ip address\n shutdown\n negotiation auto\n!\n!\nvirtual-service csr_mgmt\n!\nip forward-protocol nd\n!\nno ip http server\nno ip http secure-server\n!\n!\n!\n!\ncontrol-plane\n!\n !\n !\n !\n !\n!\n!\n!\n!\n!\nline con 0\nline vty 0 4\n login local\n transport input ssh\n!\n!\nend" ] } (snip) |
このように interface
にマッチした部分をすべて取得しています。
例えば match_greedy
を false
にしてみましょう。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi parsers/main.yml |
1 2 3 4 5 6 7 8 9 10 |
(snip) - name: match sections pattern_match: regex: "^interface" match_all: true match_greedy: false export: true register: section (snip) |
実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
(venv) [root@3600ad0bdbd6 cisco]# ansible-playbook main.yml -i inventory (snip) TASK [debug] ********************************************************************************************************************************************************************* ok: [csr1000] => { "section": [ { "matches": "interface" }, { "matches": "interface" }, { "matches": "interface" } ] } (snip) |
match_greedy
を false
にすると正規表現にマッチする部分のみしか取得できないことがわかります。
これだとinterfaceの情報を取得できないので true
にします。
pattern_groupについて
パース用のテンプレートに pattern_group
処理を追加します。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi parsers/main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
--- - name: parser meta data parser_metadata: version: 1.0 command: show run network_os: ios - name: match sections pattern_match: regex: "^interface" match_all: true match_greedy: true export: true register: section - name: match interface values pattern_group: - name: interface name pattern_match: regex: "interface (.*)" content: "{{ item }}" register: interface_name - name: ip address pattern_match: regex: "ip address (.*)" content: "{{ item }}" register: ip_address - name: vlan pattern_match: regex: "vlan-id dot1q (.*)" content: "{{ item }}" match_all: true register: vlan_id - name: port status pattern_match: regex: ".*(shu.*)" content: "{{ item }}" register: port_status - name: negotiation pattern_match: regex: "negotiation (.*)" content: "{{ item }}" register: negotiation loop: "{{ section }}" export: true register: interfaces |
pattern_group
では pattern_match
で取得した section変数
に格納された値を元に解析の処理をします。
例えばsectionの0番目のデータを見てみます。
1 2 3 4 |
"section": [ "interface GigabitEthernet1\n ip address 192.168.0.68 255.255.255.0\n negotiation auto\n!\n", (snip) |
interface name
の処理で指定している正規表現は interface (.*)
です。
(.*)
にマッチしたものがregisterの変数に格納されます。
ここでは GigabitEthernet1
が格納されます。
ip address
では 192.168.0.68 255.255.255.0
が格納されます。
negotiation
では auto
が格納されます。
vlan
と port status
は正規表現にマッチしないので null
が格納されます。
pattern_groupの動きを確認する
表示する変数を変えるためPlaybookのdebug部分を修正します。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi main.yml |
1 2 3 4 |
(snip) - debug: var=interfaces (snip) |
実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
(venv) [root@3600ad0bdbd6 cisco]# ansible-playbook main.yml -i inventory (snip) TASK [debug] ********************************************************************************************************************************************************************* ok: [csr1000] => { "interfaces": [ { "interface_name": { "matches": [ "GigabitEthernet1" ] }, "ip_address": { "matches": [ "192.168.0.68 255.255.255.0" ] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [] }, "vlan_id": null }, { "interface_name": { "matches": [ "GigabitEthernet2" ] }, "ip_address": { "matches": [] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [ "shutdown" ] }, "vlan_id": [ { "matches": "10" }, { "matches": "20" }, { "matches": "30" } ] }, { "interface_name": { "matches": [ "GigabitEthernet3" ] }, "ip_address": { "matches": [] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [ "shutdown" ] }, "vlan_id": null } ] } |
pattern_groupで作成した interfaces
変数の中身は上のようになりました。
これで、任意のjson形式にできるための構造化ができました。
これを元にレポートを生成してもいいですが、もっと分かりやすい(扱いやすい)形にしてみようと思います。
json_templateについて
パース用のテンプレートに json_template
処理を追加します。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi parsers/main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 |
--- - name: parser meta data parser_metadata: version: 1.0 command: show run network_os: ios - name: match sections pattern_match: regex: "^interface" match_all: true match_greedy: true export: true register: section - name: match interface values pattern_group: - name: interface name pattern_match: regex: "interface (.*)" content: "{{ item }}" register: interface_name - name: ip address pattern_match: regex: "ip address (.*)" content: "{{ item }}" register: ip_address - name: vlan pattern_match: regex: "vlan-id dot1q (.*)" content: "{{ item }}" match_all: true register: vlan_id - name: port status pattern_match: regex: ".*(shu.*)" content: "{{ item }}" register: port_status - name: negotiation pattern_match: regex: "negotiation (.*)" content: "{{ item }}" register: negotiation loop: "{{ section }}" export: true register: interfaces - name: generate json data structure json_template: template: - key: "{{ item.interface_name.matches.0 }}" object: - key: ip_address value: "{{ item.ip_address }}" - key: vlan_id value: "{{ item.vlan_id }}" - key: port_status value: "{{ item.port_status }}" - key: negotiation value: "{{ item.negotiation }}" loop: "{{ interfaces }}" export: true export_as: dict register: interfaces_facts |
json_templateオプション
オプション | 説明 |
---|---|
key | key名を指定 |
value | keyに紐付ける値を指定 |
object | 辞書をネストする場合は指定 |
export_as | 呼び出したPlaybook側に戻す型を指定 |
json_template
では pattern_group
で作成した interfaces変数
を元に任意のjson構造を作成できます。
上のように key
value
型で指定します。
辞書がネストする時は object
を使います。
json_templateの動きを確認する
表示する変数を変えるためPlaybookのdebug部分を修正します。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi main.yml |
1 2 3 4 |
(snip) - debug: var=interfaces_facts (snip) |
実行してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
(venv) [root@3600ad0bdbd6 cisco]# ansible-playbook main.yml -i inventory (snip) TASK [debug] ********************************************************************************************************************************************************************* ok: [csr1000] => { "interfaces_facts": { "GigabitEthernet1": { "ip_address": { "matches": [ "192.168.0.68 255.255.255.0" ] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [] }, "vlan_id": "" }, "GigabitEthernet2": { "ip_address": { "matches": [] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [ "shutdown" ] }, "vlan_id": [ { "matches": "10" }, { "matches": "20" }, { "matches": "30" } ] }, "GigabitEthernet3": { "ip_address": { "matches": [] }, "negotiation": { "matches": [ "auto" ] }, "port_status": { "matches": [ "shutdown" ] }, "vlan_id": "" } } } (snip) |
任意のjson構造が作成できました 🙂
これを元にレポートを生成してみようと思います。
レポートを作る
まずは、レポート用のテンプレート(j2)を作成します。
1 2 3 |
(venv) [root@3600ad0bdbd6 cisco]# mkdir templates (venv) [root@3600ad0bdbd6 cisco]# vi templates/report.j2 |
レポート用のテンプレートは以下のようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
interfaces: {% for key, value in interfaces_facts.items() %} - inteface: name: {{ key }} {% if value.ip_address.matches %} ip_address: {% for ip in value.ip_address.matches %} - {{ ip }} {% endfor %} {% endif %} {% if value.vlan_id %} vlan_id: {% for vlan_id in value.vlan_id %} - {{ vlan_id.matches }} {% endfor %} {% endif %} {% if value.port_status.matches %} port_status: {{ value.port_status.matches.0 }} {% endif %} negotiation: {{ value.negotiation.matches.0 }} {% endfor %} |
次にPlaybookへtemplateモジュール処理を追加します。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# vi main.yml |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
--- - name: ansible-network.network-engine role test hosts: cisco gather_facts: no roles: - ansible-network.network-engine tasks: - name: execute command 'show run' on csr1000 ios_command: commands: - show run register: show_run_result - name: parse the results of 'show run' command command_parser: file: "parsers/main.yml" content: "{{ show_run_result.stdout[0] }}" - name: generate report file template: src: "templates/report.j2" dest: report.yml |
これを実行してみます。
1 2 |
(venv) [root@3600ad0bdbd6 cisco]# ansible-playbook main.yml -i inventory |
report.yml
が作成されているので中身を確認してみます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
interfaces: - inteface: name: GigabitEthernet1 ip_address: - 192.168.0.68 255.255.255.0 negotiation: auto - inteface: name: GigabitEthernet2 vlan_id: - 10 - 20 - 30 port_status: shutdown negotiation: auto - inteface: name: GigabitEthernet3 port_status: shutdown negotiation: auto |
これで、YAMLのレポートが作成できました 🙂
このように非構造データをパースし任意のjson構造へ変換することができます。
その後は、templateモジュールを使って棚卸または現状把握用のレポートを生成したり取得したデータを元に設定変更をする処理を書いたりすることができて便利ですね 🙂
みんなでハッピーオートメーション!
追記
今回作成したファイルをGitHubに保存しました。