RADKit でルータの状態確認を自動化してみた！

# はじめに

この記事は 2024 年 CiscoSystems 合同会社の社員有志による Advent Calendar の記事の一つです :santa_tone1:

https://qiita.com/advent-calendar/2024/cisco

# RADKit で自動化してみよう！

最近、シスコから RADKit (読み方: らどきっと) という無料で利用できるトラブルシューティングツールがリリースされました。
RADKit を使うことで、リモートからネットワーク製品などの情報を収集することができます。

今回は、この RADKit を活用して自動化やってみた系の記事を作成してみました！

いろんなネットワークの検証試験を並行して進めていたりすると、どこかのルータの電源が落ちたままになっていたとか、どこかのインターフェイスがシャットダウンされたままになっていたみたいな問題に遭遇することがあります。。。:sweat_smile:

なので、今回は RADKit を使ってログ収集を自動化して、簡易的なルータの状態確認をやってみたいと思います！

## RADKit

RADKit についてざっくりいうと上述の通りですが、もっと RADKit について知りたい！という方は以下をご参照ください。

日本語ドキュメント
[無料サービス RADKit の紹介 - セキュアなツールでより迅速な問題解決が可能に！](https://community.cisco.com/t5/-/-/ta-p/5018592)

英語ドキュメント
[Cisco RADKit](https://radkit.cisco.com/)

使い方に関するドキュメント
[Cisco Remote Automation Development Kit (RADKit)](https://radkit.cisco.com/docs/index.html)

RADKit は主に RADKit Service、RADKit Client、RADKit Cloud から構成されており、RADKit Client を使うことでデバイスにアクセスすることができます。

## RADKit でルータの状態確認を自動化してみる

以下の簡易的なトポロジで、自動化を試していきたいと思います :tools:

```mermaid
flowchart LR
    R1["R1<br>Lo0: 1.1.1.1"] <--> R2["R2<br>Lo0: 2.2.2.2"]
    R2 <--> R3["R3<br>Lo0: 3.3.3.3"]
```

それぞれのルータ間で OSPF ネイバが貼られており、R1 と R3 の Loopback 間で BGP ネイバが確立されています。
R1 からの疎通性、BGP/OSPF ネイバ、各インターフェイスの状態確認を自動化していきます。

RADKit Service でデバイス登録やユーザ登録が行われている状態から始めていきます。

![Screenshot 2024-12-09 123302.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3927399/7c0b9c9b-b13c-a8d3-c817-3c3ed30fce36.png)

## RADKit Client

RADKit Service で登録されているデバイスのインベントリ情報を RADKit Client で確認してみます。
RADKit Client は Python ベースのフロントエンドとなっており、インタラクティブに以下のような感じで実行できます。

```
$ radkit-client
-snip-
>>> client = sso_login("radkit-user@example.com")
>>> service = client.service_cloud("xxxx-yyyy-zzzz")
>>> service.inventory
name    host          device_type    Terminal    Netconf    SNMP    Swagger    HTTP    description    failed
------  ------------  -------------  ----------  ---------  ------  ---------  ------  -------------  --------
r1      192.0.2.1     IOS_XR         True        False      False   False      False                  False
r2      192.0.2.2     IOS_XR         True        False      False   False      False                  False
r3      192.0.2.3     IOS_XR         True        False      False   False      False                  False
```

[ドキュメント](https://radkit.cisco.com/docs/index.html) を見てみると、[Single device, single command](https://radkit.cisco.com/docs/features/feature_exec.html#single-device-single-command) を使うとコマンドを実行できそうなので、やってみます。

```
>>> r1 = service.inventory['r1']
>>> req = r1.exec("ping 3.3.3.3").wait()
>>> print(req.result.data)
RP/0/RP0/CPU0:R1#ping 3.3.3.3
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 3.3.3.3, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 2/2/2 ms
RP/0/RP0/CPU0:R1#
```

いけそう！ですが、これまでインタラクティブに実行していたので、スクリプトとしてどうやって実行するんだ？ということで、もうちょっとドキュメント読んでみます。。。

すると、radkit-client に script というコマンドがありました。

```
$ radkit-client --help
Usage: radkit-client [OPTIONS] COMMAND [ARGS]...
-snip-
Commands:
  enroll           enroll for client certificate
  network-console  cli mode
  script           script and arguments
```

Python スクリプトを作成して、実行してみます。

```
$ radkit-client script sample.py
-snip(認証のためブラウザにとばされるのとたくさんの INFO メッセージ)-
RP/0/RP0/CPU0:R1#ping 3.3.3.3
Type escape sequence to abort.
Sending 5, 100-byte ICMP Echos to 3.3.3.3, timeout is 2 seconds:
!!!!!
Success rate is 100 percent (5/5), round-trip min/avg/max = 2/2/2 ms
RP/0/RP0/CPU0:R1#
```

[Client API](https://radkit.cisco.com/docs/client_api/client_api.html#importing-the-client-api) が用意されているので、それを使ってスクリプトは以下のように書くことで実行できました。

```py:sample.py
from radkit_client.sync import Client

with Client.create() as client:
    client.sso_login(email)
    service = client.service_cloud(serial).wait()
    r1 = service.inventory['r1']
    req = r1.exec("ping 3.3.3.3").wait()
    print(req.result.data)
```

しかし、上のスクリプトだと実行時に毎回ブラウザへ飛ばされて認証するのがめんどくさい。。。

もう少しドキュメントを読んでみると [Enrolling the Client](https://radkit.cisco.com/docs/client/client_advanced.html) なるものを発見しました。
以下のように一旦ログインして、クライアントを登録します。

```
>>> client.enroll_client()
New private key password: *********
Confirm: *********
```

上記のようにクライアントを登録することにより、`client.sso_login` ではなく `client.certificate_login` でログインできるとのことで、スクリプトをアップデートします。

```py:sample.py
from radkit_client.sync import Client

with Client.create() as client:
    client.certificate_login(identity=email, private_key_password=password)
    service = client.service_cloud(serial).wait()
    r1 = service.inventory['r1']
    req = r1.exec("ping 3.3.3.3").wait()
    print(req.result.data)
```

このスクリプトで実行するとブラウザに飛ばされることがなくなりました！
また、[client.logging.level](https://radkit.cisco.com/docs/settings/settings_reference.html#client-logging-level) を設定することで、大量の INFO メッセージも抑制できました。
初回の `<Timestamp> INFO  | Logging configured...` は消えない。。。？

```
$ radkit-client -s client.logging.level 30 script sample.py
```

あとはひたすら書いていくのみです。

:hammer_pick: (カンキンゴンガンギンゴンガンギン)

できあがりです！

```
$ radkit-client -s client.logging.level 30 script sample.py
-snip-
Checking status...
----------------------------------------
Ping from R1:
- To R2:        Pass
- To R3:        Pass
----------------------------------------
BGP (R1 -- R3): Pass
----------------------------------------
OSPF:
 - (R1 -- R2):  Pass
 - (R2 -- R3):  Pass
----------------------------------------
Interface:
- R1:
  - Hu0/0/1/1:  Up
- R2:
  - Hu0/0/0/1:  Up
  - Hu0/0/0/2:  Up
- R3:
  - Hu0/2/0/1:  Up
----------------------------------------
```

とりあえずこんな感じで出力させてみました。
R3 のインターフェースをシャットダウンして、改めて実行してみます。
失敗時には、コマンドの実行結果を出力させるようにしてみました。

```
$ radkit-client -s client.logging.level 30 script sample.py
-snip-
Checking status...
----------------------------------------
Ping from R1:
- To R2:        Pass
- To R3:        Failed
  - Details:    Success rate is 0 percent (0/5)
----------------------------------------
BGP (R1 -- R3): Failed
- Details:      BGP state = Idle (No route to multi-hop neighbor)
----------------------------------------
OSPF:
 - (R1 -- R2):  Pass
 - (R2 -- R3):  Failed
  - Details:    No Entry
----------------------------------------
Interface:
- R1:
  - Hu0/0/1/1:  Up
- R2:
  - Hu0/0/0/1:  Up
  - Hu0/0/0/2:  Down
    - Details:  HundredGigE0/0/0/2 is down, line protocol is down
- R3:
  - Hu0/2/0/1:  Down
    - Details:  HundredGigE0/2/0/1 is administratively down, line protocol is administratively down
----------------------------------------
```

突貫ではありますが、サンプルコードは以下です。

```py:sample.py
with Client.create() as client:
    client.certificate_login(identity=email, private_key_password=password)
    service = client.service_cloud(serial).wait()

    r1 = service.inventory['r1']
    r2 = service.inventory['r2']
    r3 = service.inventory['r3']

    devices = {
        'R1': {
            'inventory': r1,
            'ports': ['Hu0/0/1/1'],
            'lo': '1.1.1.1',
            'connect_to': ['R2']
        },
        'R2': {
            'inventory': r2,
            'ports': ['Hu0/0/0/1', 'Hu0/0/0/2'],
            'lo': '2.2.2.2',
            'connect_to': ['R1', 'R3']
        },
        'R3': {
            'inventory': r3,
            'ports': ['Hu0/2/0/1'],
            'lo': '3.3.3.3',
            'connect_to': ['R2']
        },
    }

    print('Checking status...')
    print('----------------------------------------')
    print('Ping from R1:')
    for device in devices:
        if device != 'R1':
            ping = r1.exec("ping {}".format(devices[device]['lo'])).wait()
            if ping.result.data.find('Success rate is 100 percent') != -1:
                result = 'Pass'
            else:
                result = 'Failed'
            print('- To {}:\t{}'.format(device, result))
            if result == 'Failed':
                print('  - Details:\t{}'.format(ping.result.data.splitlines()[-2]))
    print('----------------------------------------')

    show_bgp = r1.exec("show bgp neighbor {}".format(devices['R3']['lo'])).wait()
    if show_bgp.result.data.find('BGP state = Established') != -1:
        result = 'Pass'
    else:
        result = 'Failed'
    print('BGP (R1 -- R3):\t{}'.format(result))
    if result == 'Failed':
        print('- Details:\t{}'.format(show_bgp.result.data.splitlines()[6].strip()))
    print('----------------------------------------')

    checked = []
    print('OSPF:')
    for device in devices:
        for remote in devices[device]['connect_to']:
            if remote not in checked:
                show_ospf = devices[device]['inventory'].exec("show ospf neighbor {}".format(devices[remote]['lo'])).wait()
                if show_ospf.result.data.find('State is FULL') != -1:
                    result = 'Pass'
                else:
                    result = 'Failed'
                print(' - ({} -- {}):\t{}'.format(device, remote, result))
                if result == 'Failed':
                    if len(show_ospf.result.data.splitlines()) == 3:
                        print('  - Details:\tNo Entry')
                    else:
                        print('  - Details:\t{}'.format(show_ospf.result.data.splitlines()[10]))
        checked.append(device)
    print('----------------------------------------')

    print('Interface:')
    for device in devices:
        print('- {}:'.format(device))
        for interface in devices[device]['ports']:
            show_interface = devices[device]['inventory'].exec("show interface {}".format(interface)).wait()
            if show_interface.result.data.find("up, line protocol is up") != -1:
                result = 'Up'
            else:
                result = 'Down'
            print('  - {}:\t{}'.format(interface, result))
            if result == 'Down':
                print('    - Details:\t{}'.format(show_interface.result.data.splitlines()[2]))
    print('----------------------------------------')
```

# 終わりに

RADKit を使ってルータの状態確認を自動化してみました！

RADKit は、シスコのサポートエンジニアがリモートから機器の情報を取得できるという利点もありますが、今回のように API を活用することで社内の自動化ツールとして活用することもできます。
また、RADKit サービスを経由して Support Case Manager(SCM) などのサービスリクエスト管理ツールへログファイルを送付することもできるので、ログのアップロードも自動化することができます。

RADKit はいろいろ可能性を秘めているツールだと思いますので、ぜひお試しください！

# 免責事項

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