Cisco NFVISをAnsibleから操作する

# はじめに

従来のエンタープライズ向けネットワークでは、ルータやファイアウォール等の機能毎に専用のハードウェアを用意して導入していました。一方ここ数年、身近でもNFV（Network Functions Virtualization）を利用してネットワーク機能を仮想化し、汎用デバイス上に構築するケースが少しずつ増えてきました。

Ciscoが提供するエンタープライズNFVを例にとると、NFVは以下のコンポートから構成されます。
（加えて、これらを管理するコントローラもあり。）

- VNF（Virtual Network Functions） ･･･ ルータ、ファイアウォール、WLC、WAAS等の仮想プラットフォーム
- ハイパーバイザー ･･･ NFVIS、ESXi、Hyper-V、KVM等（VNFによってサポート対象が異なる）
- ハードウェア ･･･ Cisco UCS-C、UCS-E、ENCS等
![無題1208_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/af77a0bf-5d16-b385-8511-348be4ebc7d1.png)
[Cisco Live! US 2019 - Enterprise Network Functions Virtualization (ENFV) Architecture, Configuration and Troubleshooting - BRKARC-2012](https://www.ciscolive.com/c/dam/r/ciscolive/us/docs/2019/pdf/BRKARC-2012.pdf) p8から引用

今回の記事でご紹介するCisco NFVISは、ハイパーバイザーとしての機能に加え、REST-APIを通じた仮想サービスの制御やゼロタッチ展開（PnP）機能も提供されています。

詳細は、Cisco DevNet Lerning Labsの以下セッションで分かりやすく説明されています。
[NFVIS API を使用したネットワーク機能の仮想化の紹介](https://developer.cisco.com/learning/modules/dnav3-intro-nfvis-jp)
このセッションでは、後述するCisco dCloud環境上で、VNFのデプロイ・情報取得・削除をGUIやREST-API（Postman、Python）経由で行うハンズオンが体験できます。

本記事は、同様の内容をAnsibleの`uri`モジュールで行った時のメモになります。

～その他参考URL～

- [GitHub - CiscoDevNet/dnav3-code](https://github.com/CiscoDevNet/dnav3-code/tree/master/intro-nfvis)
- [NFVIS APIドキュメント]
(https://www.cisco.com/c/en/us/td/docs/routers/nfvis/user_guide/b-api-reference-for-cisco-enterprise-nfvis.html)



# 2. 用意した環境

以下のCisco dCloud環境を利用させて頂きました。
[Cisco DevNet Express Cisco DNA Sandbox v3.0a](https://dcloud2-sng.cisco.com/content/demo/229349?returnPathTitleKey=content-view&isLoggingIn=true)
![無題1208_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/528e2021-755f-3cd4-c053-d2a80e13c835.png)

dCloud接続方法の詳細は割愛しますが、以下の流れでセットアップしています。

1. Cisco AnyConnectを使って、自分のPCからVPN経由で図中のcentos（DevBox）へリモートデスクトップ接続。
2. DevBoxにインストールされていたPython3.6.5を使ってvenv仮想環境を構築し、その上でAnsible2.9.2をインストール。
3. DevBoxからNFVISへWebアクセス or Ansible経由でREST-APIコール。

これ以降で、Ansible Inventory/Playbookの内容と実行結果をご紹介します。

# 3. Inventoryファイル
NFVISへのHTTPSアクセスに必要な情報を記載します。

```:inventory_nfvis.ini
[cisco]
nfvis ansible_host=[NFVISのIPアドレス] ansible_port=443

[cisco:vars]
username=[NFVISのユーザ名]
password=[NFVISのパスワード]
```

# 4. VNFのデプロイ
まずは、仮想ルータISRvのデプロイを行ってみます。

## 4-1. Ansible Playbook
内容は以下の通りです。

- POSTメソッドで、Body内にISRvのデプロイ情報を定義。パラメーターはLearning Labsをそのまま流用。今回はYAML形式の変数`body_data`を、フィルタープラグイン`"{{ body_data | to_json }}"`でJSON形式で変換し、Bodyに格納。（JSONファイルをlookupプラグイン`"{{ lookup('file', 'body_data.json') }}"`で読み込んでもOK。）
- Basic認証を使用し、APIコール毎にユーザ名/パスワードを指定。
- uriモジュールは、デフォルトでStatus Code`200 (OK)`をSuccessとするが、今回は`201 (Created)`が返ってくるため、明示的に201を指定。

```yaml:playbook_nfvis_post.yml
---

- hosts: cisco
  gather_facts: no
  connection: local
  vars:
    body_data:
      deployment:
        name: "ISRv"
        vm_group:
          name: "ROUTER"
          image: "isrv-universalk9.16.06.02.tar.gz"
          flavor: "ISRv-small"
          bootup_time: 600
          recovery_wait_time: 0
          interfaces:
            interface:
              - nicid: 0
                network: "int-mgmt-net"
              - nicid: 1
                network: "wan-net"
              - nicid: 2
                network: "lan-net"
          kpi_data:
            kpi:
              event_name: "VM_ALIVE"
              metric_value: "1"
              metric_cond: "GT"
              metric_type: "UINT32"
              metric_collector:
                type: "ICMPPing"
                nicid: 0
                poll_frequency: 3
                polling_unit: "seconds"
                continuous_alarm: false
          rules: 
            admin_rules:
              rule:
                event_name: "VM_ALIVE"
                action:
                  - "ALWAYS log"
                  - "FALSE recover autohealing"
                  - "TRUE servicebooted.sh"
          config_data:
            configuration:
              dst: "bootstrap_config"
              variable:
                - name: "TECH_PACKAGE"
                  val: "ax"

  tasks:
    - name: Deploy ISRv
      uri:
        url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments
        method: POST
        headers:
          Content-Type: application/vnd.yang.data+json
          Accept: application/vnd.yang.data+json
        body_format: json
        body: "{{ body_data | to_json }}"
#        body: "{{ lookup('file', 'body_data.json') }}"
        status_code:
          - 201
        url_username: "{{ username }}"
        url_password: "{{ password }}"
        force_basic_auth: yes
        validate_certs: no
      ignore_errors: yes
```

## 4-2. Playbook実行ログ
特にエラーなく終了しました。

```
$ ansible-playbook -i inventory_nfvis.ini playbook_nfvis_post.yml 

PLAY [cisco] *************************************************************************************************

TASK [Deploy ISRv] *******************************************************************************************
ok: [nfvis]

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

## 4-3. GUI画面
ここでNFVISのGUI画面を見ると、Active状態のVNFが1つ表示され、ネットワーク`wan-net`と`lan-net`に論理接続されていました。

![無題1208_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/a96594ea-4ece-280c-23dc-d287c1a6fc32.png)
![無題1208_15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/5f005e62-ff54-3afa-842c-66e790547109.png)


# 5. ステータス情報の取得
続いて、各種ステータス情報をGETメソッドで取得してみます。

## 5-1. Ansible Playbook
各タスクの内容は以下の通りです。

1. イメージ情報を取得。Status Codeは`200 (OK)`に加え、情報がない場合`204 (No Content)`が返ってくる可能性があるため、2つをSuccess対象に指定。  
2. タスク1で取得した情報を表示。  
3. 起動中のVNF情報を取得。
4. タスク3で取得した情報を表示。  
5. 起動中のVNF情報詳細を取得。  
6. タスク5で取得した情報を表示。

```yaml:playbook_nfvis_get.yml
---

- hosts: cisco
  gather_facts: no
  connection: local

  tasks:
    - name: Get images   # (1)
      uri:
        url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/images
        method: GET
        headers:
          Content-Type: application/vnd.yang.data+json
          Accept: application/vnd.yang.data+json
        status_code:
          - 200
          - 204
        url_username: "{{ username }}"
        url_password: "{{ password }}"
        force_basic_auth: yes
        validate_certs: no
      register: result_images

    - name: Display images   # (2)
      debug:
        msg: "{{ result_images.json | default('None') }}"
      when: result_images.json is defined

    - name: Get running VNFs   # (3)
      uri:
        url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments
        method: GET
        headers:
          Content-Type: application/vnd.yang.data+json
          Accept: application/vnd.yang.data+json
        status_code:
          - 200
          - 204
        url_username: "{{ username }}"
        url_password: "{{ password }}"
        force_basic_auth: yes
        validate_certs: no
      register: result_vnfs

    - name: Display running VNFs   # (4)
      debug:
        msg: "{{ result_vnfs.json | default('None') }}"

    - name: Get running VNFs (detail)   # (5)
      uri:
        url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments?deep
        method: GET
        headers:
          Content-Type: application/vnd.yang.data+json
          Accept: application/vnd.yang.data+json
        status_code:
          - 200
          - 204
        url_username: "{{ username }}"
        url_password: "{{ password }}"
        force_basic_auth: yes
        validate_certs: no
      register: result_vnfs_detail

    - name: Display running VNFs   # (6)
      debug:
        msg: "{{ result_vnfs_detail.json | default('None') }}"
```

## 5-2. Playbook実行ログ
タスク2で、dCloud環境に元々存在しているASAvとISRvのイメージが表示されました。続いて、タスク4でVNF`ISRv`、タスク6でその詳細情報が表示されてる事が分かります。

```
$ ansible-playbook -i inventory_nfvis.ini playbook_nfvis_get.yml 

PLAY [cisco] *************************************************************************************************

TASK [Get images] ********************************************************************************************
ok: [nfvis]

TASK [Display images] ****************************************************************************************
ok: [nfvis] => {
    "msg": {
        "vmlc:images": {
            "image": [
                {
                    "name": "asav961.tar.gz"
                },
                {
                    "name": "isrv-universalk9.16.06.02.tar.gz"
                }
            ]
        }
    }
}

TASK [Get running VNFs] **************************************************************************************
ok: [nfvis]

TASK [Display running VNFs] **********************************************************************************
ok: [nfvis] => {
    "msg": {
        "vmlc:deployments": {
            "deployment": [
                {
                    "name": "ISRv"
                }
            ]
        }
    }
}

TASK [Get running VNFs (detail)] *****************************************************************************
ok: [nfvis]

TASK [Display running VNFs] **********************************************************************************
ok: [nfvis] => {
    "msg": {
        "vmlc:deployments": {
            "deployment": [
                {
                    "name": "ISRv",
                    "vm_group": [
                        {
                            "bootup_time": 600,
                            "config_data": {
                                "configuration": [
                                    {
                                        "dst": "bootstrap_config",
                                        "variable": [
                                            {
                                                "name": "TECH_PACKAGE",
                                                "val": [
                                                    "ax"
                                                ]
                                            }
                                        ]
                                    }
                                ]
                            },
                            "flavor": "ISRv-small",
                            "image": "isrv-universalk9.16.06.02.tar.gz",
                            "interfaces": {
                                "interface": [
                                    {
                                        "network": "int-mgmt-net",
                                        "nicid": 0
                                    },
                                    {
                                        "network": "wan-net",
                                        "nicid": 1
                                    },
                                    {
                                        "network": "lan-net",
                                        "nicid": 2
                                    }
                                ]
                            },
                            "kpi_data": {
                                "kpi": [
                                    {
                                        "event_name": "VM_ALIVE",
                                        "metric_collector": {
                                            "continuous_alarm": false,
                                            "nicid": 0,
                                            "poll_frequency": 3,
                                            "polling_unit": "seconds",
                                            "type": "ICMPPing"
                                        },
                                        "metric_cond": "GT",
                                        "metric_type": "UINT32",
                                        "metric_value": "1"
                                    }
                                ]
                            },
                            "name": "ROUTER",
                            "recovery_wait_time": 0,
                            "rules": {
                                "admin_rules": {
                                    "rule": [
                                        {
                                            "action": [
                                                "ALWAYS log",
                                                "FALSE recover autohealing",
                                                "TRUE servicebooted.sh"
                                            ],
                                            "event_name": "VM_ALIVE"
                                        }
                                    ]
                                }
                            }
                        }
                    ]
                }
            ]
        }
    }
}

PLAY RECAP ***************************************************************************************************
nfvis                      : ok=6    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

# 6. VNFの削除
最後に、作成したVNFをすべて削除してみます。

## 6-1. Ansible Playbook
各タスクの内容は以下の通りです。

1. （事前）起動中のVNF情報を取得。
2. （事前）タスク1で取得した情報を表示。  
↓ これ以降は`block`と`when`を用いて、VNFが1つでもデプロイされている場合のみ実施。  
3. 起動中のVNF名一覧を変数`running_vnfs`に格納。
4. `loop`を用いて、各VNFをDELETEメソッドで1つずつ削除。
5. （事後）起動中のVNF情報を取得。
6. （事後）タスク5で取得した情報を表示。

```yaml:playbook_nfvis_delete.yml
---

- hosts: cisco
  gather_facts: no
  connection: local

  tasks:
    - name: Get running VNFs (before)   # (1)
      uri:
        url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments
        method: GET
        headers:
          Content-Type: application/vnd.yang.data+json
          Accept: application/vnd.yang.data+json
        status_code:
          - 200
          - 204
        url_username: "{{ username }}"
        url_password: "{{ password }}"
        force_basic_auth: yes
        validate_certs: no
      register: result_vnfs_before

    - name: Display running VNFs (before)   # (2)
      debug:
        msg: "{{ result_vnfs_before.json | default('None') }}"

    - block:
      - name: Set running VNFs   # (3)
        set_fact:
          running_vnfs: "{{ result_vnfs_before.json['vmlc:deployments']['deployment'] }}"

      - name: Delete running VNFs   # (4)
        uri:
          url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments/deployment/{{item.name}}
          method: DELETE
          headers:
            Content-Type: application/vnd.yang.data+json
            Accept: application/vnd.yang.data+json
          status_code:
            - 200
            - 204
          url_username: "{{ username }}"
          url_password: "{{ password }}"
          force_basic_auth: yes
          validate_certs: no
        loop: "{{ running_vnfs }}"

      - name: Get running VNFs (after)   # (5)
        uri:
          url: https://{{ansible_host}}:{{ansible_port}}/api/config/vm_lifecycle/tenants/tenant/admin/deployments
          method: GET
          headers:
            Content-Type: application/vnd.yang.data+json
            Accept: application/vnd.yang.data+json
          status_code:
            - 200
            - 204
          url_username: "{{ username }}"
          url_password: "{{ password }}"
          force_basic_auth: yes
          validate_certs: no
        register: result_vnfs_after

      - name: Display running VNFs (after)   # (6)
        debug:
          msg: "{{ result_vnfs_after.json | default('None') }}"
      when: result_vnfs_before.json is defined
```


## 6-2. Playbook実行ログ
タスク2で表示されていたVNF`ISRv`が、タスク6で消えている事が分かります。

```
$ ansible-playbook -i inventory_nfvis.ini playbook_nfvis_delete.yml

PLAY [cisco] *************************************************************************************************

TASK [Get running VNFs (before)] *****************************************************************************
ok: [nfvis]

TASK [Display running VNFs (before)] *************************************************************************
ok: [nfvis] => {
    "msg": {
        "vmlc:deployments": {
            "deployment": [
                {
                    "name": "ISRv"
                }
            ]
        }
    }
}

TASK [Set running VNFs] **************************************************************************************
ok: [nfvis]

TASK [Delete running VNFs] ***********************************************************************************
ok: [nfvis] => (item={'name': 'ISRv'})

TASK [Get running VNFs (after)] ******************************************************************************
ok: [nfvis]

TASK [Display running VNFs (after)] **************************************************************************
ok: [nfvis] => {
    "msg": "None"
}

PLAY RECAP ***************************************************************************************************
nfvis                      : ok=6    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0  
```

## 6-3. GUI画面
NFVISのGUI画面でも、該当のVNFが削除されました。

![無題1208_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/471db2be-2a3e-8a32-c2bc-c32a3c2144fc.png)


# 最後に
今回は汎用モジュール`uri`使いましたが、Cisco DevNetで[ansible-nvfis](https://github.com/CiscoDevNet/ansible-nfvis)や[nfvis-ops](https://github.com/CiscoDevNet/nfvis-ops)も開発されているようです。Playbookもよりシンプルになりそうですので、実運用する場合はこちらも検討したいと思います。