はじめに
以前の記事で、Jinja2テンプレートを使ったConfig自動生成を行いました。こちらはCSV形式のパラメータ表とJinja2テンプレートを、Pythonを使ってレンダリングしてConfig生成した例です。
ポート管理表+パラメータ表+Jinja2テンプレートから、L2SWのConfigを自動生成してみた
一方よこちさんの記事で、Pythonの代わりにAnsibleのtemplateモジュールを使った例も紹介頂いています。
[Ansible] 「ポート管理表+パラメータ表+Jinja2テンプレートから、L2SWのConfigを自動生成してみた」を Ansible で
各方法のメリットは以下の通りかと思います。
Python
- AnsibleをインストールできないWindows環境でも実行できる
Ansible
- Pythonの知識はなくてもOK
- Ansible独自のJinja2テンプレート(フィルタープラグイン)を使って効率的にConfig生成ができる
(例)ipaddr filterでプレフィックス/24
をサブネットネットマスク255.255.255.0
やワイルドカードマスク0.0.0.255
に変換可能 - 生成したConfigをそのままAnsibleで流し込みできる
本記事では、Ansibleの例を掘り下げ、CSV形式のACL要件資料とJinja2テンプレートから「名前付き拡張ACL」のConfigファイルを生成し、後続ステップで設定変更を行ってみました(下図)。対象OSはCisco IOS、NX-OS、ASAの3種類です。
※作ったものはGitHub/acl-jinja2-templateにアップロードしています。
1. ACL要件資料
3種類の機器で6要件ずつ、計18要件分のConfigを生成してみます。
各項目の内容は以下の通りです。
項目名 | 内容 |
---|---|
hostname | 対象機器のホスト名 |
change | 設定変更種別 例:add(追加)、delete(削除) |
acl_type | ACL種別 例:name_extended(名前付き拡張)、name_standard(名前付き標準)、number_extended(番号付き拡張)、number_standard(番号付き標準)※現時点の実装はname_extendedのみ |
acl_name | ACL名 or ACL番号 指定しない場合、ACLコンフィグレーションモード配下のコマンド(permit/deny ~)のみ生成 |
seq_no | シーケンス番号(任意) |
action | 通信許可 or 拒否 例:permit、deny |
protocol | プロトコル 例:ip、tcp、udp |
src_addr | 送信元アドレス アドレス/プレフィックス長の形式で指定 |
src_oper | 送信元演算子(任意) 例:eq、gt、lt、range |
src_port | 送信元ポート番号(任意) rangeの場合、FromとToをスペースを空けて指定 |
dest_addr | 宛先アドレス アドレス/プレフィックス長の形式で指定 |
dest_oper | 宛先演算子(任意) 例:eq、gt、lt、range |
dest_port | 宛先ポート番号(任意) rangeの場合、FromとToをスペースを空けて指定 |
modifier | 追加オプション(任意) 例:log、established、log-input |
description | リマーク設定(任意) 該当ACLの前に定義 |
2. Jinja2テンプレート
ファイルはacl_template.j2です。詳細は割愛しますが、意識したポイントは以下の通りです。
-
Jinja2テンプレートの可読性を高めるため、インデントを付与。
-
Jinja2のWhitespace Controlを利用し、実際のConfigとインデントが同じになるように調整。
具体的には、例えば{% if ... -%}
のように、ifの後に-
を付けることで、後ろの改行とスペースが削られるようにしています。合わせて後述のAnsible Playbook内で、templateモジュールのオプション「lstrip_blocks」を有効化することで、ブロック行{% block %}
の左のWhitespaceを取り除いています。 -
任意設定が空データの場合は何も追加せず、逆にデータがある場合は「スペース+設定」を追加することで、スペースが2つ以上連ならないよう工夫。
-
ipaddr filterを使い、
{{ アドレス/プレフィックス | ipaddr('network') }}
でアドレスのみ抽出。また、IOS ACLでは{{ アドレス/プレフィックス | ipaddr('wildcard') }}
を使ってプレフィックスからワイルドカードマスクを生成し、ASA ACLでは{{ アドレス/プレフィックス | ipaddr('netmask') }}
を使ってプレフィックスからサブネットマスクを抽出しています。
(本フィルタを使用するには、pip install netaddr
でPythonパッケージのインストールが必要。) -
どのテンプレートを使うかは、Ansible変数
ansible_network_os
を元に判定。
3. Ansible Inventoryファイル
以前の記事でVIRL上に構築したIOSv、NX-OSv9000、ASAvを使用しました。使用したAnsibleバージョンは2.9.0です。
4. ACL Config作成
本項でまず、Ansibleを使ったACL Config作成を行います。
4-1. Ansible Playbook
各タスクの内容は以下の通りです。
-
read_csv
モジュールを使い、[{ "hostname": "xx", "acl_name": "aa", ~ }, { "hostname": "yy", "acl_name": "bb", ~ }, ~]
のように、リスト内の要素として、CSVの各行のデータが辞書形式で定義された形に変換。 -
debug
モジュールで、タスク1のリストを表示。 -
set_fact
モジュールで、上記のリストをJinja2テンプレート内で読み込めるよう変数acls
に格納。 -
template
モジュールで、レンダリングした結果をテキストファイルとして出力。
(このタイミングで、人手でレビューすることを想定。)
---
- hosts: all
gather_facts: no
connection: network_cli
vars:
template: acl_template.j2
acl_list: add/acl_requests1.csv
config_filename: "add/acl_config_{{ inventory_hostname }}.txt"
tasks:
- name: read acl requests from CSV file and return a list # (1)
read_csv:
path: "{{ acl_list }}"
run_once: yes
register: acl_requests
- name: display acl requests # (2)
debug:
msg: "{{ acl_requests.list }}"
run_once: yes
- name: set acl requests # (3)
set_fact:
acls: "{{ acl_requests.list }}"
run_once: yes
- name: build template # (4)
template:
src: "{{ template }}"
dest: "{{ config_filename }}"
lstrip_blocks: yes
4-2. Playbook実行ログ
タスク2の出力結果を見ると、問題なくリストが出力されている事が分かります。
$ ansible-playbook -i inventory_virl1.ini playbook_aclgen.yml
PLAY [all] ***************************************************************************************************
TASK [read acl requests from CSV file and return a list] *****************************************************
ok: [asav-1]
TASK [display acl requests] **********************************************************************************
ok: [asav-1] => {
"msg": [
{
"acl_name": "TEST1",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST1",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "iosv-1",
"modifier": "",
"protocol": "udp",
"seq_no": "110",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "iosv-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "120",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "iosv-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "130",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST2",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST2",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "iosv-1",
"modifier": "",
"protocol": "udp",
"seq_no": "",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "iosv-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "iosv-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST1",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST1",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "nx-osv9000-1",
"modifier": "",
"protocol": "udp",
"seq_no": "110",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "nx-osv9000-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "120",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "nx-osv9000-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "130",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST2",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST2",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "nx-osv9000-1",
"modifier": "",
"protocol": "udp",
"seq_no": "",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "nx-osv9000-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "nx-osv9000-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST1",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST1",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "asav-1",
"modifier": "",
"protocol": "udp",
"seq_no": "1",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST1",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "asav-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "2",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "TEST1",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "asav-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "3",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST2",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "ACL_TEST2",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "ntp",
"hostname": "asav-1",
"modifier": "",
"protocol": "udp",
"seq_no": "",
"src_addr": "192.168.1.1/32",
"src_oper": "",
"src_port": ""
},
{
"acl_name": "TEST2",
"acl_type": "name_extended",
"action": "deny",
"change": "add",
"description": "",
"dest_addr": "192.168.2.1/32",
"dest_oper": "eq",
"dest_port": "www",
"hostname": "asav-1",
"modifier": "",
"protocol": "tcp",
"seq_no": "",
"src_addr": "192.168.1.2/32",
"src_oper": "range",
"src_port": "1024 65535"
},
{
"acl_name": "TEST2",
"acl_type": "name_extended",
"action": "permit",
"change": "add",
"description": "",
"dest_addr": "192.168.2.0/24",
"dest_oper": "",
"dest_port": "",
"hostname": "asav-1",
"modifier": "log",
"protocol": "ip",
"seq_no": "",
"src_addr": "192.168.1.0/24",
"src_oper": "",
"src_port": ""
}
]
}
TASK [set acl requests] **************************************************************************************
ok: [asav-1]
TASK [build template] ****************************************************************************************
changed: [nx-osv9000-1]
changed: [iosv-1]
changed: [asav-1]
PLAY RECAP ***************************************************************************************************
asav-1 : ok=4 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
iosv-1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
nx-osv9000-1 : ok=1 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
4-3. 生成されたConfigファイル
いずれも問題なく作成されました。
ip access-list extended TEST1
remark ACL_TEST1
110 deny udp host 192.168.1.1 host 192.168.2.1 eq ntp
120 deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www
130 permit ip 192.168.1.0 0.0.0.255 192.168.2.0 0.0.0.255 log
ip access-list extended TEST2
remark ACL_TEST2
deny udp host 192.168.1.1 host 192.168.2.1 eq ntp
deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www
permit ip 192.168.1.0 0.0.0.255 192.168.2.0 0.0.0.255 log
ip access-list TEST1
105 remark ACL_TEST1
110 deny udp 192.168.1.1/32 192.168.2.1/32 eq ntp
120 deny tcp 192.168.1.2/32 range 1024 65535 192.168.2.1/32 eq www
130 permit ip 192.168.1.0/24 192.168.2.0/24 log
ip access-list TEST2
remark ACL_TEST2
deny udp 192.168.1.1/32 192.168.2.1/32 eq ntp
deny tcp 192.168.1.2/32 range 1024 65535 192.168.2.1/32 eq www
permit ip 192.168.1.0/24 192.168.2.0/24 log
access-list TEST1 remark ACL_TEST1
access-list TEST1 line 1 extended deny udp host 192.168.1.1 host 192.168.2.1 eq ntp
access-list TEST1 line 2 extended deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www
access-list TEST1 line 3 extended permit ip 192.168.1.0 255.255.255.0 192.168.2.0 255.255.255.0 log
access-list TEST2 remark ACL_TEST2
access-list TEST2 extended deny udp host 192.168.1.1 host 192.168.2.1 eq ntp
access-list TEST2 extended deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www
access-list TEST2 extended permit ip 192.168.1.0 255.255.255.0 192.168.2.0 255.255.255.0 log
5. ACL設定変更
本項では4.で生成したConfigファイルを、Ansibleのxxx_config
モジュールのsrcオプションで指定して設定変更を行います。
5-1. Ansible Playbook
各タスクの内容は以下の通りです。
-
ios_config
モジュールでACL設定変更&設定保存。when
でIOSの場合のみ実行。matchオプションをnone
とし、冪等性が機能しないようにしていますが、これは自動生成したConfigファイル内のip access-list ~
が冪等性によって設定対象から除外され、エラーになるのを回避するための苦肉の策です。。 -
nxos_config
モジュールで1.と同様の処理を実行。NX-OSの場合のみ実行。冪等性× -
asa_config
モジュールで1.と同様の処理を実行。ASAの場合のみ実行。冪等性〇 - ベンダ非依存モジュール
cli_command
で、設定変更後のRunning Configを取得。 -
debug
モジュールで4.の取得結果を表示。
---
- hosts: all
gather_facts: no
connection: network_cli
vars:
config_filename: "add/acl_config_{{ inventory_hostname }}.txt"
tasks:
- name: change acls on remote device (ios) # (1)
ios_config:
src: "{{ config_filename }}"
match: none
save_when: modified
when: ansible_network_os == 'ios'
- name: change acls on remote device (nxos) # (2)
nxos_config:
src: "{{ config_filename }}"
match: none
save_when: modified
when: ansible_network_os == 'nxos'
- name: change acls on remote device (asa) # (3)
asa_config:
src: "{{ config_filename }}"
save: yes
when: ansible_network_os == 'asa'
- name: run show command on remote device # (4)
cli_command:
command: show run | begin TEST
register: result
- name: debug # (5)
debug:
msg:
- "{{ result.stdout_lines }}"
5-2. Playbook実行ログ
タスク5の出力結果を見ると、問題なくACL設定変更されている事が分かります。
$ ansible-playbook -i inventory_virl1.ini playbook_aclconf.yml
PLAY [all] ***************************************************************************************************
TASK [change acls on remote device (ios)] ********************************************************************
skipping: [asav-1]
skipping: [nx-osv9000-1]
changed: [iosv-1]
TASK [change acls on remote device (nxos)] *******************************************************************
skipping: [asav-1]
skipping: [iosv-1]
changed: [nx-osv9000-1]
TASK [change acls on remote device (asa)] ********************************************************************
skipping: [iosv-1]
skipping: [nx-osv9000-1]
changed: [asav-1]
TASK [run show command on remote device] *********************************************************************
ok: [asav-1]
ok: [nx-osv9000-1]
ok: [iosv-1]
TASK [debug] *************************************************************************************************
ok: [iosv-1] => {
"msg": [
[
"ip access-list extended TEST1",
" remark ACL_TEST1",
" deny udp host 192.168.1.1 host 192.168.2.1 eq ntp",
" deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www",
" permit ip 192.168.1.0 0.0.0.255 192.168.2.0 0.0.0.255 log",
"ip access-list extended TEST2",
" remark ACL_TEST2",
" deny udp host 192.168.1.1 host 192.168.2.1 eq ntp",
" deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www",
" permit ip 192.168.1.0 0.0.0.255 192.168.2.0 0.0.0.255 log",
(省略)
]
]
}
ok: [asav-1] => {
"msg": [
[
"access-list TEST1 extended deny udp host 192.168.1.1 host 192.168.2.1 eq ntp ",
"access-list TEST1 extended deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www ",
"access-list TEST1 extended permit ip 192.168.1.0 255.255.255.0 192.168.2.0 255.255.255.0 log ",
"access-list TEST1 remark ACL_TEST1",
"access-list TEST2 remark ACL_TEST2",
"access-list TEST2 extended deny udp host 192.168.1.1 host 192.168.2.1 eq ntp ",
"access-list TEST2 extended deny tcp host 192.168.1.2 range 1024 65535 host 192.168.2.1 eq www ",
"access-list TEST2 extended permit ip 192.168.1.0 255.255.255.0 192.168.2.0 255.255.255.0 log ",
(省略)
]
]
}
ok: [nx-osv9000-1] => {
"msg": [
[
"ip access-list TEST1",
" 105 remark ACL_TEST1",
" 110 deny udp 192.168.1.1/32 192.168.2.1/32 eq ntp ",
" 120 deny tcp 192.168.1.2/32 range 1024 65535 192.168.2.1/32 eq www ",
" 130 permit ip 192.168.1.0/24 192.168.2.0/24 log ",
"ip access-list TEST2",
" 10 remark ACL_TEST2",
" 20 deny udp 192.168.1.1/32 192.168.2.1/32 eq ntp ",
" 30 deny tcp 192.168.1.2/32 range 1024 65535 192.168.2.1/32 eq www ",
" 40 permit ip 192.168.1.0/24 192.168.2.0/24 log ",
(省略)
]
]
}
PLAY RECAP ***************************************************************************************************
asav-1 : ok=3 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
iosv-1 : ok=3 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
nx-osv9000-1 : ok=3 changed=1 unreachable=0 failed=0 skipped=2 rescued=0 ignored=0
最後に
今回は設定追加の例でしたが、ACL要件資料の項目change
をdelete
とすることで、設定削除も問題なくできました。(結果はGitHubのdeleteフォルダにアップしています。)
今回のような例だと、どうしてもJinja2テンプレートが複雑になってしまいますが、一度作ってしまえば、パラメーター表から簡単にConfig生成ができるようになるので、作業頻度の高いものは作ってみてはいかがでしょうか。
ただし、ios_bgp
モジュールのように、既にモジュールが存在する設定項目は、そちらを使った方が楽だと思います。