えんでぃの技術ブログ

えんでぃの技術ブログ

ネットワークエンジニアの視点で、IT系のお役立ち情報を提供する技術ブログです。

Ansibleによるネットワークステータス確認101

お伝えしたいこと

この記事はAnsible Advent Calendar 2024の11日目の記事です。

AnsibleでLinuxやクラウド、ネットワーク機器の設定変更を自動化できるのは周知のとおりですが、情報取得の機能も充実しています。
充実...していますが、機能が多すぎて「やりたいこと1つに対して実現手段が2つも3つも存在する」状況になっています。

一体私たちはどの機能を使いたいのでしょうか?
私自身あまり理解できていなかったので、この場でまとめてみたいと思います。

サマリ表

下表にNW機器のステータス情報を取得する方法をまとめます。
取得できる情報は大きく分けて4種類あります。
もっと大きく分けると、(1)の文字列データと(2-1)〜(2-3)の構造化データの2つに大別できます。

network_configuration_gathering_pattern

(※) 本記事では例示にArista EOSを使います

自分が使いたい機能を見つける

以降のセクションでは、いくつかの具体的なユースケースに基づいて上表の機能を選択し、実行結果を示していきます。
少しでもイメージが湧きやすくなれば幸いです。

未加工のshowコマンドログを取得する

showコマンドの出力をそのまま見たりファイルに保存したいときは、#サマリ表の 「(1) showコマンドそのまま」 の機能を使用します。

ユースケース (1) showコマンドを未加工で保存したいのはどんな場面?

ネットワーク機器のshowコマンド出力結果を加工せずそのままファイルに保存したいケースとして、私の中では以下の2点が思い当たります。

  1. 作業やメンテナンス前後で対象機器にshowコマンドをAnsibleで一括実行し、実行結果をファイル保存する。ファイルを目視で確認して作業の成否を判断する (コマンド実行のみ自動化、確認はマニュアル)
  2. Ansibleでコンフィグを定期的に一括取得・ファイル保存する。作業者は必要なときにコンフィグを参照し、設計業務などに活用する

これらのユースケースを満たすAnsibleの具体的な実装方法を次のセクションで見ていきましょう。

実行例 (1) arista.eos.eos_command module

arista.eos.eos_command moduleでshowコマンドを実行し、結果を記録する例を示します。
今回はshow running-configを実行し、ネットワーク機器のコンフィグを表示します。

例示ではansible.builtin.debugモジュールでコマンド出力を画面表示していますが、実際に業務で使う際はansible.builtin.copyモジュールなどに置き換えてファイル保存する想定です。

- name: Retrieve Arista EOS config
  hosts: eos
  gather_facts: false
  tasks:
    - name: Execute show running-config
      arista.eos.eos_command:
        commands:
          - show running-config
      register: _eos_show_running_config_result

    - name: Debug _eos_show_running_config_result
      ansible.builtin.debug:
        msg: '{{ _eos_show_running_config_result.stdout }}'

以下に実行結果を示します。

TASK [Debug _eos_show_running_config_result] ***
ok: [veos1] => 
  msg:
  - |-
    ! Command: show running-config
    ! device: veos1 (vEOS-lab, EOS-4.26.5M)
    !
    ! boot system flash:/vEOS-lab.swi
    !
    enable password sha512 $6$xxxxxx # マスクしました
    no aaa root
# ...

showコマンドログを構造化データに変換し、活用する

showコマンドの実行結果を構造化データに変換したいときは、#サマリ表の(2-1)〜(2-3)の機能を使用します。

  • (2-1) パーサ利用
  • (2-2) network resource module
  • (2-3) gather_subset

(参考) 構造化データへの変換のイメージ

「構造化データへの変換」のイメージを補足として説明します。
既に知っている方は読み飛ばしてください。

「構造化データへの変換」がどのようなものかを理解するには、実際に動いているところを見るのが一番です。

以下のネットワーク構成を例にします。
Arista vEOSスイッチが3台接続されています。

nw_diagram1

veos1でshow interface descriptionを実行し、インターフェースのステータスを確認します。

Interface  Status  Protocol  Description
Et1        up      up        veos2 Et1
Et2        up      up        veos3 Et1

