ネットワーク自動化開発実践: NSO と PyATS を仲良く使う pyatstool


# はじめに
この記事はシスコの同志による Advent Calendar の一部として投稿しています

- 2017年版: https://qiita.com/advent-calendar/2017/cisco
- 2018年版: https://qiita.com/advent-calendar/2018/cisco
- 2019年版: https://qiita.com/advent-calendar/2019/cisco
- 2020年版: https://qiita.com/advent-calendar/2020/cisco
- 2020年版(2枚目): https://qiita.com/advent-calendar/2020/cisco2
- 2021年版: https://qiita.com/advent-calendar/2021/cisco
- 2021年版(2枚目): https://qiita.com/advent-calendar/2021/cisco2

本記事は2021年版 Advent Calendar (2枚目) の 5日目の投稿です

これまでネットワーク自動化ツールとして **Cisco Network Services Orchestrator (NSO)** や **PyATS** を取り上げてきましたが、今回はそれを組み合わせた Better Together な ( 語彙力() ) ものを作ってみました。

NSO や PyATS については以前にも[私自身](https://qiita.com/radiantmarch/items/9df086002ee3f4f334d2)や[同僚](https://ccieojisan.net/) がたくさん取り上げていますので、ぜひ参照していただけたら幸いです。


# Key Takeaways この記事を読むと:

- NSO と PyATS を組み合わせたツールを便利に使えるようになる :smile: 
- コードの中身を理解することで、自分で NSO のパッケージをかけるようになる :laughing: 


# pyatstool

NSO のパッケージを自作し pyatstool というものを作ってみました
https://github.com/radiantmarch/pyatstool

パッケージのインストール方法や使い方は github にも書いているのですが、せっかくですのでこちらの Qiita の記事でも紹介させていただこうと思いました。

ざっくり言うと、NSO からデバイスに対してコマンドを発行 (show コマンド等) する際に、裏で PyATS parser を利用して構造的な表示を可能にするものです。通常、NSO から `devices device live-status exec show/any` コマンドでは基本的に CLI アウトプットがそのまま取得されますが、それを便利に扱うツールです。


# テスト環境

以下の環境でテストしています

- NSO 5.6.2 on Linux Ubuntu 20.04.3
- Python 3.8.12
- PyATS 21.10
- IOSv 15.9(3) (CML 2.2 上で動作させています)


# 使い方

早速使ってみましょう。NSO CLI セッション上からデバイスに対して `pyats-parser` コマンドを使います.

```json
admin@ncs-pyatstool# devices device router1 pyats-parser "show inventory"
result {
    "main": {
        "chassis": {
            "IOSv": {
                "name": "IOSv",
                "descr": "IOSv chassis, Hw Serial#: 932IQH25MN2MAXTUOW9EY, Hw Revision: 1.0",
                "pid": "IOSv",
                "vid": "1.0",
                "sn": "932IQH25MN2MAXTUOW9EY"
            }
        }
    }
}
```

めでたく inventory 情報を PyATS parser を使ってパースして JSON 形式で出力できています！:sweat_smile:

他にも試してみましょう. 自動化のときに確認したくなることが多いインターフェイス情報の一覧も:sunglasses:

```json
admin@ncs-pyatstool# devices device router1 pyats-parser "show ip interface brief"
result {
    "interface": {
        "GigabitEthernet0/0": {
            "ip_address": "10.xx.xxx.xxx",
            "interface_is_ok": "YES",
            "method": "DHCP",
            "status": "up",
            "protocol": "up"
        },
        "GigabitEthernet0/1": {
            "ip_address": "unassigned",
            "interface_is_ok": "YES",
            "method": "NVRAM",
            "status": "administratively down",
            "protocol": "down"
        },
        "GigabitEthernet0/2": {
            "ip_address": "unassigned",
            "interface_is_ok": "YES",
            "method": "NVRAM",
            "status": "administratively down",
            "protocol": "down"
        },
        "GigabitEthernet0/3": {
            "ip_address": "unassigned",
            "interface_is_ok": "YES",
            "method": "NVRAM",
            "status": "administratively down",
            "protocol": "down"
        },
        "Router#": {
            "ip_address": "",
            "interface_is_ok": "",
            "method": "",
            "status": "",
            "protocol": ""
        }
    }
}
```

ルーティングテーブルも:sunglasses:

```json
admin@ncs-pyatstool# devices device router1 pyats-parser "show ip route vrf management"
result {
    "vrf": {
        "management": {
            "address_family": {
                "ipv4": {
                    "routes": {
                        "10.xx.xxx.0/24": {
                            "route": "10.xx.xxx.0/24",
                            "active": true,
                            "source_protocol_codes": "C",
                            "source_protocol": "connected",
                            "next_hop": {
                                "outgoing_interface": {
                                    "GigabitEthernet0/0": {
                                        "outgoing_interface": "GigabitEthernet0/0"
                                    }
                                }
                            }
                        },
(snip)

```
注: 内部アドレスを xx でマスクしてます。

などなど、[PyATS parser が対応している IOS コマンド](https://pubhub.devnetcloud.com/media/genie-feature-browser/docs/#/parsers) であればパースが可能です。いまのところ pyatstool 側が対応しているのが Cisco IOS だけなのですが、コードを少し拡張すれば他のデバイスにも対応できます。


なお JSON 表示にしたくない場合は `no-parser` オプションをつけると Raw テキスト出力が得られます。

```
admin@ncs-pyatstool# devices device router1 pyats-parser "show inventory" no-parser
result
> show inventory
NAME: "IOSv", DESCR: "IOSv chassis, Hw Serial#: 932IQH25MN2MAXTUOW9EY, Hw Revision: 1.0"
PID: IOSv              , VID: 1.0, SN: 932IQH25MN2MAXTUOW9EY
```

なお、PyATS parser が未対応のコマンドを指定した場合も、no-parser と同様に Raw テキストが返ってくるようになっています。



# ご注意

ミスリードとなるといけないので付け加えますと、pyatstool は NSO CLI 画面上の表示を構造化して見せているもので、API で取得できる結果自体が json になっているわけではありません。上記のように `result` というオブジェクト内に json strings として格納されますので、取得した結果をプログラミング処理する場合は、一旦 json strings を dict (python の場合) などに格納する処理をしてください。

とはいえ、テキスト出力をパースするよりも格段に結果を扱いやすくなっているのは確実だと思います。

願望としては PyATS が NSO で live-status パーサにネイティブで取り込まれると嬉しいのですが、、個人的に @ccieojisan にお願いしておきます。


# コード解説

せっかくですので pyatstool のコードの中身を解説します

パッケージの全体はこちら. パッケージ構造のひながたは、NSO の `ncs-make-package` コマンドで作成できます。

```bash:tree
$ cd packages/pyatstool
$ tree
.
├── README
├── README.md
├── load-dir
│   └── pyatstool.fxs
├── package-meta-data.xml
├── python
│   └── pyatstool
│       ├── __init__.py
│       └── main.py
├── src
│   ├── Makefile
│   ├── java
│   │   └── src
│   └── yang
│       └── pyatstool.yang
├── templates
└── test
(snip)
```

まず `pyatstool.yang` で device に対する augment を記述し、action を追加します。これでコマンドに `pyats-parser` が追加されます。

```yang:pyatstool.yang
    augment /ncs:devices/ncs:device {
            tailf:action pyats-parser {
                tailf:actionpoint pyats-parser;
                input {
                    leaf command {
                        tailf:cli-drop-node-name;
                        tailf:info "show command string";
                        description "show command string";
                        type string;
                        mandatory true;
                    }
                    leaf no-parser {
                        type empty;
                        tailf:info "Do not use pyats parsers.";
                    }
                }
                output {
                    leaf result {
                        type string;
                    }
                }
            }
        }
```

そして実際に、`pyats-parser` アクションで実行する内容を `main.py` で定義します。

まず YANG 内の actionpoint とマッチする action を登録し実際の処理をおこなう Class と紐付け:

```python:main.py
class Main(ncs.application.Application):
    def setup(self):
...
        self.register_action('pyats-parser', PyAtsParser)
...
```

そしてコードの上部に、実際に PyAtsParser クラスを記述します

```python:main.py(続き)
import ncs.maagic as maagic
from ncs.dp import Action

from genie.conf.base import Device
import json

...(snip)

class PyAtsParser(Action):

    @Action.action
    def cb_action(self, uinfo, name, kp, input, output):
        self.log.info('Action called (service=', kp, ')')

        with ncs.maapi.single_read_trans(uinfo.username, uinfo.context) as trans:
            # get root path
            root = ncs.maagic.get_root(trans, kp)
            service = ncs.maagic.cd(root, kp)
```

↑ ここで `ncs.maapi.single_read_trans` によって read only トランザクションを開始しています。


```python:main.py(続き)
            # device_name = service._parent._parent.name
            device_name = service.name
            device = root.ncs__devices.device[device_name]

            self.log.info('input = ', type(input), input)
            self.log.info('device_name = ', device_name)

            command = input.command
            self.log.info(command)
            live_input = device.live_status.ios_stats__exec.any.get_input()
            live_input.args = command.split(" ")

            live_output = device.live_status.ios_stats__exec.any(live_input)

```

↑ ここでは device に対して live_status コマンドの発行を実施しています。YANG の `input` 内容（コマンド列）を受け取り、それを live_status コマンドの内容としてデバイスに向けて発行しています。


```python:main.py(続き)
            ### with pyats parser
            output.result = ''
            if input.no_parser:
                output.result += live_output.result
            else:
                pyats_device = Device(device_name, os='ios')
                pyats_device.custom.setdefault("abstraction", {})["order"] = ["os"]
                try:
                    pyats_output = pyats_device.parse(command, output=live_output.result)
                    output.result = json.dumps(pyats_output, indent=4)
                except Exception as e:
                    output.result = '\nWarning from {}: pyats parser not found. Message: {}.\nDisplaying plan text result...'.format(pkg_name, str(e))
                    output.result += live_output.result

```

↑ ここでは live_status の結果 = デバイスからの結果出力を pyats parser に output として渡しています。pyats parser は通常は pyats 自身がデバイスにコマンドを発行しにいくのですが、このように output 引数で渡すことができるおかげで、NSO など外部ツールで取得したコマンド結果もパースすることができるわけです

そして最後に、YANG の output パラメータに、結果を json.dumps した結果 (json string) を格納しました。これで、CLI 上に結果が表示されるという流れでした。

このように、非常に少ないコードで NSO のコマンド強化ができてしまいました。（多くは PyATS に頼っているからですが）。



# おわりに

今回は私のつくった `pyatstool` を紹介させていただきました。ぜひ便利に活用いただくとともに、NSO で追加のアクションパッケージをどのようにコーディングすればよいかイメージを持っていただけたら幸いです。

今後も NSO の使い方やパッケージ開発について発信していきます。よろしければご覧ください。また、NSO や PyATS を使ったネットワーク自動化のコミュニティを広げるべく、ご関心のある方はぜひ [radiantmarch @ twitter](https://twitter.com/radiantmarch) にご連絡ください。


# 免責事項

本サイトおよび対応するコメントにおいて表明される意見は、投稿者本人の個人的意見であり、シスコの意見ではありません。本サイトの内容は、情報の提供のみを目的として掲載されており、シスコや他の関係者による推奨や表明を目的としたものではありません。各利用者は、本 Web サイトへの掲載により、投稿、リンクその他の方法でアップロードした全ての情報の内容に対して全責任を負い、本 Web サイトの利用に関するあらゆる責任からシスコを免責することに同意したものとします。