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

Cisco IOS の機器などで、インターフェースへのトランクVLANの割り当て時に以下のように、範囲指定を含めて指定ができます。
interface GigabitEthernet1/3 switchport trunk allowed vlan 1,10-13,100
上記の例の場合は、VLAN 1、10、11、12、13、100 を割り当てていることが読み取れます。
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"] にというコンフィグっぽい方向に変換ができたのですが、これの逆をしたかった次第です。
[2025/07/23 追記] 現在は以下にansible.netcommon.vlan_expander フィルターのページがあります。
サンプル
簡単なサンプル 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つのフィルターとして実装すると再利用しやすくて便利だなと思いました。