Ansible+Batfishで必要なACLだけ設定出来るようにしてみた

# はじめに

以前の記事で、CSV形式のACL要件資料とJinja2テンプレートから、Configファイル生成と設定変更を行いました。
[AnsibleでACL Config作成＆設定変更を自動化してみた](https://qiita.com/tech_kitara/items/c098306c7cfcdeb4496f)

実際のACL運用では、同じACLが既に許可されていたり、より大きな範囲で許可されていたりしないか、事前確認するケースが多いと思います。本記事では、この辺をBatfishというConfig解析ツールを使ってチェックし、未許可のACLのみ設定出来るようにしてみました。（下図の「1. ACL Config解析」と「2. ACL Config作成」）


![無題1207_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/39e6a599-747f-1243-a79e-628814349521.png)


# 1. セットアップ

## 1-1. Ansible
CentOS Python3.6.8の仮想環境上に、Ansible2.8.4をインストールしました。

## 1-2. Batfish
BatfishはDockerイメージの形で提供されています。Batfish本体のみの`batfish/batfish`と、Jupyter Notebookの実行環境とチュートリアルを含んだ `batfish/allinone`の2種類がありますが、今回は簡単にお試しできる後者を使用しました。

セットアップ方法は、[公式ドキュメント](https://github.com/batfish/batfish/blob/master/README.md)に加え、以下の記事を参考にさせて頂きました。
[[Batfish] ネットワーク機器のコンフィグを読み込んでルーティングなどの様々な検証ができるツール「Batfish」の紹介](https://tekunabe.hatenablog.jp/entry/2018/10/25/batfish)

また今回は、Ansible用ディレクトリ配下にBatfish用ディレクトリを作りたかったので、以下記事も参考にしています。
[[Batfish] allinone イメージの Jupyter Notebook から自前のコンフィグディレクトリを利用可能にする方法](https://tekunabe.hatenablog.jp/entry/2018/12/08/batfish_mydata_mout)

合わせて、Ansible GalaxyからBatfish用のロールをインストールしました。

```
$ ansible-galaxy install --force batfish.base
```

## 1-3. ディレクトリ構成
各種ファイルのディレクトリ構成は以下の通りです。

```
ansible/
　├ inventory.ini (Ansible Inventoryファイル)
　├ playbook_batfish6.yml (Ansible Playbook1)
　├ playbook_aclgen_batfish.yml (Ansible Playbook2)
　├ acl_requests_batfish.csv (ACL要件資料)
　├ acl_requests_batfish.j2 (Jinja2テンプレート)
　├ mynetworks/ (BatfishのSnapshot保管用ディレクトリ)
　│　└ test1/
　│　　　└ configs/
　│　　  　└ [ホスト名]_sh run.log (既存Configファイル)
　├ acl_requests_not_permitted.txt (ACL要件 未許可のもの)
　└ add/
　│　└ acl_config_[ホスト名].txt (追加Configファイル)
```

Jinja2テンプレートは[前回](https://github.com/Tachashi/acl-jinja2-template/blob/master/acl_template.j2)と同じものを使いました。それ以外については次項以降でご説明します。

# 2. ACL設定

## 2-1. 既存Configファイル
既存のホスト名`hqdist1`(Catalyst3560)に、名前付き拡張ACL×2行を適用しています。

```text:hqdist1_shrun.log
interface Vlan101
 description << Server Segment >>
 ip address 192.168.101.49 255.255.255.0
 ip access-group ACL_TEST out
!
ip access-list extended ACL_TEST
 permit ip 10.10.1.0 0.0.0.255 192.168.1.0 0.0.0.255
 permit tcp host 10.10.2.1 host 192.168.2.1 eq www
```

## 2-2. ACL要件資料
以下のACLを依頼されたものとします。

・acl_requests_batfish.csv
![無題1207_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/73bc2a8a-808a-6ee3-92ee-4e98fb74a595.png)

- 1行目 ･･･ 未設定
- 2行目 ･･･ 設定済み（1行目の既存設定に含まれる）  
- 3行目 ･･･ 設定済み（2行目の既存設定と完全一致。ただし、要件内ではポート番号`80`を指定しているのに対し、既存Configではニーモニック`www`に変換されている。） 
- 4行目 ･･･ 一部未設定（2行目の既存設定と一部重複）

最終目標は、不要な2,3行目をあらかじめ除外し、1,4行目のみ設定変更することです。

# 3. ACL Config解析

## 3-1. Ansible Playbook

Batfish用のAnsibleロール[Batfish/Ansible](https://github.com/batfish/ansible)を使用しました。
大まかな流れは以下の通りです。

1. `bf_session`モジュールでBatfishへ接続
2. `bf_init_snapshot`モジュールで、Batfishに既存Configファイル等のSnapshotデータを読み込ませる
3. CSV形式のACL要件資料をリスト形式に変換
4. タスク3の結果を表示
5. タスク3の結果を変数`acls_all`に格納
6. `acls_all`の各要素（1～4行目）に対し、`bf_assert`モジュールを使って既存で許可される要件があるかチェック。一部でも許可されなければ`failed`、すべて許可される場合は`ok`となる。`failed`となった場合も後続タスクを実行できるよう、`ignore_errors`を`yes`に指定。この結果を`result`に格納。
7. 今回設定したい`failed`の要件について、`result`の中からACL要件`item`を抜き出す。
8. タスク7の結果を表示
9. タスク7の結果をファイル保存

```yaml:playbook_batfish6.yml
---

- hosts: localhost
  gather_facts: no
  connection: local
  vars:
    acls: []
    acl_list: acl_requests_batfish.csv
    acl_list_not_permitted: acl_requests_not_permitted.txt
    batfish_session: c333e6cc5b62
    batfish_network: example_network
    batfish_snapshot: example_snapshot
    batfish_snapshot_data: ./mynetworks/test1

  roles:
    - batfish.base

  tasks:
  - name: Setup connection to Batfish service   # (1)
    bf_session:
      host: "{{ ansible_host }}"
      name: "{{ batfish_session }}"
    run_once: yes

  - name: Initialize the example network   # (2)
    bf_init_snapshot:
      network: "{{ batfish_network }}"
      snapshot: "{{ batfish_snapshot }}"
      snapshot_data: "{{ batfish_snapshot_data }}"
      overwrite: true
    run_once: yes

  - name: Read acl requests from CSV file and return a list   # (3)
    read_csv:
      path: "{{ acl_list }}"
    run_once: yes
    register: acl_requests

  - name: Display acl requests   # (4)
    debug:
      msg: "{{ acl_requests.list }}"
    run_once: yes

  - name: Set acl requests   # (5)
    set_fact:
      acls_all: "{{ acl_requests.list }}"
    run_once: yes

  - name: Confirm a filter permits some specific traffic   # (6)
    bf_assert:
      assertions:
        - type: assert_filter_permits
          name: confirm the acl requests are permitted by existing acls
          parameters:
            filters: '{{ item.hostname }}["{{ item.acl_name }}"]'
            headers:
              ipProtocols: "{{ item.protocol | regex_replace('^ip$', omit) }}"
              srcIps: "{{ item.src_addr }}"
              srcPorts: "{{ item.src_port | regex_replace('^$', omit) }}"
              dstIps: "{{ item.dest_addr }}"
              dstPorts: "{{ item.dest_port | regex_replace('^$', omit) }}"
              applications: "{{ item.application | regex_replace('^$', omit) }}"
    ignore_errors: yes
    register: result
    loop: "{{ acls_all }}"

  - name: Set "not permitted" acl requests   # (7)
    set_fact:
      acls: "{{ acls + [item.item] }}"
    when: item.failed == true
    loop: "{{ result.results }}"

  - name: Display "not permitted" acl requests   # (8)
    debug:
      msg: "{{ acls }}"

  - name: Copy file with "not permitted" acl requests   # (9)
    copy:
      content: "{{ acls }}"
      dest: "{{ acl_list_not_permitted }}"
```

## 3-2. Playbook実行ログ
AnsibleとBatfishは同じCentOS上で動かしているため、localhost上で実行します。
タスク4で今回のACL要件×4行が表示され、タスク6で想定通り1,4行目が`failed`となっています。3行目はおそらくBatfish内でポート番号`80`がニーモニック`www`と紐づけられ、`ok`になったと推測されます。
続くタスク8で、`failed`のACL要件×2行のみが表示されました。

```
$ ansible-playbook playbook_batfish6.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'


PLAY [localhost] ************************************************************************************************************************

TASK [Setup connection to Batfish service] **********************************************************************************************
changed: [localhost]

TASK [Initialize the example network] ***************************************************************************************************
 [WARNING]: Your snapshot was successfully initialized but Batfish failed to fully recognized some lines in one or more input files.
Some unrecognized configuration lines are not uncommon for new networks, and it is often fine to proceed with further analysis.  You can
help the Batfish developers improve support for your network by running the bf_upload_diagnostics module:
https://github.com/batfish/ansible/blob/master/docs/bf_upload_diagnostics.rst

changed: [localhost]

TASK [Read acl requests from CSV file and return a list] ********************************************************************************
ok: [localhost]

TASK [Display acl requests] *************************************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.0.0/24",
            "dest_oper": "",
            "dest_port": "",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "ip",
            "seq_no": "",
            "src_addr": "10.10.0.0/24",
            "src_oper": "",
            "src_port": ""
        },
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.1.1/32",
            "dest_oper": "",
            "dest_port": "",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "ip",
            "seq_no": "",
            "src_addr": "10.10.1.1/32",
            "src_oper": "",
            "src_port": ""
        },
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.2.1/32",
            "dest_oper": "eq",
            "dest_port": "80",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "tcp",
            "seq_no": "",
            "src_addr": "10.10.2.1/32",
            "src_oper": "",
            "src_port": ""
        },
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.2.0/24",
            "dest_oper": "",
            "dest_port": "",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "ip",
            "seq_no": "",
            "src_addr": "10.10.2.0/24",
            "src_oper": "",
            "src_port": ""
        }
    ]
}

TASK [Set acl requests] *****************************************************************************************************************
ok: [localhost]

TASK [Confirm a filter permits some specific traffic] ***********************************************************************************
failed: [localhost] (item={'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.0.0/24', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.0.0/24', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''}) => {"ansible_loop_var": "item", "changed": false, "item": {"acl_name": "ACL_TEST", "acl_type": "name_extended", "action": "permit", "application": "", "change": "add", "description": "", "dest_addr": "192.168.0.0/24", "dest_oper": "", "dest_port": "", "hostname": "hqdist1", "modifier": "", "protocol": "ip", "seq_no": "", "src_addr": "10.10.0.0/24", "src_oper": "", "src_port": ""}, "msg": "1 of 1 assertions failed", "result": [{"details": "Found a flow that was denied, when expected to be permitted\n[{'Node': 'hqdist1', 'Filter_Name': 'ACL_TEST', 'Flow': Flow(dscp=0, dstIp='192.168.0.0', dstPort=None, ecn=0, fragmentOffset=0, icmpCode=None, icmpVar=None, ingressInterface=None, ingressNode='hqdist1', ingressVrf='default', ipProtocol='HOPOPT', packetLength=0, srcIp='10.10.0.0', srcPort=None, state='NEW', tag='BASE', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), 'Action': 'DENY', 'Line_Content': 'no-match', 'Trace': AclTrace(events=[AclTraceEvent(class_name='org.batfish.datamodel.acl.DefaultDeniedByIpAccessList', description=\"Flow did not match 'extended ipv4 access-list' named 'ACL_TEST'\", lineDescription=None)])}]", "name": "confirm the acl requests are permitted by existing acls", "status": "Fail", "type": "assert_filter_permits"}], "summary": "1 of 1 assertions failed"}
ok: [localhost] => (item={'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.1.1/32', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.1.1/32', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''})
ok: [localhost] => (item={'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'tcp', 'src_addr': '10.10.2.1/32', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.2.1/32', 'dest_oper': 'eq', 'dest_port': '80', 'application': '', 'modifier': '', 'description': ''})
failed: [localhost] (item={'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.2.0/24', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.2.0/24', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''}) => {"ansible_loop_var": "item", "changed": false, "item": {"acl_name": "ACL_TEST", "acl_type": "name_extended", "action": "permit", "application": "", "change": "add", "description": "", "dest_addr": "192.168.2.0/24", "dest_oper": "", "dest_port": "", "hostname": "hqdist1", "modifier": "", "protocol": "ip", "seq_no": "", "src_addr": "10.10.2.0/24", "src_oper": "", "src_port": ""}, "msg": "1 of 1 assertions failed", "result": [{"details": "Found a flow that was denied, when expected to be permitted\n[{'Node': 'hqdist1', 'Filter_Name': 'ACL_TEST', 'Flow': Flow(dscp=0, dstIp='192.168.2.0', dstPort=None, ecn=0, fragmentOffset=0, icmpCode=None, icmpVar=None, ingressInterface=None, ingressNode='hqdist1', ingressVrf='default', ipProtocol='HOPOPT', packetLength=0, srcIp='10.10.2.0', srcPort=None, state='NEW', tag='BASE', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), 'Action': 'DENY', 'Line_Content': 'no-match', 'Trace': AclTrace(events=[AclTraceEvent(class_name='org.batfish.datamodel.acl.DefaultDeniedByIpAccessList', description=\"Flow did not match 'extended ipv4 access-list' named 'ACL_TEST'\", lineDescription=None)])}]", "name": "confirm the acl requests are permitted by existing acls", "status": "Fail", "type": "assert_filter_permits"}], "summary": "1 of 1 assertions failed"}
...ignoring

TASK [Set "not permitted" acl requests] *************************************************************************************************
ok: [localhost] => (item={'msg': '1 of 1 assertions failed', 'changed': False, 'result': [{'name': 'confirm the acl requests are permitted by existing acls', 'type': 'assert_filter_permits', 'status': 'Fail', 'details': 'Found a flow that was denied, when expected to be permitted\n[{\'Node\': \'hqdist1\', \'Filter_Name\': \'ACL_TEST\', \'Flow\': Flow(dscp=0, dstIp=\'192.168.0.0\', dstPort=None, ecn=0, fragmentOffset=0, icmpCode=None, icmpVar=None, ingressInterface=None, ingressNode=\'hqdist1\', ingressVrf=\'default\', ipProtocol=\'HOPOPT\', packetLength=0, srcIp=\'10.10.0.0\', srcPort=None, state=\'NEW\', tag=\'BASE\', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), \'Action\': \'DENY\', \'Line_Content\': \'no-match\', \'Trace\': AclTrace(events=[AclTraceEvent(class_name=\'org.batfish.datamodel.acl.DefaultDeniedByIpAccessList\', description="Flow did not match \'extended ipv4 access-list\' named \'ACL_TEST\'", lineDescription=None)])}]'}], 'summary': '1 of 1 assertions failed', 'failed': True, 'invocation': {'module_args': {'assertions': [{'type': 'assert_filter_permits', 'name': 'confirm the acl requests are permitted by existing acls', 'parameters': {'filters': 'hqdist1["ACL_TEST"]', 'headers': {'srcIps': '10.10.0.0/24', 'dstIps': '192.168.0.0/24'}}}], 'session': {'host': '127.0.0.1'}, 'snapshot': 'example_snapshot', 'network': 'example_network'}}, 'item': {'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.0.0/24', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.0.0/24', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''}, 'ansible_loop_var': 'item'})
skipping: [localhost] => (item={'changed': False, 'result': [{'name': 'confirm the acl requests are permitted by existing acls', 'type': 'assert_filter_permits', 'status': 'Pass', 'details': 'Assertion passed'}], 'summary': 'Assertion(s) completed successfully', 'invocation': {'module_args': {'assertions': [{'type': 'assert_filter_permits', 'name': 'confirm the acl requests are permitted by existing acls', 'parameters': {'filters': 'hqdist1["ACL_TEST"]', 'headers': {'srcIps': '10.10.1.1/32', 'dstIps': '192.168.1.1/32'}}}], 'session': {'host': '127.0.0.1'}, 'snapshot': 'example_snapshot', 'network': 'example_network'}}, 'failed': False, 'item': {'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.1.1/32', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.1.1/32', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''}, 'ansible_loop_var': 'item'}) 
skipping: [localhost] => (item={'changed': False, 'result': [{'name': 'confirm the acl requests are permitted by existing acls', 'type': 'assert_filter_permits', 'status': 'Pass', 'details': 'Assertion passed'}], 'summary': 'Assertion(s) completed successfully', 'invocation': {'module_args': {'assertions': [{'type': 'assert_filter_permits', 'name': 'confirm the acl requests are permitted by existing acls', 'parameters': {'filters': 'hqdist1["ACL_TEST"]', 'headers': {'ipProtocols': 'tcp', 'srcIps': '10.10.2.1/32', 'dstIps': '192.168.2.1/32', 'dstPorts': '80'}}}], 'session': {'host': '127.0.0.1'}, 'snapshot': 'example_snapshot', 'network': 'example_network'}}, 'failed': False, 'item': {'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'tcp', 'src_addr': '10.10.2.1/32', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.2.1/32', 'dest_oper': 'eq', 'dest_port': '80', 'application': '', 'modifier': '', 'description': ''}, 'ansible_loop_var': 'item'}) 
ok: [localhost] => (item={'msg': '1 of 1 assertions failed', 'changed': False, 'result': [{'name': 'confirm the acl requests are permitted by existing acls', 'type': 'assert_filter_permits', 'status': 'Fail', 'details': 'Found a flow that was denied, when expected to be permitted\n[{\'Node\': \'hqdist1\', \'Filter_Name\': \'ACL_TEST\', \'Flow\': Flow(dscp=0, dstIp=\'192.168.2.0\', dstPort=None, ecn=0, fragmentOffset=0, icmpCode=None, icmpVar=None, ingressInterface=None, ingressNode=\'hqdist1\', ingressVrf=\'default\', ipProtocol=\'HOPOPT\', packetLength=0, srcIp=\'10.10.2.0\', srcPort=None, state=\'NEW\', tag=\'BASE\', tcpFlagsAck=0, tcpFlagsCwr=0, tcpFlagsEce=0, tcpFlagsFin=0, tcpFlagsPsh=0, tcpFlagsRst=0, tcpFlagsSyn=0, tcpFlagsUrg=0), \'Action\': \'DENY\', \'Line_Content\': \'no-match\', \'Trace\': AclTrace(events=[AclTraceEvent(class_name=\'org.batfish.datamodel.acl.DefaultDeniedByIpAccessList\', description="Flow did not match \'extended ipv4 access-list\' named \'ACL_TEST\'", lineDescription=None)])}]'}], 'summary': '1 of 1 assertions failed', 'failed': True, 'invocation': {'module_args': {'assertions': [{'type': 'assert_filter_permits', 'name': 'confirm the acl requests are permitted by existing acls', 'parameters': {'filters': 'hqdist1["ACL_TEST"]', 'headers': {'srcIps': '10.10.2.0/24', 'dstIps': '192.168.2.0/24'}}}], 'session': {'host': '127.0.0.1'}, 'snapshot': 'example_snapshot', 'network': 'example_network'}}, 'item': {'hostname': 'hqdist1', 'change': 'add', 'acl_type': 'name_extended', 'acl_name': 'ACL_TEST', 'seq_no': '', 'action': 'permit', 'protocol': 'ip', 'src_addr': '10.10.2.0/24', 'src_oper': '', 'src_port': '', 'dest_addr': '192.168.2.0/24', 'dest_oper': '', 'dest_port': '', 'application': '', 'modifier': '', 'description': ''}, 'ansible_loop_var': 'item'})

TASK [Display "not permitted" acl requests] *********************************************************************************************
ok: [localhost] => {
    "msg": [
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.0.0/24",
            "dest_oper": "",
            "dest_port": "",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "ip",
            "seq_no": "",
            "src_addr": "10.10.0.0/24",
            "src_oper": "",
            "src_port": ""
        },
        {
            "acl_name": "ACL_TEST",
            "acl_type": "name_extended",
            "action": "permit",
            "application": "",
            "change": "add",
            "description": "",
            "dest_addr": "192.168.2.0/24",
            "dest_oper": "",
            "dest_port": "",
            "hostname": "hqdist1",
            "modifier": "",
            "protocol": "ip",
            "seq_no": "",
            "src_addr": "10.10.2.0/24",
            "src_oper": "",
            "src_port": ""
        }
    ]
}

TASK [Copy file with "not permitted" acl requests] **************************************************************************************
ok: [localhost]

PLAY RECAP ******************************************************************************************************************************
localhost                  : ok=9    changed=2    unreachable=0    failed=0    skipped=0    rescued=0    ignored=1   
```

最後のタスク9で、結果がファイルに保存されました。

```acl_requests_not_permitted.txt
[{"hostname": "hqdist1", "change": "add", "acl_type": "name_extended", "acl_name": "ACL_TEST", "seq_no": "", "action": "permit", "protocol": "ip", "src_addr": "10.10.0.0/24", "src_oper": "", "src_port": "", "dest_addr": "192.168.0.0/24", "dest_oper": "", "dest_port": "", "application": "", "modifier": "", "description": ""}, {"hostname": "hqdist1", "change": "add", "acl_type": "name_extended", "acl_name": "ACL_TEST", "seq_no": "", "action": "permit", "protocol": "ip", "src_addr": "10.10.2.0/24", "src_oper": "", "src_port": "", "dest_addr": "192.168.2.0/24", "dest_oper": "", "dest_port": "", "application": "", "modifier": "", "description": ""}]
```

# 4. ACL Config作成

## 4-1. Ansible Playbook
続いて、前項で保存したファイルとJinja2テンプレートを使い、`template`モジュールで追加Configファイルの作成を行いました。

```yaml:playbook_aclgen_batfish.yml
---

- hosts: all
  gather_facts: no
  connection: network_cli

  vars:
    template: acl_template.j2
    acl_list: acl_requests_not_permitted.txt
    config_filename: "add/acl_config_{{ inventory_hostname }}.txt"

  tasks:
    - name: set acl requests
      set_fact:
        acls: "{{ lookup('file', acl_list) }}"
      run_once: yes

    - name: build template
      template:
        src: "{{ template }}"
        dest: "{{ config_filename }}"
        lstrip_blocks: yes
```

## 4-2. Playbook実行ログ
Config作成にあたりホスト名やOSタイプ情報が必要なため、localhostではなく、Inventoryファイルに`hqdist1`のログイン情報を定義しています。

```
$ ansible-playbook -i inventory.ini playbook_aclgen_batfish.yml

PLAY [all] ******************************************************************************************************************************

TASK [set acl requests] *****************************************************************************************************************
ok: [hqdist1]

TASK [build template] *******************************************************************************************************************
changed: [hqdist1]

PLAY RECAP ******************************************************************************************************************************
hqdist1                    : ok=2    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
```

作成されたConfigは以下の通りです。必要な2行のみが問題なく作成されています。

```acl_config_hqdist1.txt
ip access-list extended ACL_TEST
 permit ip 10.10.0.0 0.0.0.255 192.168.0.0 0.0.0.255
ip access-list extended ACL_TEST
 permit ip 10.10.2.0 0.0.0.255 192.168.2.0 0.0.0.255
```

これ以降は、前回の記事でご紹介した「ACL設定変更」と同じ方法でNW機器へ適用可能です。

# 最後に
今回、既存Configファイルは手動でアップロードしましたが、以下のAnsible helpers for Batfishを使えば、ここも自動化できそうです。
[GitHub - rickdonato/batfish-ansible-helpers](https://github.com/rickdonato/batfish-ansible-helpers)
BatfishはCiscoに限らずJuniper、Arista等の[様々なプラットフォーム](https://github.com/batfish/batfish#supported-network-device-and-operating-system-list)に対応していますので、Batfish + Ansibleの組み合わせは、特に大規模なACL設定変更で重宝してくれるのではないでしょうか。