Cisco Guest ShellをAPIサーバにしてPostmanから操作してみた

# はじめに

Cisco Guest Shellは、ホストデバイス（IOS-XE）とは分離されたLinuxベースのコンテナ環境で、Python等を利用してIOS-XE設定を自動制御可能です。
今回は、Python Webフレームワーク「Flask」を用いてGuest Shell上でAPIサーバを構築し、Postmanからshowコマンド結果の取得、インターフェース設定変更を行ってみました。

# CSR1000V セットアップ

### IOS-XE
IOS-XE側で仮想インターフェース`VirtualPortGroup0`（192.168.30.1）を作成し、Guest Shellのアドレス（192.168.30.2）と関連付けを行い、外部からGuest Shellへのアクセスを可能としています。
環境依存のため割愛しますが、インターネットアクセスのため、Guest ShellのアドレスをGigabitEthernet1のアドレスに変換するPAT設定も行っています。

```text:設定Config

Router(config)#iox

Router(config)#ip http server

Router(config)#interface GigabitEthernet1
Router(config-if)# ip address 192.168.100.196 255.255.255.0
Router(config-if)# exit

Router(config)#interface VirtualPortGroup0
Router(config-if)# ip address 192.168.30.1 255.255.255.0
Router(config-if)# exit

Router(config)#app-hosting appid guestshell
Router(config-app-hosting)# app-vnic gateway0 virtualportgroup 0 guest-interface 0
Router(config-app-hosting-gateway0)# guest-ipaddress 192.168.30.2 netmask 255.255.255.0
Router(config-app-hosting-gateway0)# app-default-gateway 192.168.30.1 guest-interface 0
Router(config-app-hosting)# name-server0 192.168.100.1
Router(config-app-hosting)# end

Router#guestshell enable
```

```text:確認コマンド
Router#show iox-service

IOx Infrastructure Summary:
---------------------------
IOx service (CAF) 1.10.0.1 : Running
IOx service (HA)          : Not Supported
IOx service (IOxman)      : Running
IOx service (Sec storage) : Not Supported
Libvirtd   1.3.4          : Running

Router#show app-hosting list
App id                                   State
---------------------------------------------------------
guestshell                               RUNNING
```


### Guest Shell
デフォルト設定をそのまま使用。

```
Router#guestshell
[guestshell@guestshell ~]$ sudo ifconfig
eth0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
        inet 192.168.30.2  netmask 255.255.255.0  broadcast 192.168.30.255
(省略)

[guestshell@guestshell ~]$ netstat -nr
Kernel IP routing table
Destination     Gateway         Genmask         Flags   MSS Window  irtt Iface
0.0.0.0         192.168.30.1    0.0.0.0         UG        0 0          0 eth0
192.168.30.0    0.0.0.0         255.255.255.0   U         0 0          0 eth0
```

