うさラボ

お勉強と備忘録

NW機器SSH/Telnet接続PythonライブラリのScrapliを試してみた

エーピーコミュニケーションズ Advent Calendar 2021 11日目の記事です

qiita.com

Scrapliとは

ネットワーク機器へTelnetまたはSSHの接続するためのPythonライブラリです。
特徴としては、

  1. 簡単に始められる
  2. 速い
  3. 開発が簡単
  4. 拡張性がある(プラグ可能)

トランスポート部分のカスタマイズができるのも特徴です、ssh2やparamiko,telnetなど柔軟に変更ができるようです。しかし、トランスポートはよっぽどの要件がない限り変更しないで十分なようです。

Scrapliのメインリポジトリ github.com

サポートするドライブ(コア)を拡張し様々な機器に対応するためのscrapli_community github.com

Netconf対応すためのscrapli_netconf github.com

Nornirのプライグインとして利用するnornir_scrapli github.com

テストのために「実際の」ネットワークデバイスのように見えるセミインタラクティブSSHサーバーを作成するためのツールscrapli_replay github.com

NW機器の対応状況(2021/12/11時点)

scrapli

scrapli_community

  • aethra
  • alcatel
  • edgecore
  • eltex
  • fortinet
  • hp
  • huawei
  • mikrotik
  • nokia
  • paloalto
  • ruckus
  • simens

Netmikoとの違い

似たようなライブラリにNetmikoがあります。メソッドの構成は似ています(send_command,send_commandsなど)

違いとしては、サポートしているデバイスの数、Netconfのサポートの有無でしょうか。 Netmikoはサポートしているデバイスの数がとても多いです。 github.com

scrapliではまだやり方が分かっていないだけなんですが、Netmikoではredispatchを使ってNW機器の踏み台アクセスもできます。 (SVを踏み台にするのはScrapliでも可能)

どちらも機能としてパーサーと連携もできるため、どちらを使うか?は利用しているデバイスの種類や設定変更によって決めることになりそうです。処理自体もScrapliのほうが速いようです。
手元で比較したときは、close(切断)の処理がNetmikoは数秒かかってしまうのでスクリプトの終了まではScrapliのほうが速いといった結果になりました。

Netmikoは公式ドキュメントがとてもおしゃれです。

環境

Python 3.9.2

scrapli==2021.7.30

インストール

最低限のインストール

pip install scrapli

他の機能を使う場合に、ライブラリをまとめてインストール

pip install scrapli[full]
pip install scrapli[paramiko]
pip install scrapli[ssh2]
pip install scrapli[asyncssh]
pip install scrapli[textfsm]
pip install scrapli[genie]
pip install scrapli[ttp]

使ってみる

ログイン/ログアウト

from scrapli import Scrapli

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

conn = Scrapli(**device)
conn.open()
print(conn.get_prompt())
conn.close()

platformを指定することで対応するDriverを選択してくれるようです。
platformを指定せずにIOSXEDriver()を利用しても、同じ挙動になります。
接続はopen()でログアウトはclose()で実施します。

get_prompt()はその名の通りプロンプトを取得してくれます

Scrapli# python example.py 
csr1000v-1#

showコマンドの実行

続いて、ログイン後showコマンドを実行してみます。 send_command()/send_commandsを利用します。 ter len 0など表示に関する設定は自動的に行われるため、showコマンドのみを指定します。

send_command: 1コマンド実行

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_command("show ip interface brief")


print(res.start_time)
print(res.result)
print(res.finish_time)

resultに実行結果が格納されているので結果の確認はres.resultを表示させます。

# python show.py 
2021-12-11 12:21:18.694178
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    up
2021-12-11 12:21:18.943480

send_commands: 複数コマンド実行 複数のコマンドを実行するにはsend_commands()を利用します。
commands引数にリスト形式でshowコマンドを指定します。

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_commands(commands=["show ip interface brief",
                                      "show version"])
print(res.result) # すべて表示
print(res[0].result) # 1つ目の要素(show ip interface brief)を表示
print(res[1].result) # 2つ目の要素(show version)を表示

実行結果も、リストになっているためX番目の結果を表示するにはres[X].resultと指定します。

python show2.py 
# すべて表示
show ip interface brief <- 実行したコマンドも含まれる
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    upshow version <- 実行したコマンドも含まれる
Cisco IOS XE Software, Version 17.03.01a
Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2020 by Cisco Systems, Inc.
Compiled Wed 12-Aug-20 00:16 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON
csr1000v-1 uptime is 1 day, 3 hours, 57 minutes
Uptime for this control processor is 1 day, 3 hours, 58 minutes
System returned to ROM by reload
System image file is "bootflash:packages.conf"
Last reload reason: reload



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

