Cisco IOS-XEのACLをRESTCONFで操作する(Python編)

# はじめに

モデル駆動型プログラマビリティで使用されるトランスポートプロトコル（NETCONF、RESTCONF、gRPC）の内、RESTCONFを使ってCisco IOS-XEのACL設定を行った時のメモです。
操作ツールとしては、Postman、Python、Ansible等がありますが、今回はPythonを使い、名前付き拡張ACLに対してCRUD（作成、取得、更新、削除）を行いました。

# 1. RESTCONF概要
RESTCONFは、RESTFulインターフェースを提供するHTTP(S)ベースのプロトコルです。データ形式として、XMLに加えJSON形式もサポートしています（NETCONFはSSHベースでXML形式のみ）。

RESTCONF/NETCONFのCRUD操作

| RESTCONF       |  NETCONF    |
|:-----------------|:------------------|
| GET | `<get>`, `<get-config>` |
| POST | `<edit-config>` (operation="create") |
| PUT | `<edit-config>` (operation="create/replace") |
| PATCH | `<edit-config>` (operation="merge") |
| DELETE | `<edit-config>` (operation="delete") |

IOSはもともとREST APIもサポートしていますが、REST APIがベンダー固有の実装なのに対し、RESTCONFはRFC8040で標準化されたプロトコルであり、別物になります。

