ntc-ansibleでヤマハのNW機器を操作する

# はじめに

前回の記事で、Netmikoからヤマハ機器のログイン、ログ取得、設定変更等を行いました。
[NetmikoでヤマハのNW機器を操作する](https://qiita.com/tech_kitara/items/b2496f74c5a4e0aab174)

Netmikoをベースとしたサードパーティ製のAnsibleモジュール群として、ntc-ansibleがあります。
[GitHub - networktocode/ntc-ansible](https://github.com/networktocode/ntc-ansible)
※セットアップ方法や基本操作方法は、以下記事を参照頂ければと思います。
[Ansibleのntc-ansibleモジュール群まとめ (①概要、セットアップ編)](https://qiita.com/tech_kitara/items/422f34e1f327b0f8b7f8)
[Ansibleのntc-ansibleモジュール群まとめ (②確認コマンドモジュール実行編)](https://qiita.com/tech_kitara/items/748701d243b0181e462b)
[Ansibleのntc-ansibleモジュール群まとめ (③設定変更モジュール実行編)](https://qiita.com/tech_kitara/items/d0e39a84a5e96391f605)

今回は、ntc-ansibleの以下モジュールを使って、ヤマハ機器を操作できるか確認してみました。
確認バージョンは、ntc-ansible=0.1.0、Ansible=2.8.4、Netmiko=2.4.2です。

| No. | モジュール名       | 内容              |
|:----|:-----------------|:------------------|
|  1  | ntc_show_command | CLIベースのNW機器から構造化されたデータを取得。（showコマンド実行など） |
|  2  | ntc_config_command | CLIベースのNW機器へコマンドを送信。（設定変更など） |

（注意！）ntc-ansibleは、公式のネットワークモジュールで使われる`network_cli`のようなconnectionプラグインをサポートしておらず、`local`を使用します。また、冪等性も担保されません。


# ntc-ansible 既存コードの修正

そのままでは動かなかったり、結果が見づらかったりしたので、各モジュールのPythonファイルを一部修正しました。

```python:ntc_show_command.py
# (省略)
def main():
# (省略)
#    cisco_ios以外のTelnetログインを受け付けない実装になっているので、コメントアウトで無効化。
#    if connection in ['netmiko_telnet', 'telnet'] and platform != 'cisco_ios':
#        module.fail_json(msg='only cisco_ios supports '
#                             'telnet/netmiko_telnet connection')

    if platform == 'cisco_ios' and connection in ['netmiko_telnet', 'telnet']:
        device_type = 'cisco_ios_telnet'

    if module.params['port']:
        port = int(module.params['port'])
    else:
#        if device_type == 'cisco_ios_telnet':   # device_typeの代わりにconnectionで使用ポートを判定。
        if connection == 'telnet':   # modify
            port = 23
        else:

            port = 22
# (省略)
    results = {}
    results['response'] = []
    results['response_list'] = []

    if use_templates:
        if rawtxt:
            results['response'] = parse_raw_output(rawtxt, module)
        elif trigger_device_list:
            results['response_list'] = parse_raw_output(commando.results, module)
    elif rawtxt:
        results['response'] = [rawtxt]
        results['response_list'] = rawtxt.split('\n')   # 出力結果をリスト形式で表示できるように修正。
    elif trigger_device_list:
        results['response'] = [commando.results]

    module.exit_json(**results)
# (省略)
```

```python:ntc_config_command.py
# (省略)
def main():
# (省略)
#    cisco_ios以外のTelnetログインを受け付けない実装になっているので、コメントアウトで無効化。
#    if connection == 'telnet' and platform != 'cisco_ios':
#        module.fail_json(msg='only cisco_ios supports '
#                             'telnet connection')
# (省略)
```

# Inventory
platformは、基本的にNetmikoのdevice_typeと紐づいています。そのため、ログイン方式がSSHの場合は`yamaha`、Telnetの場合は`yamaha_telnet`を指定します。Ansibleの公式モジュールで使われる、network_os、ansible_host、ansible_become_method等はサポートされていませんので注意が必要です。

```:inventory
[yamaha]
192.168.100.88 hostname=yamaha1 lan1_address=10.1.1.24/24

[yamaha:vars]
# platform=yamaha   # SSHログインの場合
platform=yamaha_telnet   # Telnetログインの場合
ansible_user=yamaha
ansible_password=yamaha
ansible_become_pass=yamaha
ansible_python_interpreter=/home/centos/venv_an2.8.4/bin/python3.6
```

# Playbook
今回は、Telnetを使ってIPアドレスの変更、showコマンドの取得、ログファイルの保管、パースしたshowコマンドの取得を行っています。SSHを使う場合、connectionオプションの指定は不要です。connectionプラグインはlocalであり、タスク毎のログイン情報の指定が必要です。

```yaml:playbook_yamaha1.yml
---

- hosts: yamaha
  gather_facts: no
  connection: local

  tasks:
    - name: change configuration on remote devices
      ntc_config_command:
        connection: telnet   # 今回はログイン方式としてtelnetを指定。デフォルト値はssh
        commands:
          - ip lan1 address "{{ lan1_address }}"
          - save
        provider: "{{ cli }}"
        platform: "{{ platform }}"

    - name: run show command and store config file
      ntc_show_command:
        connection: telnet
        command: show status lan1
        use_templates: false   # パース用のTextFSM templateの使用有無を指定。デフォルト値はtrue
        provider: "{{ cli }}"
        local_file: "./{{ hostname }}_log.txt"   # 保存先のディレクトリ・ファイル名を指定
      register: result

    - name: display show command
      debug:
        var: result.response_list

    - name: run show command and get parsed data
      ntc_show_command:
        connection: telnet
        command: show environment
        template_dir: "/home/centos/ntc-templates/templates"   # Specifies where to search templates
        provider: "{{ cli }}"
      register: result2

    - name: display parsed show command
      debug:
        var: result2.response

  vars:
    cli:
      host: "{{ inventory_hostname }}"
      username: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      secret: "{{ ansible_become_pass }}"
      platform: "{{ platform }}"   # 対象機器のOSに合わせて指定する必要あり
```

# 実行結果
[display show command]を見ると、IPアドレス10.1.1.24/24が問題なく設定されています。[display parsed show command]で、show environmentのパースも出来ました。記載は省略しますがファイル保存もOKでした。ただ、2つの処理でWARNINGが出ています。

```:実行結果
$ ansible-playbook -i inventory playbook_yamaha1.yml
 [WARNING]: Skipping plugin (/home/centos/venv_an2.8.4/ntc-ansible/filter_plugins/split.py) as it seems to be invalid: invalid
syntax (split.py, line 7)


PLAY [yamaha] ***********************************************************************************************************************

TASK [change configuration on remote devices] ***************************************************************************************
changed: [192.168.100.88]

TASK [run show command and store config file] ***************************************************************************************
 [WARNING]: The value 1 (type int) in a string field was converted to '1' (type string). If this does not look like what you expect,
quote the entire value to ensure it does not change.

ok: [192.168.100.88]

TASK [display show command] *********************************************************************************************************
ok: [192.168.100.88] => {
    "result.response_list": [
        "LAN1",
        "Description:                    ",
        "IP Address:                     10.1.1.24/24 ",
        "Ethernet Address:               00:a0:de:00:00:00",
        "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"
    ]
}

TASK [run show command and get parsed data] *****************************************************************************************
ok: [192.168.100.88]

TASK [display parsed show command] **************************************************************************************************
ok: [192.168.100.88] => {
    "result2.response": [
        {
            "buffer_huge": "0",
            "buffer_large": "5",
            "buffer_middle": "0",
            "buffer_small": "0",
            "cpu_1_min": "1",
            "cpu_5_min": "0",
            "cpu_5_sec": "0",
            "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": "23:43:53",
            "timezone": "+09:00",
            "uptime": "4days 01:43:36",
            "version": "11.01.33"
        }
    ]
}

PLAY RECAP **************************************************************************************************************************
192.168.100.88             : ok=5    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 
```

# 最後に
一応、ヤマハ機器でも動くことが確認できました。冪等性やログイン情報の指定など、公式のモジュールと比べてデメリットはありますが、公式が未サポートのベンダーをAnsibleに取り込める点や、止むを得ずTelnetを使う必要がある場面では有用ではないでしょうか。