これを構造化データに変換すると、以下のようなデータ構造になります。
利用するパーサによってデータ構造は変わりますが、基本的にはlistとdictのネスト構造になります。
例えばntc_templatesやTextFSMを利用した場合は以下のようなデータ構造になります。
(※) パーサとは、文字列を構造化データとして解釈するプログラムのことです。本記事の文脈では文字列を構造化するプログラムと思って差し支えありません。ntc_templatesやTextFSMは正規表現で実装されています

interfaces:
  - port: Et1
    status: up
    protocol: up
    description: veos2 Eth1

  - port: Et2
    status: up
    protocol: up
    description: veos3 Eth1

構造化することで「特定の値を取り出す」ことが非常に容易になります。
例えばEt1のstatusは{{ interfaces.0.status }}や{{ (interfaces.selectattr("port", "eq", "Et1"))["status"] }}で取り出せます。

後者の参照方法は一見すると難しく思えるかもしれません。
しかし「同じパーサを使う限りどのshowコマンドでも基本的に同じ方法でデータを取り出せる」ので、分析対象のshowコマンドが今後増えた場合も同じフィルタを使い回せます。
構造化されていないデータを都度grepやansible.builtin.regex_search filterで都度整形するよりも遥かに少ない手間でデータを構造化できます。

...話が少し逸れましたが、データを構造化できれば夢がかなり広がります。
構造化されたデータを変数に格納して追加のロジックを組めるのです。

例えば、以下のような処理が考えられます。

  1. Et1がupなら何もせず、downならエラーメッセージを出力する (→ showコマンドの実行だけでなく確認まで自動化できる)
  2. ansible.builtin.templateモジュールで表やレポートを自動生成 (→ ステータス可視化、レポート自動生成)

2の具体例を挙げます。
上述のlist of dict形式に構造化されたデータをinputに、templateモジュールを使えば以下のmarkdownを容易に生成できます。
(※) 余談ですが、list of dictは本質的には表と同じ構造のデータなのです。なので表形式に変換することは本当に簡単です

| port | status | protocol | description |
| ---- | ------ | -------- | ----------- |
| Et1 | up | up | veos2 Eth1 |
| Et2 | up | up | veos3 Eth1 |

適切なツールでmarkdownをhtml化すると、以下のような見た目の表になります。

interface_list

showコマンドログを構造化し、整形することで見やすい表を生成できました。

ユースケース (2) showコマンドを構造化したいのはどんな場面?

#構造化データへの変換のイメージで既に触れたとおり、私が思いつくユースケースは以下の2点です。

  1. コマンド実行だけでなく、実行結果の確認まで自動化する
  2. コマンド実行結果を整形してレポートや表形式で出力する

実行例 (2-1) パーサを使う場合

さて、Ansibleでネットワーク機器のステータス情報をパースする手段は3つあります。
順番に説明していきますが、まずは私が個人的に一番好んでいる「(2) パーサ利用」について説明します。

パーサを使う場合、基本的にはansible.utils.cli_parseモジュールを使います。

以下にサンプルを示します。
show interfaces descriptionをパースするplaybookです。
今回はntc_templatesパーサを利用しているので、動作にはntc_templates Pythonパッケージが必要です。

- name: Parse EOS show commands
  hosts: eos
  gather_facts: false
  tasks:
    - name: Parse show interfaces description
      ansible.utils.cli_parse:
        command: show interfaces description
        parser:
          name: ansible.netcommon.ntc_templates
        set_fact: _show_interfaces_description_parsed

    - name: Debug _show_interfaces_description_parsed
      ansible.builtin.debug:
        msg: '{{ _show_interfaces_description_parsed }}'

実行結果は以下のようなイメージになります。
TextFSMは必ずlist[dict] (list of dicts) というデータ型に構造化します。
したがって、showコマンドが変わってもそこそこ画一的な方法でデータを加工できることが利点だと私は考えています。

TASK [Debug _show_interfaces_description_parsed] ***
ok: [veos1] => 
  msg:
  - description: veos2 Et1
    port: Et1
    protocol: up
    status: up
  - description: ''
    port: Et2
    protocol: up
    status: up
  # ...

ご参考までに、show interfaces descriptionの実行ログも再掲します。

Interface  Status  Protocol  Description
Et1        up      up        veos2 Et1
Et2        down    down      veos3 Et1

ansible.utils.cli_parse module以外にも、似た機能を持つフィルタとしてansible.netcommon.parse_cli filterやansible.netcommon.cli_parse_textfsm filterが存在します。
これらのフィルタはcli_parseモジュールよりも先にリリースされた経緯があり使われたこともありますが、対応しているパーサも限定的ですし現在ではあまり使う理由がないと思います。
フィルタについては実行例を割愛します。