License Level: ax
License Type: N/A(Smart License Enabled)
Next reload license Level: ax

The current throughput level is 1000 kbps


Smart Licensing Status: UNREGISTERED/No Licenses in Use

cisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.
Processor board ID 9ESGOBARV9D
Router operating mode: Autonomous
3 Gigabit Ethernet interfaces
32768K bytes of non-volatile configuration memory.
3978420K bytes of physical memory.
6188032K bytes of virtual hard disk at bootflash:.

Configuration register is 0x2102
# 1つ目の要素(show ip interface brief)を表示
Interface              IP-Address      OK? Method Status                Protocol
GigabitEthernet1       10.10.20.48     YES NVRAM  up                    up
GigabitEthernet2       192.168.1.1     YES other  up                    up
GigabitEthernet3       unassigned      YES NVRAM  administratively down down
Loopback1              56.56.56.56     YES manual up                    up

# 2つ目の要素(show version)を表示
Cisco IOS XE Software, Version 17.03.01a
Cisco IOS Software [Amsterdam], Virtual XE Software (X86_64_LINUX_IOSD-UNIVERSALK9-M), Version 17.3.1a, RELEASE SOFTWARE (fc3)
Technical Support: http://www.cisco.com/techsupport
Copyright (c) 1986-2020 by Cisco Systems, Inc.
Compiled Wed 12-Aug-20 00:16 by mcpre


Cisco IOS-XE software, Copyright (c) 2005-2020 by cisco Systems, Inc.
All rights reserved.  Certain components of Cisco IOS-XE software are
licensed under the GNU General Public License ("GPL") Version 2.0.  The
software code licensed under GPL Version 2.0 is free software that comes
with ABSOLUTELY NO WARRANTY.  You can redistribute and/or modify such
GPL code under the terms of GPL Version 2.0.  For more details, see the
documentation or "License Notice" file accompanying the IOS-XE software,
or the applicable URL provided on the flyer accompanying the IOS-XE
software.


ROM: IOS-XE ROMMON
csr1000v-1 uptime is 1 day, 3 hours, 57 minutes
Uptime for this control processor is 1 day, 3 hours, 58 minutes
System returned to ROM by reload
System image file is "bootflash:packages.conf"
Last reload reason: reload



This product contains cryptographic features and is subject to United
States and local country laws governing import, export, transfer and
use. Delivery of Cisco cryptographic products does not imply
third-party authority to import, export, distribute or use encryption.
Importers, exporters, distributors and users are responsible for
compliance with U.S. and local country laws. By using this product you
agree to comply with applicable laws and regulations. If you are unable
to comply with U.S. and local laws, return this product immediately.

A summary of U.S. laws governing Cisco cryptographic products may be found at:
http://www.cisco.com/wwl/export/crypto/tool/stqrg.html

If you require further assistance please contact us by sending email to
export@cisco.com.

License Level: ax
License Type: N/A(Smart License Enabled)
Next reload license Level: ax

The current throughput level is 1000 kbps


Smart Licensing Status: UNREGISTERED/No Licenses in Use

cisco CSR1000V (VXE) processor (revision VXE) with 715705K/3075K bytes of memory.
Processor board ID 9ESGOBARV9D
Router operating mode: Autonomous
3 Gigabit Ethernet interfaces
32768K bytes of non-volatile configuration memory.
3978420K bytes of physical memory.
6188032K bytes of virtual hard disk at bootflash:.

Configuration register is 0x2102

パーサーと組み合わせる

CLIのテキストデータを構造化するために、パースすることもできます。

  • textfsm_parse_output
  • ttp_parse_output
  • genie_parse_output
from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res = conn.send_commands(commands=["show ip interface brief",
                                      "show version"])


[ pprint.pprint(i.textfsm_parse_output()) for i in res]
[ pprint.pprint(i.genie_parse_output()) for i in res]

textfsmパーサとgenieパーサを試します。事前にntc-templates,genieのライブラリをインストールしておきます。 この2つのパーサはテンプレートを指定せずにすぐに利用が可能です。

TTPは利用するテンプレートを指定する必要があるためひと手間必要なので割愛します。

## textfsm(ntc-template): show interface brief
[{'intf': 'GigabitEthernet1',
  'ipaddr': '10.10.20.48',
  'proto': 'up',
  'status': 'up'},
 {'intf': 'GigabitEthernet2',
  'ipaddr': '192.168.1.1',
  'proto': 'up',
  'status': 'up'},
 {'intf': 'GigabitEthernet3',
  'ipaddr': 'unassigned',
  'proto': 'down',
  'status': 'administratively down'},
 {'intf': 'Loopback1', 'ipaddr': '56.56.56.56', 'proto': 'up', 'status': 'up'}]

