てくなべ (tekunabe)

ansible / network automation / 学習メモ

[Ansible] Cisco IOS の allowed vlan の 範囲表記をリストにバラす vlan_expander フィルター

この記事は Ansible Advent Calendar 2021 の12日目の記事です。

はじめに

f:id:akira6592:20211204213553p:plain

Cisco IOS の機器などで、インターフェースへのトランクVLANの割り当て時に以下のように、範囲指定を含めて指定ができます。

interface GigabitEthernet1/3
 switchport trunk allowed vlan 1,10-13,100

上記の例の場合は、VLAN 110111213100 を割り当てていることが読み取れます。

Ansibleで、この読み取り方の変換をする ansible.netcommon.vlan_expander フィルターを今年作りました。- による範囲表記を含むカンマ区切りの数字を、連番にバラして数字のリストに変換します。

ansible.netcommon collection の 2.3.0 から利用できます。

もともとは ansible.netcommon.vlan_parser というフィルターがあり、VLAN のリスト(例: [1, 10, 11, 12, 13, 100] を、["1,10-13,100"] にというコンフィグっぽい方向に変換ができたのですが、これの逆をしたかった次第です。

サンプル

簡単なサンプル Playbook をご紹介します。(ansible.netcommon collection 2.4.0 で確認)

文字列 '1,10-13,100'ansible.netcommon.vlan_expander フィルターにかけます。

---
- hosts: localhost
  gather_facts: false
  connection: local

  tasks:
    - name: vlan_expander test
      ansible.builtin.debug:
        msg: "{{ '1,10-13,100' | ansible.netcommon.vlan_expander }}"

実行結果(抜粋)です。 [1, 10, 11, 12, 13, 100] のリストにバラけます。

TASK [vlan_expander test] ****************************
ok: [localhost] => {
    "msg": [
        1,
        10,
        11,
        12,
        13,
        100
    ]
}

コンフィグとしてはないと思いますが、元の文字列が '1, 10-13, 100' のように , の後にスペースがあっても変わりません。

また、'10-13,1,100' のように、順番がバラバラでも結果のリストは昇順ソートします(上記結果と同じ)。


ユースケース: allowed vlan に特定の VLAN が入ってるかチェック

ansible.netcommon.vlan_expander フィルターと、assert モジュールを組み合わせると、allowed vlan に特定の VLAN が 入ってるかどうかをチェックできます。

応用サンプルでは、下準備としてansible/utils/cli_parse モジュールと、パーサー ntc-templates を組みあせて、show interfaces switchport の結果をパースされた構造化データとして取得します。allowed vlan された vlan の情報は、構造化データ内の trunking_vlans 配下にありますので、それに対して ansible.netcommon.vlan_expander フィルターをかけると、リストにバラせます。その後、assert モジュールin を使ってチェックすれば目的のことができます。

なお、パーサーとして ntc-templates を利用するため、あらかじめ pip install ntc-templates でインストールが必要です。

Playbook

以下の Playbook では、Gi1/3 の allowed vlan に 11 があることをチェックします。途中、デバッグのためのタスクも挟んでいます。

---
- hosts: sw01
  gather_facts: false

  tasks:
    # show interfaces switchport の実行とパース
    - name: exec and parse show interfaces switchport
      ansible.utils.cli_parse:
        command: show interfaces switchport
        parser:
          name: ansible.netcommon.ntc_templates
      register: result_switchport
    
    # Gi/3 を抽出して変数にセット
    - name: set_fact for Gi1/3
      ansible.builtin.set_fact:
        parsed_gi1_3: "{{ result_switchport.parsed | selectattr('interface', '==', 'Gi1/3') | first }}"

    # Gi/3 の情報をデバッグ表示
    - name: debug parsed_gi1_3
      ansible.builtin.debug:
        msg: "{{ parsed_gi1_3 }}"

    #  Gi1/3 の allowed vlan に 11 があることを確認
    - name: assert allowed vlan
      ansible.builtin.assert:
        that:
          - 11 in (parsed_gi1_3.trunking_vlans | first | ansible.netcommon.vlan_expander)

実行例

正常(Gi1/3 の allowed vlan に 11 がある)の場合、以下のような実行結果になります。

PLAY [sw01] ********************************************************************

TASK [exec and parse show interfaces switchport] *******************************
ok: [sw01]

TASK [set_fact for Gi1/3] ******************************************************
ok: [sw01]

TASK [debug parsed_gi1_3] ******************************************************
ok: [sw01] => {
    "msg": {
        "access_vlan": "1",
        "admin_mode": "trunk",
        "interface": "Gi1/3",
        "mode": "trunk",
        "native_vlan": "1",
        "switchport": "Enabled",
        "switchport_monitor": "",
        "switchport_negotiation": "On",
        "trunking_vlans": [
            "1,10-13,100"
        ],
        "voice_vlan": "none"
    }
}

TASK [assert allowed vlan] *****************************************************
ok: [sw01] => {
    "changed": false,
    "msg": "All assertions passed"
}

PLAY RECAP *********************************************************************
sw01   : ok=4    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

assert モジュールの that に与える条件式のバリエーションとしては

含まれることを期待する VLAN をリストで指定する場合は subset

          - "[1, 12] is subset(parsed_gi1_3.trunking_vlans | first | ansible.netcommon.vlan_expander)"

完全一致を期待する場合は

          - "[1, 10, 11, 12, 13, 100] == (parsed_gi1_3.trunking_vlans | first | ansible.netcommon.vlan_expander)"


おわりに

既存のフィルターを駆使しても類似の処理はできそうではありますが、今回はフィルターを作って ansible.netcommon collection にマージしていただきました。(ただ、CI環境の変更の都合で別の方にプルリクを出し直していただいた関係で私の名前はどこか彼方へ)

やはり1つのフィルターとして実装すると再利用しやすくて便利だなと思いました。