実行例 (2-2) network resource moduleを使う場合

network resource moduleが存在するOSの場合、この方法が使えます。
#実行例 (2-1) パーサを使う場合と比較するとパーサ導入が不要であるため、お手軽に使い始めることができます。
ntc_templatesやTextFSMは「CLIで得られる情報であれば全て構造化できる」汎用性と、「必ずlist[dict]形式でデータを返す」統一感があるので、私個人としてはパーサ利用に統一するのが好きです。
このあたりは宗教戦争です。

同じパース結果を得られる書き方が私の知る限り3通り存在するので、順番に挙げていきます。

1つ目の書き方はarista.eos.eos_facts moduleを呼び出す方法です。
Factsを収集すると、ansible_facts変数に自動的に格納されます。

- name: Test gather facts on EOS
  hosts: eos
  gather_facts: false
  tasks:
    - name: Gather facts
      arista.eos.eos_facts:
        gather_network_resources:
          - interfaces
          - l2_interfaces
          - l3_interfaces

    - name: Debug _eos_facts_result
      ansible.builtin.debug:
        msg: '{{ ansible_facts }}'

実行結果は以下の通りです。
network_resourcesの配下がnetwork resource moduleの機能で収集した情報です。 Factsを使うと、network_resource_module以外のFacts情報も勝手に収集されます。
gather_subsetパラメータに!allを指定してもminに相当する情報は最低限収集されます。

TASK [Debug _eos_facts_result] ***
ok: [veos1] => 
  msg:
    net_api: cliconf
    net_fqdn: veos1
# ...
    net_system: eos
    net_version: 4.26.5M
    network_resources:
      interfaces:
      - description: veos2 Et1
        enabled: true
        name: Ethernet1
# ...
      l2_interfaces:
      - mode: trunk
        name: Ethernet1
        trunk:
          trunk_allowed_vlans:
          - '2'
# ...
      l3_interfaces:
# ...
      - ipv4:
        - address: 192.168.0.11/24
        name: Management1

2つ目の書き方は、network resource moduleのstate: gatheredを指定する方法です。
変数格納するにはregisterキーワードが必要です。

1つのFactsだけを取得するときはこの書き方でも良いと思います。
複数のFactsを収集する場合は、arista.eos.eos_factsの方がコンパクトに書けると思います。

- name: Gather EOS Facts
  hosts: eos
  gather_facts: false
  tasks:
    - name: Gather interfaces Facts
      arista.eos.eos_interfaces:
        state: gathered
      register: _eos_interfaces_result

    - name: Debug _eos_facts_result
      ansible.builtin.debug:
        msg: '{{ _eos_interfaces_result.gathered }}'

3つ目の書き方は、gather_factsを指定する方法です。
書き方がやや特殊で、個人的に覚えにくいです。
またFactsの収集タイミングがPlayの冒頭に固定されます。
私の環境では実機検証できていませんが、module_defaultsの配下にeos以外のOSについてもデフォルト値を指定することで複数OS分のデータ取得を1つのPlayでスッキリ表現できることがこの書き方のメリットなのだと推測します。
Gathering facts from network devices

- name: Test gather facts on EOS
  hosts: eos
  gather_facts: true
  module_defaults:
    arista.eos.eos_facts:
      gather_network_resources:
        - interfaces
        - l2_interfaces
        - l3_interfaces
  tasks:
    - name: Debug _eos_facts_result
      ansible.builtin.debug:
        msg: '{{ ansible_facts }}'

(参考) cli_parseモジュールの概要説明

cli_parseモジュールは、文字列をパースして構造化データを変数格納する機能を持ちます。
cli_parseモジュールを実行するには以下のような情報が必要となります。

cli_module_data_structure

構造化に使用するパーサ名をパラメータに指定する必要があり、選択可能なパーサとして以下の選択肢があります。

  1. ntc_templates
  2. TextFSM
  3. TTP (dmulyalin/ttp_templatesにテンプレートサンプルあり)
  4. PyATS
  5. Ansible Native Parsing Engine

parsers

パーサが複数あるとどれを使うか迷うと思います。
一概には言えませんが、「使い慣れたパーサを使いたい」、「日本語情報が多いものを使いたい」、「自分の現場にはCisco機器が多い」など様々な事情を総合して選定するものと想像します。