## textfsm(ntc-template): show version
[{'config_register': '0x2102',
  'hardware': ['CSR1000V'],
  'hostname': 'csr1000v-1',
  'mac': [],
  'reload_reason': 'reload',
  'restarted': '',
  'rommon': 'IOS-XE',
  'running_image': 'packages.conf',
  'serial': ['9ESGOBARV9D'],
  'uptime': '1 day, 4 hours, 36 minutes',
  'uptime_days': '1',
  'uptime_hours': '4',
  'uptime_minutes': '36',
  'uptime_weeks': '',
  'uptime_years': '',
  'version': '17.3.1a'}]

## genie: show interface brief
{'interface': {'GigabitEthernet1': {'interface_is_ok': 'YES',
                                    'ip_address': '10.10.20.48',
                                    'method': 'NVRAM',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet2': {'interface_is_ok': 'YES',
                                    'ip_address': '192.168.1.1',
                                    'method': 'other',
                                    'protocol': 'up',
                                    'status': 'up'},
               'GigabitEthernet3': {'interface_is_ok': 'YES',
                                    'ip_address': 'unassigned',
                                    'method': 'NVRAM',
                                    'protocol': 'down',
                                    'status': 'administratively down'},
               'Loopback1': {'interface_is_ok': 'YES',
                             'ip_address': '56.56.56.56',
                             'method': 'manual',
                             'protocol': 'up',
                             'status': 'up'}}}

## genie: show version
{'version': {'chassis': 'CSR1000V',
             'chassis_sn': '9ESGOBARV9D',
             'compiled_by': 'mcpre',
             'compiled_date': 'Wed 12-Aug-20 00:16',
             'curr_config_register': '0x2102',
             'disks': {'bootflash:.': {'disk_size': '6188032',
                                       'type_of_disk': 'virtual hard disk'}},
             'hostname': 'csr1000v-1',
             'image_id': 'X86_64_LINUX_IOSD-UNIVERSALK9-M',
             'image_type': 'production image',
             'label': 'RELEASE SOFTWARE (fc3)',
             'last_reload_reason': 'reload',
             'license_level': 'ax',
             'license_type': 'N/A(Smart License Enabled)',
             'main_mem': '715705',
             'mem_size': {'non-volatile configuration': '32768',
                          'physical': '3978420'},
             'next_reload_license_level': 'ax',
             'number_of_intfs': {'Gigabit Ethernet': '3'},
             'os': 'IOS-XE',
             'platform': 'Virtual XE',
             'processor_type': 'VXE',
             'returned_to_rom_by': 'reload',
             'rom': 'IOS-XE ROMMON',
             'router_operating_mode': 'Autonomous',
             'rtr_type': 'CSR1000V',
             'system_image': 'bootflash:packages.conf',
             'uptime': '1 day, 4 hours, 36 minutes',
             'uptime_this_cp': '1 day, 4 hours, 38 minutes',
             'version': '17.3.1a',
             'version_short': '17.3',
             'xe_version': '17.03.01a'}}

パースも簡単にできます。

設定変更

send_configsで設定を送ることができます。 configureモードへの変更などはドライバー側で入力してくれるため、設定のみを指定します。

from scrapli import Scrapli
import pprint

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res_configure = conn.send_configs(["interface loopback100", "description configured by scrapli"])

print(res_configure.result)

resultには実行したコマンドと実行時の標準出力が格納されています。

python configure1.py 
interface loopback100
description configured by scrapli

存在しないリソースを消そうとした。などでエラーメッセージが出た場合もresultに格納されます

python configure_back.py  # loopback100がない状態で削除をしようとした
no interface loopback100
                                        ^
% Invalid input detected at '^' marker.

ここまでの要素を組み合わせて、 参照→変更→参照の流れを実施します。 設定前と設定後のshow interfaceを取得しパースしてテーブルにします。 テーブル作成のため、numpyとtabulateを利用します。

from scrapli import Scrapli
import tabulate
import numpy as np

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "platform": "cisco_iosxe"
}

with Scrapli(**device) as conn:
   res_before = conn.send_command("show interfaces")
   res_configure = conn.send_configs(["interface loopback100", "description configured by scrapli"])
   res_after = conn.send_command("show interfaces")

parsed_before = res_before.genie_parse_output()
parsed_after = res_after.genie_parse_output()


## NOTE: 出力結果をテーブルにする処理
after_interface_list = list()
after_description_list = list()
before_interface_list = list()
before_description_list = list()