### Pythonパッケージのインストール
プリインストールされているPython2.7.5を使用し、Flaskと、showコマンドのパーサーテンプレート[ntc_templates](https://github.com/networktocode/ntc-templates)をインストールしました。

```
[guestshell@guestshell ~]$ python -V
Python 2.7.5
[guestshell@guestshell ~]$ pip -V
pip 20.2.3 from /usr/lib/python2.7/site-packages/pip (python 2.7)
[guestshell@guestshell ~]$ pip install flask
[guestshell@guestshell ~]$ pip install ntc_templates
```

# HTTP GETでshowコマンド結果を取得

### Pythonコード
Guest Shellのホームディレクトリ直下にapi.pyを作成しました。

```
[guestshell@guestshell ~]$ pwd
/home/guestshell
[guestshell@guestshell ~]$ touch api.py
```

処理の大まかな流れは以下の通りです。

- 実行のトリガーは、APIクライアントからURI `http://<Guest ShellのIPアドレス>/show/<コマンド名を_を繋いだもの>`宛てのHTTP GET
- 関数`getCommand()`を実行
- URI末尾の'_'を' '(スペース)に変換し、showコマンドを生成
- プリインストールされているCisco CLI Pythonモジュールでshowコマンドを実行
- 出力結果をNTC-templates/TextFSMでパースしてJSONで返す
- 途中で失敗した場合はエラーメッセージをJSONで返す

また外部からアクセスするために、`app.run()`の引数として`host='0.0.0.0'`を指定しています。

```python:api.py
from flask import Flask, jsonify, request
from cli import configurep, cli
from ntc_templates.parse import parse_output

app = Flask(__name__)

@app.route("/show/<command>", methods=["GET"])
def getCommand(command):
    cmd = "show " + command.replace("_", " ")
    try:
        sh_output = cli(cmd)
        sh_output_parsed = parse_output(platform="cisco_ios", command=cmd, data=sh_output)
        return jsonify(sh_output_parsed)
    except:
        return jsonify([{"result": "Fail to parse the output"}])

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=8080)
```

### Python実行/APIサーバ起動

```
[guestshell@guestshell ~]$ python api.py
 * Serving Flask app "api" (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http://0.0.0.0:8080/ (Press CTRL+C to quit)
```

### PostmanからHTTP GET
Postmanからshowコマンド`show ip interface brief`の結果を取得する例です。
GETで以下の通りURIを入力します。末尾が`ip_int_brief`と省略されていますが、NTC-templates側で許容された省略形であれば問題なくパース可能です。
![0927_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/51fa7889-713e-9b5c-1911-ea8243c45298.png)

# HTTP POSTでインターフェース設定変更
### Pythonコード
上記のapi.pyにコードを追加しました。処理の大まかな流れは以下の通りです。

- 実行のトリガーは、APIクライアントからURI `http://<Guest ShellのIPアドレス>/set/interface`宛てのHTTP POST。Body内にインタフェースの設定パラメータをJSON形式で記載。
- 関数`setInterface()`を実行
- 設定パラメータに従いCLIコマンドを生成
- Cisco CLI Pythonモジュールで設定変更と設定保存を実施
- 成功した場合は、SuccessメッセージをJSONで返す

編集後、再度Python実行/APIサーバ起動を行います。

```python:api.py(追加分)
@app.route("/set/interface", methods=["POST"])
def setInterface():
    interface_cmd = "interface " + request.json['interface']

    if 'state' in request.json:
        if request.json['state'] == "enabled":
            state_cmd = "no shutdown"
        elif request.json['state']  == "disabled":
            state_cmd = "shutdown"
    else:
            state_cmd = ""

    if 'description' in request.json:
        description_cmd = "description " + request.json['description']
    else:
        description_cmd = ""

    if 'address' in request.json:
        address_cmd = "ip address " + request.json['address'] + " " + request.json['netmask']
    else:
        address_cmd = ""

    configurep([interface_cmd, state_cmd, description_cmd, address_cmd, "end"])
    cli("write")

    return jsonify([{"result": "Success"}])
```

### PostmanからHTTP POST
Postmanから`GigabitEthernet2`のDescription、IPアドレスの設定と、ポート開放を行う例です。

POSTで以下の通りURIを入力し、HeaderでContent-Typeを`application/json`に指定します。
![0927_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/6832c1b0-55dd-43f4-f4b6-22314a0bf89a.png)

Bodyに設定パラメータを記載します。下記の内、インターフェース名`interface`は必須ですが、その他の開放/閉塞`state`、Description`description`、IPアドレス`address/netmask`の指定は任意です。
![0927_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/3b2bafc1-7b52-bcdb-3903-e61380a89e91.png)

Sendボタンをクリックし、成功した場合は以下メッセージが表示されます。
![0927_04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/706685ad-6089-c4c4-c873-9f2ece11a071.png)

api.py実行中のターミナル画面からも、設定状況が確認できます。

```
192.168.100.100 - - [27/Sep/2020 05:59:20] "POST /set/interface HTTP/1.1" 200 -
Line 1 SUCCESS: interface GigabitEthernet2
Line 2 SUCCESS: no shutdown
Line 3 SUCCESS: description TEST
Line 4 SUCCESS: ip address 10.1.1.1 255.255.255.0
Line 5 SUCCESS: end
```

IOS-XE側でも問題なく設定変更、保存が行われていました。

```
Router#sh conf | begin interface GigabitEthernet2
interface GigabitEthernet2
 description TEST
 ip address 10.1.1.1 255.255.255.0
 negotiation auto
 no mop enabled
 no mop sysid
```

# 最後に
簡単な例ですが、無事Guest ShellでAPIサーバが構築出来ました。ただし、Guest Shellは認証無しでデバイスのshowコマンド実行/設定変更が出来てしまうので、セキュリティや証跡管理の観点で課題はあるかもしれません。
また今回の用途であれば、Guest ShellのようなOn-Box形式ではなく、Ansibleのような外部ツールを使ってOff-Boxで制御するのがベターかも知れません。