ご参考までに、私個人としてはansible.netcommon.ntc_templatesとansible.utils.textfsmを好んで使います。

ntc_templatesについては主要なネットワークOSのパーサ定義は既にある程度揃っていて、GitHub上に公開されています (networktocode/ntc-templates - /ntc_templates/templates)。
自分の欲しいパーサテンプレートがGitHub上に公開されていれば、まずはそれを使ってみます。

もしntc-templatesのリポジトリに既存のパーサ定義が存在しなかったり、既存のパーサ定義とは異なるデータ構造が欲しかった場合にはTextFSMテンプレートを自分で記述しつつansible.utils.textfsmを使います。
テンプレートファイルの文法はntc-templatesと同様なので、他のテンプレートファイルの書き方を参考にできます。
テンプレートファイルの文法を調べる際は、TextFSMのWiki情報やるつぼっとの記事がおすすめです。

その他、使い方の詳細はansible.utils.cli_parseのパラメータ一覧や実行例が参考になります。

実行例 (2-3) gather_subsetを使う場合

network resource moduleがAnsibleに登場する前から、eos_factsモジュールなどネットワーク機器からFactsを収集する機能は存在していました。
ややこしいですが、この機能はgather_subsetというキーワードとしてまだ残っています。
network resource moduleの方が機能としては後発ですし、恐らくgather_subset周りの機能が今後拡充されることはないと思います。
かといって廃止の予告は今のところないので、network resource moduleで取得できない情報の中でgather_subsetで取れる情報があれば、選択肢に入れてみても良いかもしれません。

くどいようですが私個人としては#実行例 (2-1) パーサを使う場合で全て賄ってしまうのが今のところは好きです。
とはいえパーサの使い方を学習するコストもありますし、一概にそれが正義だとは言えません (宗教戦争です)。

今回もFacts機能のため、書き方としてはeos_factsとgather_factsの2通りがあります。
1つずつ紹介していきます。

まずは1つ目のarista.eos.eos_factsを利用したplaybookのサンプルです。

- name: Test gather facts on EOS
  hosts: eos
  gather_facts: false
  tasks:
    - name: Gather facts
      arista.eos.eos_facts:
        gather_subset:
          - interfaces

    - name: Debug _eos_facts_result
      ansible.builtin.debug:
        msg: '{{ ansible_facts }}'

実行結果は以下の通りです。
なんだかshow interfacesをパースした感がありますね。

TASK [Debug _eos_facts_result] ***
ok: [veos1] => 
  msg:
# ...
    net_interfaces:
      Ethernet1:
        bandwidth: 1000000000
        description: veos2 Et1
        duplex: duplexFull
        ipv4: {}
        lineprotocol: up
        macaddress: 0c:65:34:fe:00:01
        mtu: 9214
        operstatus: connected
        type: bridged
# ...
    network_resources: {}

続いて2つ目のgather_factsを利用した書き方です。
Factsの取得タイミングがPlayの冒頭に固定されるものの、ネットワーク機器のOSの差分を吸収できる書き方にはなってそうです。
「AnsibleをOSごとの操作手順の違いを吸収する抽象化レイヤーとして使用する」考え方、昔は流行ってましたね。
抽象化の利点について聞くことは最近減ってきましたが、gather_factsを使用する唯一のモチベーションはそこなんじゃないかと思っています。

- name: Test gather facts on EOS
  hosts: eos
  gather_facts: true
  gather_subset:
    - interfaces
  tasks:

    - name: Debug _eos_facts_result
      ansible.builtin.debug:
        msg: '{{ ansible_facts }}'

おわりに

Ansibleのネットワーク機器の情報を集める方法は本当にたくさんあります。

showコマンドログを保存するだけなら迷いませんが、パースの手段は本当にたくさんあります。
ですが、迷ったらパーサかnetwork resource module系の機能を使いましょう。
言い換えると、#サマリ表の(2-3)は理由がなければ使わなくて良いと思います。

(Aristaのnetwork resource moduleはlldp_interfacesの情報をなぜか収集できないので、show lldp neighbors相当の情報をパースしたいときはcli_parseかgather_subsetのどちらかを使う必要があるという話はありますがそれはそのうち修正される小さなissueです)

結局のところは宗教戦争ですが、この記事がパース手段の選択の一助となれば幸いです。