[before_interface_list.append(i) for i in parsed_before]
[before_description_list.append(parsed_after[i]['description']) for i in parsed_before]

[after_interface_list.append(i) for i in parsed_after]
[after_description_list.append(parsed_after[i]['description']) for i in parsed_after]

headers = ["interface", "description"]
table_before = [before_interface_list, before_description_list]
table_after= [after_interface_list, after_description_list]

res_table_before = tabulate.tabulate(np.array(table_before).transpose(), headers,tablefmt="grid")
res_table_after = tabulate.tabulate(np.array(table_after).transpose(), headers,tablefmt="grid")

## NOTE: テーブルの表示
print("==BEFORE==")
print(res_table_before)

print("==AFTER==")
print(res_table_after)

実行します

python configure.py 
==BEFORE==
+------------------+---------------------------------------+
| interface        | description                           |
+==================+=======================================+
| GigabitEthernet1 | MANAGEMENT INTERFACE - DON'T TOUCH ME |
+------------------+---------------------------------------+
| GigabitEthernet2 | Configured by RESTCONF                |
+------------------+---------------------------------------+
| GigabitEthernet3 | Network Interface                     |
+------------------+---------------------------------------+
| Loopback1        | This is a test                        |
+------------------+---------------------------------------+
==AFTER==
+------------------+---------------------------------------+
| interface        | description                           |
+==================+=======================================+
| GigabitEthernet1 | MANAGEMENT INTERFACE - DON'T TOUCH ME |
+------------------+---------------------------------------+
| GigabitEthernet2 | Configured by RESTCONF                |
+------------------+---------------------------------------+
| GigabitEthernet3 | Network Interface                     |
+------------------+---------------------------------------+
| Loopback1        | This is a test                        |
+------------------+---------------------------------------+
| Loopback100      | configured by scrapli                 |
+------------------+---------------------------------------+

変更前と変更後のテーブルを表示してみました、Loopback100が追加されたことも確認できます。

Netconf

最後にScrapliの特徴の一つでもあるNetconfにドライバーも試してみます。

事前にscrapli-netconfをインストールしておきます。

pip install scrapli-netconf

スクリプト作成

from scrapli_netconf.driver import NetconfDriver

device = {
   "host": "sandbox-iosxe-latest-1.cisco.com",
   "auth_username": "developer",
   "auth_password": "XXXXXXXXX",
   "auth_strict_key": False,
   "port": 830,
}

INTERFACE_FILTER = """
          <if:interfaces xmlns:if="urn:ietf:params:xml:ns:yang:ietf-interfaces">
          </if:interfaces>
          """

with NetconfDriver(**device) as nc_conn:
  response= nc_conn.get(filter_=INTERFACE_FILTER)

print(response.result)

Interfaeの情報を取得します。

実行

python netconf_get.py 
<rpc-reply xmlns="urn:ietf:params:xml:ns:netconf:base:1.0" message-id="101">
  <data>
    <interfaces xmlns="urn:ietf:params:xml:ns:yang:ietf-interfaces">
      <interface>
        <name>GigabitEthernet1</name>
        <description>MANAGEMENT INTERFACE - DON'T TOUCH ME</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>10.10.20.48</ip>
            <netmask>255.255.255.0</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>GigabitEthernet2</name>
        <description>Configured by RESTCONF</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>192.168.1.1</ip>
            <netmask>255.255.255.252</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>GigabitEthernet3</name>
        <description>Network Interface</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:ethernetCsmacd</type>
        <enabled>false</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback1</name>
        <description>This is a test</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>56.56.56.56</ip>
            <netmask>255.255.255.255</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback100</name>
        <description>configured by scrapli</description>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
      <interface>
        <name>Loopback101</name>
        <type xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type">ianaift:softwareLoopback</type>
        <enabled>true</enabled>
        <ipv4 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip">
          <address>
            <ip>101.101.101.101</ip>
            <netmask>255.255.255.255</netmask>
          </address>
        </ipv4>
        <ipv6 xmlns="urn:ietf:params:xml:ns:yang:ietf-ip"/>
      </interface>
    </interfaces>
  </data>
</rpc-reply>

情報取得できることを確認できました。

設定変更(edit)やコンフィグ取得(get_config)などのメソッドも用意されています。

まとめ

Scrapliに入門してみました。Netmikoを触ったことがあったので抵抗なく始めることができました、それくらい似ていると思います。
DriverにはAsync用のものもあり非同期処理にも対応しているようです。 標準ライブラリconcurrent.futuresを使って平行処理も実装してみましたが、軽量でサクサク動くので便利です。

興味があればぜひ試してみてください。

Golangで書かれたScrapligoもあるようです。 github.com