うさラボ

お勉強と備忘録

[Ansible] running-configをいい感じに比較をしたい(NetworkConfigを使う)

Ansible Advent Calendar 2023 10日目の記事です

qiita.com

概要

Ansibleでネットワーク機器に設定をするとき、設定後に事前に用意した想定コンフィグと比較したいときがあります。

iosのshow runは、スペースの個数で階層を表していて、単純に差分を出すだけではどこの差分か?がわからなかったりします。

例えば、こんなdiff結果

差分があるのはわかったけどどの部分かわかりにくい、、(行番号とか出るけど、まぁ、、)

今回はansible.netcommonのmodule_utilsにあるNetworkConfigクラスというものを使って、いい感じに差分を出す仕組みを考えてみました

NetworkConfigとは

netcommon/plugins/module_utils/network/common/config.pyにあるNetworkConfigクラスを使って実装します。 NetworkConfigはxxx_config系のモジュールで使われるもので、Ciscoのようなスペースで階層(親子関係)を示すコンフィグをパースしたり、Diffしたりする機能があります。

xxx_config系はこのクラスを活用して、送信前後の差分などを判定しているようです。 (本ブログではここには触れないです)
つまり、この機能をうまく使えば簡単にrunning-configの比較ができる?と考えました

github.com

ざっくり説明すると
NetworkConfigにコンテンツ(文字列)を読み込ませると、1行ずつConfigLineクラスを生成されて設定の親子関係設定をしているようです。

手順

フィルタ

NetworkConfigクラスを使って2つの機能を考えてみました

1つは、NetworkConfigのparseメソッドを使ってConfigLineクラス呼び出し、行ごとの親子関係含めた形でリストを作成します
こんなコンフィグが

!
policy-map TRAFFIC_CONTROL
 class HTTP
  police 1000000 10000 exceed-action drop
!

こんなリストになります

- policy-map TRAFFIC_CONTROL
- policy-map TRAFFIC_CONTROL class HTTP
- policy-map TRAFFIC_CONTROL class HTTP police 1000000 10000 exceed-action drop

XRのshow run formatの出力みたいなものです
この形式でコンフィグがリストになれば、差分が出しやすそうと思って作りました。(こちらは表示するまでにしてます)

2つ目は、2つのコンフィグの差分を出す機能です NetworkConfigクラスにはdifferenceメソッドがあり、matchの条件に沿った差分を出すことができます

igrnore_linesは文字通り無視する行になります、正規表現のリストを渡すことができます。

# NOTE: netcommonのNetworkConfigをインポート
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.config import (
    NetworkConfig,
)


def running_to_list(cfg, ignore_lines=None):
    ncfg = NetworkConfig(contents=cfg, ignore_lines=ignore_lines)
    return [i.line for i in ncfg.items]


def diff_config(cfg1, cfg2, match="line", ignore_lines=None):
    res = list()
    ncfg1 = NetworkConfig(contents=cfg1, ignore_lines=ignore_lines)
    ncfg2 = NetworkConfig(contents=cfg2, ignore_lines=ignore_lines)
    diff_lines = ncfg1.difference(other=ncfg2, match=match)
    for i in diff_lines:
        res.append(str(i))
    return "\n".join(res)


class FilterModule(object):
    def filters(self):
        return {
            "running_to_list": running_to_list,
            "diff_config": diff_config,
        }

用意したコンフィグ

適当なコンフィグをChatGPTに作ってもらいました、
設定が3階層(親・子・孫)くらいあればいいなぁと思って出しました

↑のコンフィグをベースに、一部のみ設定を変えたコンフィグを準備しました
ACLを一行追加したのでとTRAFFIC_CONTROLのexceed-actionをtrasmitに変更しました

2つのコンフィグに変化をつけたかっただけなので、値は適当です

before.cfg

Building configuration...

Current configuration:
!
version 15.1
service timestamps debug datetime msec
service timestamps log datetime msec
!
access-list 101 permit tcp any host 192.168.1.1 eq 80
access-list 101 deny ip any any
!
class-map match-all HTTP
 match access-group 101
!
policy-map TRAFFIC_CONTROL
 class HTTP
  police 1000000 10000 exceed-action drop
!
interface GigabitEthernet0/0
 service-policy input TRAFFIC_CONTROL
!
end

after.cfg

Building configuration...

Current configuration:
!
version 15.1
service timestamps debug datetime msec
service timestamps log datetime msec
!
access-list 101 permit tcp any host 192.168.1.1 eq 80
access-list 101 permit tcp any host 192.168.1.2 eq 80
access-list 101 deny ip any any
!
class-map match-all HTTP
 match access-group 101