RESTCONFを含むモデル駆動型プログラマビリティの概要は、DevNet Learning Labsの以下セッションで分かり易くまとめられています。
[モデル駆動型プログラマビリティの紹介](https://developer.cisco.com/learning/modules/dnav3-intro-mdp-jp)

# 2. 用意した環境
[こちらの記事](https://qiita.com/tech_kitara/items/4c201a83f1f9286d002b#2-%E7%94%A8%E6%84%8F%E3%81%97%E3%81%9F%E7%92%B0%E5%A2%83)と同様、Cisco dCloud環境を利用させて頂きました。
対象機器は「CSR1000v with IOS XE 16.08」、クライアントのPythonバージョンは「3.6.8」を使用しました。

# 3. ACL作成
まず、ACL設定が何も無い状態から、ACL名`TEST`を1行追加します。

## 3-1. Pythonコード
URL`https://[IPアドレス]:[ポート番号]/restconf/data/Cisco-IOS-XE-native:native/ip/access-list`に対し、`PUT`メソッドでリクエストを行っています。Body内のデータはJSON形式で定義しています。
ちなみに`POST`メソッドでは作成できませんでした。

```python:create_acl.py
#!/usr/bin/python
import requests
import json

# disable warnings from SSL/TLS certificates
requests.packages.urllib3.disable_warnings()

# credentials for CSR1000v
HOST = '[IPアドレス]'
PORT = 443  # 環境によって異なる
USER = '[ユーザ名]'
PASS = '[パスワード]'


def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # RESTCONF doby for new ACL
    body_data = {
                    "Cisco-IOS-XE-native:access-list": {
                        "Cisco-IOS-XE-acl:extended": [
                            {
                                "name": "TEST",
                                "access-list-seq-rule": [
                                    {
                                        "sequence": "30",
                                        "ace-rule": {
                                            "action": "permit",
                                            "protocol": "ip",
                                            "ipv4-address": "192.168.4.0",
                                            "mask": "0.0.0.255",
                                            "dest-ipv4-address": "192.168.100.0",
                                            "dest-mask": "0.0.0.255"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }

    # this statement performs a PUT on the specified url
    response = requests.put(url, auth=(USER, PASS),
                            headers=headers, data=json.dumps(body_data), verify=False)

    # print the json that is returned
    print(response)

if __name__ == '__main__':
    main()
```

## 3-2. 実行結果
Status Code 204 (No Content)が返ってきました。

```
$ python create_acl.py
<Response [204]>
```

Configを見ると、想定通りのACLが作成されていました。

```
csr1#sh run | begin TEST
ip access-list extended TEST
 permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
```

# 4. ACL取得
次に、作成したACLをRESTCONFで情報取得してみます。

## 4-1. Pythonコード(1) IP設定取得
`main()`関数以外は3.と同様のため、これ以降は抜粋して記載します。
まず、URL`https://[IPアドレス]:[ポート番号]/restconf/data/Cisco-IOS-XE-native:native/ip`に対し、IP関連設定の情報を`GET`メソッドで取得してみました。

```python:get_ip_info.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # this statement performs a GET on the specified url
    response = requests.get(url, auth=(USER, PASS),
                            headers=headers, verify=False)

    # print the json that is returned
    print(response.text)
```

## 4-2. 実行結果(1) IP設定取得
途中の`"access-list": { "Cisco-IOS-XE-acl:extended": [ ～ ] }`で、設定した内容を確認できました。
ちなみに、ACL設定前はこの`"access-list"`キーを含めて作成されていませんでした。そのため、3.の作成時、下の階層の`"Cisco-IOS-XE-acl:extended": [ ～ ] `をいきなりPUTしてもエラーになりました。

```
$ python get_ip_info.py
{
  "Cisco-IOS-XE-native:ip": {
    "domain": {
      "name": "demo.dcloud.cisco.com"
    },
    "forward-protocol": {
      "protocol": "nd"
    },
    "route": {
      "ip-route-interface-forwarding-list": [
        {
          "prefix": "0.0.0.0",
          "mask": "0.0.0.0",
          "fwd-list": [
            {
              "fwd": "GigabitEthernet1",
              "interface-next-hop": [
                {
                  "ip-address": "198.18.128.1"
                }
              ]
            }
          ]
        }
      ]
    },
    "ssh": {
      "rsa": {
        "keypair-name": "ssh-key"
      },
      "version": 2
    },
    "access-list": {
      "Cisco-IOS-XE-acl:extended": [
        {
          "name": "TEST",
          "access-list-seq-rule": [
            {
              "sequence": "30",
              "ace-rule": {
                "action": "permit",
                "protocol": "ip",
                "ipv4-address": "192.168.4.0",
                "mask": "0.0.0.255",
                "dest-ipv4-address": "192.168.100.0",
                "dest-mask": "0.0.0.255"
              }
            }
          ]
        }
      ]
    },
    "Cisco-IOS-XE-http:http": {
      "authentication": {
        "local": [null]
      },
      "server": true,
      "secure-server": true
    }
  }
}
```

## 4-3. Pythonコード(2) 拡張ACL設定取得
こちらは、より深い階層のURL`https://[IPアドレス]:[ポート番号]/restconf/data/Cisco-IOS-XE-native:native/ip/access-list/extended`で、拡張ACLのみをピンポイントで取得する例です。

```python:get_acl.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list/extended".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # this statement performs a GET on the specified url
    response = requests.get(url, auth=(USER, PASS),
                            headers=headers, verify=False)

    # print the json that is returned
    print(response.text)
```

## 4-4. 実行結果(2) 拡張ACL設定取得

```
$ python get_acl.py
{
  "Cisco-IOS-XE-acl:extended": [
    {
      "name": "TEST",
      "access-list-seq-rule": [
        {
          "sequence": "30",
          "ace-rule": {
            "action": "permit",
            "protocol": "ip",
            "ipv4-address": "192.168.4.0",
            "mask": "0.0.0.255",
            "dest-ipv4-address": "192.168.100.0",
            "dest-mask": "0.0.0.255"
          }
        }
      ]
    }
  ]
}
```

# 5. ACLマージ
既存ACLに対し、エントリ（ACE）追加と、別のACLを追加する例をご紹介します。

## 5-1. Pythonコード(1) エントリ追加
既存のACL名`TEST`の2行目に、エントリを追加してみます。先ほどのシーケンス番号`30`に対し、今回は`50`を指定しています。ちなみにシーケンス番号を指定しない場合、エラーで設定できませんでした。
マージ用のメソッドである`PATCH`を使い、既存設定はそのままで、Body内の内容を追加している形です。

```python:merge_ace.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # RESTCONF doby for new ACL
    body_data = {
                    "Cisco-IOS-XE-native:access-list": {
                        "Cisco-IOS-XE-acl:extended": [
                            {
                                "name": "TEST",
                                "access-list-seq-rule": [
                                    {
                                        "sequence": "50",
                                        "ace-rule": {
                                            "action": "permit",
                                            "protocol": "ip",
                                            "ipv4-address": "192.168.5.0",
                                            "mask": "0.0.0.255",
                                            "dest-ipv4-address": "192.168.100.0",
                                            "dest-mask": "0.0.0.255"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }

    # this statement performs a PUT on the specified url
    response = requests.patch(url, auth=(USER, PASS),
                            headers=headers, data=json.dumps(body_data), verify=False)

    # print the json that is returned
    print(response)
```

## 5-2. 実行結果(1) エントリ追加
Status Code 204 (No Content)が返ってきました。

```
$ python merge_ace.py
<Response [204]>
```

Configを見ると、想定通り2行目に追加されていました。

```
csr1#sh run | begin TEST
ip access-list extended TEST
 permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
 permit ip 192.168.5.0 0.0.0.255 192.168.100.0 0.0.0.255
```

## 5-3. Pythonコード(2) 別のACL追加
今度は、別のACL名`TEST2`を1行追加してみます。

```python:merge_another_acl.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # RESTCONF doby for new ACL
    body_data = {
                    "Cisco-IOS-XE-native:access-list": {
                        "Cisco-IOS-XE-acl:extended": [
                            {
                                "name": "TEST2",
                                "access-list-seq-rule": [
                                    {
                                        "sequence": "70",
                                        "ace-rule": {
                                            "action": "permit",
                                            "protocol": "ip",
                                            "ipv4-address": "192.168.7.0",
                                            "mask": "0.0.0.255",
                                            "dest-ipv4-address": "192.168.100.0",
                                            "dest-mask": "0.0.0.255"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }

    # this statement performs a PUT on the specified url
    response = requests.patch(url, auth=(USER, PASS),
                            headers=headers, data=json.dumps(body_data), verify=False)

    # print the json that is returned
    print(response)
```

## 5-4. 実行結果(2) 別のACL追加

```
$ python merge_another_acl.py
<Response [204]>
```

ACL名`TEST2`が追加されている事が分かります。

```
csr1#sh run | begin TEST
ip access-list extended TEST
 permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
 permit ip 192.168.5.0 0.0.0.255 192.168.100.0 0.0.0.255
ip access-list extended TEST2
 permit ip 192.168.7.0 0.0.0.255 192.168.100.0 0.0.0.255
```

# 6. ACLリプレイス
今度は、既存ACLを上書きし、別のACL名`TEST3`を作成する例です。通常のACL作業では基本的に実施しない（意図せずやってしまうと大事故になる）ケースだと思います。ただ、ACL設定以外では、パラメーターシートを元に「～～の設定は〇〇であるべき」のように宣言型で定義できるので、既存設定削除などの煩わしさ無しで設定できるメリットはあると思います。

## 6-1. Pythonコード
メソッドは、新規作成時と同様`PUT`を用います。

```python:replace_acl.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # RESTCONF doby for new ACL
    body_data = {
                    "Cisco-IOS-XE-native:access-list": {
                        "Cisco-IOS-XE-acl:extended": [
                            {
                                "name": "TEST3",
                                "access-list-seq-rule": [
                                    {
                                        "sequence": "90",
                                        "ace-rule": {
                                            "action": "permit",
                                            "protocol": "ip",
                                            "ipv4-address": "192.168.9.0",
                                            "mask": "0.0.0.255",
                                            "dest-ipv4-address": "192.168.100.0",
                                            "dest-mask": "0.0.0.255"
                                            }
                                        }
                                    ]
                                }
                            ]
                        }
                    }

    # this statement performs a PUT on the specified url
    response = requests.put(url, auth=(USER, PASS),
                            headers=headers, data=json.dumps(body_data), verify=False)

    # print the json that is returned
    print(response)
```

## 6-2. 出力結果

```
$ python replace_acl.py
<Response [204]>
```

見事に上書されました。

```
csr1#sh run | begin TEST
ip access-list extended TEST3
 permit ip 192.168.9.0 0.0.0.255 192.168.100.0 0.0.0.255
```

ちなみに、以下記事を書いた方によると、NX-OSの場合、`PUT`メソッドがRFC8040の仕様通りではなく、`PATCH`メソッドと同様マージになるケースもあるようです。
[Exploring IOS-XE and NX-OS based RESTCONF Implementations with YANG and Openconfig](https://www.networktocode.com/blog/post/Exploring-IOS-XE-and-NX-OS-based-RESTCONF-Implementations-with-YANG-and-Openconfig/)

# 7. ACL削除
最後に、作成したACLの削除を行ってみます。
事前に「3.ACL作成」と「5.ACLマージ」を実行し、`TEST3`が上書きされ、`TEST`と`TEST2`が設定された状態にしておきます。

```
csr1#sh run | begin TEST
ip access-list extended TEST
 permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
 permit ip 192.168.5.0 0.0.0.255 192.168.100.0 0.0.0.255
ip access-list extended TEST2
 permit ip 192.168.7.0 0.0.0.255 192.168.100.0 0.0.0.255
```

## 7-1. Pythonコード
URL`https://[IPアドレス]:[ポート番号]/restconf/data/Cisco-IOS-XE-native:native/ip/access-list/extended=TEST2`に対し、`DELETE`メソッドでリクエストを行います。
URLの末尾に`/extended=TEST2`を指定することで、`TEST2`をACL単位で削除できます。
(エントリ単位での削除方法はちょっと分かりませんでした。。）

```python:delete_acl.py
def main():
    # url string to issue request
    url = "https://{h}:{p}/restconf/data/Cisco-IOS-XE-native:native/ip/access-list/extended=TEST2".format(h=HOST, p=PORT)

    # RESTCONF media types for REST API headers
    headers = {'Content-Type': 'application/yang-data+json',
               'Accept': 'application/yang-data+json'}

    # this statement performs a DELETE on the specified url
    response = requests.delete(url, auth=(USER, PASS),
                            headers=headers, verify=False)

    # print the json that is returned
    print(response)
```

## 7-2. 実行結果

```
$ python delete_acl.py
<Response [204]>
```

`TEST2`が削除されました。

```
csr1#sh run | begin TEST
ip access-list extended TEST
 permit ip 192.168.4.0 0.0.0.255 192.168.100.0 0.0.0.255
 permit ip 192.168.5.0 0.0.0.255 192.168.100.0 0.0.0.255
```

# 最後に
まだインターネット上にサンプルコードが少なかったので、お試しでやってみました。以前の記事で、要件資料と複雑なJinja2テンプレートを組み合わせてACL Configの生成を行いましたが、RESTCONFであれば、要件資料のようなパラメーター情報をJSON形式に変換すれば設定ができるため、自動化と相性が良いと思います。
今回はPythonの例でしたが、Ansibleの`restconf_get`と`restconf_config`モジュールを使った例や、設定項目毎のモデル（URL）の確認・作成方法もまとめてみたいと思います。