NetmikoでヤマハのNW機器を操作する

# はじめに
Netmikoとは、ネットワーク機器へのログイン、ログ取得、設定変更等を行うための、マルチベンダー対応のPythonライブラリです。
今回は、ヤマハのルータ（RTX、NVR）やファイアウォール（FWX）のプロンプト方式に対応したNetmikoクラスを作成してみました。
[GitHub - netmiko](https://github.com/ktbyers/netmiko)

作成にあたり、公式ドキュメントに加え、以下記事も参考にさせて頂きました。ありがとうございますm(_ _)m

[netmiko と TextFSM を利用してネットワーク機器の show コマンド結果をパースする](https://tekunabe.hatenablog.jp/entry/2018/12/31/netmiko_textfsm)
[netmikoでネットワーク機器の種類を自動検出してコマンドを実行する](https://qiita.com/akira6592/items/feff470e0093d0b7ca43)
[netmikoをYamahaに対応させてみた](https://qiita.com/sat0ken/items/7c341673dd7911680440)

以降で、実装時に考慮した点と、簡単なサンプルコードを書き残しておきます。

# 1. 考慮した点

## 1-1. コマンドモード
Netmikoのドキュメントを見ると、新規ベンダーを追加する時は「可能な限り、既存のCiscoSSHConnectionやBaseConnectionから継承したメソッドを再利用するべき。」と記載されています。
[Steps for adding a new vendor](https://ktbyers.github.io/netmiko/VENDOR.html)

Cisco機器の設定変更では、下表の通り`enable()`メソッドで特権EXECモードへ移行した後、`send_config_set()`、`send_config_from_file()`メソッドでグローバルコンフィグレーションモードへの移行と設定変更を行います。
一方Yamaha機器の設定変更では、`enable()`メソッドで管理モードへの移行、`send_config_set()`、`send_config_from_file()`メソッドで管理モードへの移行と設定変更を行います。
`enable()`メソッドは他ベンダー機器との統一性を図るために実装していますが、設定変更時の記述は必須ではないです。

| Ciscoモード名      | プロンプト | Yamahaモード名       | プロンプト | モード移行のためのメソッド |
|:-----------------|:------------------:|:-----------------|:------------------:|:------------------|
| ユーザEXEC | > | 一般 | > | メソッド不要  |
| 特権EXEC ※  | # | 管理 ※ | # | enable() |
| グローバルコンフィグレーション | (config)# | 管理 ※ | # | send_config_set()、send_config_from_file() |
※モードに入る前に、enable or 管理パスワード入力が必要。


## 1-2. Config保存
管理モードで設定変更を行った後、exit（もしくはquit）で一般モードに移行する際、以下の通りConfigを保存するか聞かれます。勝手に保存されると困るケースもあると思うので、「Y/Nという文字列があったらNを返す」ようにして、一律保存しないようにしています。
保存したい場合は、設定Config内に`save`コマンドを含めるか、Netmikoの`save_config()`メソッドを使うようにします。

```
yamaha1# ip lan1 address 10.1.1.100/24 
yamaha1# exit 
Save new configuration ? (Y/N)
```

# 2. 環境準備
テストを実施するにあたり、RTX側で以下設定を行っています。

- ユーザパスワード、管理パスワードの設定
- コンソールの文字コードをSJISからASCIIに変更
- Telnet、SSHの有効化

```:config(snippet)
yamaha1> show config
# RTX810 Rev.11.01.33 (Fri Apr 20 08:44:58 2018)
administrator password *
login user yamaha *
timezone +09:00
console character ascii
console lines infinity
console prompt yamaha1
ip route default gateway 192.168.100.1
ip lan1 address 10.1.1.40/24
ip lan2 address 192.168.100.100/24
telnetd service on
sshd service on
sshd host key generate *
```
# 3. サンプルコード

## 3-1. SSHログイン、設定変更、ログ取得、Config保存
RTX810にSSHログインし、LAN1のIPアドレスを変更してみます。確認のため、事前事後で`show status lan1`コマンドを取得し、アドレスを確認します。最後にConfig保存も行います。

```python:send_config.py
from netmiko import ConnectHandler

device = {
    "host": "192.168.100.100",
    "username": "yamaha",
    "password": "yamaha",
    "secret": "yamaha",   # administrator password
    "device_type": "yamaha",   # device type for yamaha SSH login
}

# connect
net_connect = ConnectHandler(**device)

# send show command (before)
output = net_connect.send_command("show status lan1")
print('---------------before log---------------', output, sep='\n')

# change ip address
config_commands = ['ip lan1 address 10.1.1.100/24']
output = net_connect.send_config_set(config_commands)
print('---------------change config---------------', output, sep='\n')

# send show command (after)
output = net_connect.send_command("show status lan1")
print('---------------after log---------------', output, sep='\n')

# save config
output = net_connect.save_config()
print('---------------save config---------------', output, sep='\n')

# disconnect
net_connect.disconnect()
```

実行結果を見ると、事前事後でIPアドレスが10.1.1.40/24から10.1.1.100/24に変更されているのが分かります。また、設定変更後、問題なくNが入力され、一般モードに移行できているのが分かります。Configも問題なくconfig0に保存されています。

```:result3-1
$ python send_config_telnet.py
---------------before log---------------
LAN1
Description:                    
IP Address:                     10.1.1.40/24 
Ethernet Address:               00:a0:de:XX:XX:XX
Operation mode setting:         Type (Link status)
               PORT1:           Auto Negotiation (Link Down)
               PORT2:           Auto Negotiation (Link Down)
               PORT3:           Auto Negotiation (Link Down)
               PORT4:           Auto Negotiation (Link Down)
Maximum Transmission Unit(MTU): 1500 octets
Promiscuous mode:               OFF
Transmitted:                    0 packet (0 octet)
  IPv4(all/fastpath):           0 packet / 0 packet
  IPv6(all/fastpath):           0 packet / 0 packet
Received:                       0 packet (0 octet)
  IPv4:                         0 packet
  IPv6:                         0 packet
---------------change config---------------
administrator 
Password: 
yamaha1# ip lan1 address 10.1.1.100/24 
yamaha1# exit 
Save new configuration ? (Y/N)N
yamaha1> 
yamaha1> 
yamaha1> 
---------------after log---------------
LAN1
Description:                    
IP Address:                     10.1.1.100/24 
Ethernet Address:               00:a0:de:XX:XX:XX
Operation mode setting:         Type (Link status)
               PORT1:           Auto Negotiation (Link Down)
               PORT2:           Auto Negotiation (Link Down)
               PORT3:           Auto Negotiation (Link Down)
               PORT4:           Auto Negotiation (Link Down)
Maximum Transmission Unit(MTU): 1500 octets
Promiscuous mode:               OFF
Transmitted:                    0 packet (0 octet)
  IPv4(all/fastpath):           0 packet / 0 packet
  IPv6(all/fastpath):           0 packet / 0 packet
Received:                       0 packet (0 octet)
  IPv4:                         0 packet
  IPv6:                         0 packet
---------------save config---------------
Saving ... CONFIG0 Done .
```

## 3-2. Telnetログイン、設定変更(Configファイル)、ログ取得
先ほどはSSHログインでしたが、device_typeを`yamaha_telnet`とすることで、Telnetログインも可能です。
また、先ほどは`send_config_set()`メソッドで、リスト形式のデータを読み込んで設定変更しましたが、`send_config_from_file()`でConfigファイルを流し込むことも可能です。今回は、以下のyamaha_config.txtでスタティックルート2行の追加とConfig保存(save)を行い、事前事後でConfig確認を行います。

```:yamaha_config.txt
ip route 10.1.2.0/24 gateway 10.1.1.1
ip route 10.1.3.0/24 gateway 10.1.1.1
save
```

```python:send_config_file.py
from netmiko import ConnectHandler

device = {
    "host": "192.168.100.100",
    "username": "yamaha",
    "password": "yamaha",
    "secret": "yamaha",
    "device_type": "yamaha_telnet",   # device type for yamaha telnet login
}

# connect
net_connect = ConnectHandler(**device)

# send show command (before)
output = net_connect.send_command("show config | grep route")
print('---------------before log---------------', output, sep='\n')

# add static routes
output = net_connect.send_config_from_file(config_file='yamaha_config.txt')
print('---------------change config---------------', output, sep='\n')

# send show command (after)
output = net_connect.send_command("show config | grep route")
print('---------------after log---------------', output, sep='\n')

# disconnect
net_connect.disconnect()
```

実行結果を見ると、スタティックルートが問題なく追加されています。事前にConfig保存を行ったため、管理モードから抜ける時にY/Nを聞かれませんでしたが、後続処理もできています。

```:result3-2
$ python send_config_file.py 
---------------before log---------------
Searching ...
ip route default gateway 192.168.100.1
---------------change config---------------
administrator 
Password: 
yamaha1# ip route 10.1.2.0/24 gateway 10.1.1.1 
yamaha1# ip route 10.1.3.0/24 gateway 10.1.1.1 
yamaha1# save 
Saving ... CONFIG0 Done .
yamaha1# 
yamaha1# exit 
yamaha1> 
yamaha1> 
yamaha1> 
yamaha1> 
yamaha1> 
---------------after log---------------
Searching ...
ip route default gateway 192.168.100.1
ip route 10.1.2.0/24 gateway 10.1.1.1
ip route 10.1.3.0/24 gateway 10.1.1.1
```

##3-3. 自動検出、TextFSMによるパース
最後に、Netmikoの自動検出機能を使ったdevice_typeの判定を行います。判定方法は、`show copyright`コマンドを取得し、返ってきた値の中に「Yamaha Corporation」が含まれる場合、device_typeを「yamaha」としています。
合わせて、`show environment`コマンドのTextFSMによるパースも行います。出力結果をパースする場合、`send_command()`に引数`use_textfsm=True`を追加します。(TextFSMのTemplateファイルは自作したものを用意。）

```python:auto_detect.py 
from netmiko.ssh_autodetect import SSHDetect
from netmiko import ConnectHandler
from pprint import pprint

device = {
    "host": "192.168.100.100",
    "username": "yamaha",
    "password": "yamaha",
    "secret": "yamaha",
    "device_type": "autodetect",  # set device type to "autodetect"
}

# autodetect device_type
guesser = SSHDetect(**device)
best_match = guesser.autodetect()

# display detected device_type
print("device_type: " + best_match)

# set device_type and connect
device['device_type'] = best_match
connection = ConnectHandler(**device)

# send show command and display output
output = connection.send_command('show environment')
print('---------------show environment---------------', output, sep='\n')

# send show command and display parsed output with textfsm
output = connection.send_command('show environment', use_textfsm=True)
print('---------------show environment (parsed)---------------')
pprint(output)

# disconnect
connection.disconnect()
```

自動検出機能により、「yamaha」と判定されています。パースも問題なくできました。

```result3-3
$ python auto_detect.py 
device_type: yamaha
---------------show environment---------------
RTX810 BootROM Ver. 1.00
RTX810 FlashROM Table Ver. 1.00
RTX810 Rev.11.01.33 (Fri Apr 20 08:44:58 2018)
  main:  RTX810 ver=00 serial=S3KXXXXXX MAC-Address=00:a0:de:00:00:00 MAC-Addre
ss=00:a0:de:aa:aa:aa
CPU:   8%(5sec)   1%(1min)   0%(5min)    Memory: 7% used
Packet-buffer:   0%(small)   0%(middle)   5%(large)   0%(huge) used
Firmware: internal  
Config. file: config0  Default config. file: config0
Boot time: 2019/10/01 22:00:17 +09:00
Current time: 2019/10/05 21:06:54 +09:00
Elapsed time from boot: 3days 23:06:37
Security Class: 1, FORGET: ON, TELNET: OFF
---------------show environment (parsed)---------------
[{'buffer_huge': '0',
  'buffer_large': '5',
  'buffer_middle': '0',
  'buffer_small': '0',
  'cpu_1_min': '1',
  'cpu_5_min': '0',
  'cpu_5_sec': '1',
  'date': '2019/10/05',
  'forget': 'ON',
  'hardware': 'RTX810',
  'mac': ['00:a0:de:00:00:00', '00:a0:de:aa:aa:aa'],
  'mem': '7',
  'sec_class': '1',
  'serial': 'S3KXXXXXX',
  'telnet': 'OFF',
  'temperature': '',
  'time': '21:06:55',
  'timezone': '+09:00',
  'uptime': '3days 23:06:38',
  'version': '11.01.33'}]
```

# 最後に
今回開発したものは、Netmikoにプルリクエストを出しマージされています。(ただし、Telnetログインの不具合があり、2021/1/1に再度プルリクエストを出しています。)
TextFSMのTemplateファイルは、後日プルリクエストを出したいと思います。