!
policy-map TRAFFIC_CONTROL
 class HTTP
  police 1000000 10000 exceed-action transmit
!
interface GigabitEthernet0/0
 service-policy input TRAFFIC_CONTROL
!
end

Playbook

Playbookではlookupでコンフィグを読み込み、 読み込んだデータに対してフィルタをかけます

---
- name: 2023 Advent Carender
  hosts: localhost
  gather_facts: false
  tasks:
    - name: set data
      ansible.builtin.set_fact:
        conf1: "{{ lookup('file','before.cfg') }}"
        conf2: "{{ lookup('file','after.cfg') }}"
        match: "line"
        ignore_lines:
          - "end"
          - "Current"
          - "version"

    - name: 😊
      ansible.builtin.debug:
        msg: 
          - "{{ conf1 | running_to_list(ignore_lines) }}"
          - "{{ conf2 | running_to_list(ignore_lines) }}"

    - name: 👌
      ansible.builtin.debug:
        msg: 
          - "事前\n{{ conf1 | diff_config(conf2, match, ignore_lines) }}"
          - "事後\n{{ conf2 | diff_config(conf1, match, ignore_lines) }}"

実行結果

差分のフィルタは文字列で結果を返すので、出力を見やすくするためにcallbackをyamlに変更して実行します
親のコンフィグも結果に帰ってくるので、どの親に所属するコンフィグで差分があるか?までがわかりますね

$ ANSIBLE_STDOUT_CALLBACK=yaml ansible-playbook config.yml 
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match
'all'

PLAY [2023 Advent Carender] **********************************************************************************************

TASK [set data] **********************************************************************************************************
ok: [localhost]

TASK [😊] ****************************************************************************************************************
ok: [localhost] => 
  msg:
  - - service timestamps debug datetime msec
    - service timestamps log datetime msec
    - access-list 101 permit tcp any host 192.168.1.1 eq 80
    - access-list 101 deny ip any any
    - class-map match-all HTTP
    - class-map match-all HTTP match access-group 101
    - policy-map TRAFFIC_CONTROL
    - policy-map TRAFFIC_CONTROL class HTTP
    - policy-map TRAFFIC_CONTROL class HTTP police 1000000 10000 exceed-action drop
    - interface GigabitEthernet0/0
    - interface GigabitEthernet0/0 service-policy input TRAFFIC_CONTROL

TASK [👌] ****************************************************************************************************************
ok: [localhost] => 
  msg:
  - |-
    事前
    policy-map TRAFFIC_CONTROL
     class HTTP
      police 1000000 10000 exceed-action drop
  - |-
    事後
    access-list 101 permit tcp any host 192.168.1.2 eq 80
    policy-map TRAFFIC_CONTROL
     class HTTP
      police 1000000 10000 exceed-action transmit

PLAY RECAP ***************************************************************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

手動でDiffした結果とも同じく変更がある行のみがフィルタの結果として帰ってきています

試しにmatch変数を変更してみます。 デフォルトで選択するlineは行単位,structは行の位置,exactは厳密なチェックとなります
厳密なチェックというのは、出力全体の差分が一切ない状態です

structを試してみます、コンフィグの表示位置を無理くり変更させます interfaceの設定はpolicy-mapの設定より後に表示されるのが通常ですが、access-listの前に配置してみました

match: lineのままだと、この位置の違いは検出できませんがstrictにして実行すると 位置がずれている行も差分として検出されます

TASK [👌] ******************************************************************************************
ok: [localhost] => 
  msg:
  - |-
    事前
    interface GigabitEthernet0/0
     service-policy input TRAFFIC_CONTROL
    access-list 101 permit tcp any host 192.168.1.1 eq 80
    access-list 101 deny ip any any
    class-map match-all HTTP
     match access-group 101
    policy-map TRAFFIC_CONTROL
     class HTTP
      police 1000000 10000 exceed-action drop
  - |-
    事後
    access-list 101 permit tcp any host 192.168.1.1 eq 80
    access-list 101 permit tcp any host 192.168.1.2 eq 80
    access-list 101 deny ip any any
    class-map match-all HTTP
     match access-group 101
    policy-map TRAFFIC_CONTROL
     class HTTP
      police 1000000 10000 exceed-action transmit
    interface GigabitEthernet0/0
     service-policy input TRAFFIC_CONTROL

このフィルタを活用すれば、
事後確認用のコンフィグを手元で用意したときにも実機の出力と、どれほど差分があるか/ないかを確認することができそうです。