Ansible自作モジュールを使ってターミナルサーバ経由でNW機器を操作してみた

# はじめに

以前の記事で、Pythonを使ったターミナルサーバ経由のNW機器操作例をご紹介しました。
[Pythonを使ってターミナルサーバ経由でNW機器を操作する方法](https://qiita.com/tech_kitara/items/b7319d53992e4adfd27d)

今回は、このPythonコードをベースに、同じ事ができるAnsibleモジュールを自作しました。


# console_commandモジュール
モジュール作成にあたり、[networktocode/ntc-ansible](https://github.com/networktocode/ntc-ansible)の`ntc_show_command`モジュールを参考にさせて頂きました。
実装機能は、複数のshowコマンドを実行し、標準出力するだけのシンプルなものです。
 

```console_command.py
import os.path
import socket
from netmiko import redispatch
import time

HAS_NETMIKO = True
try:
    from netmiko import ConnectHandler
except:
    HAS_NETMIKO = False


def main():

    module = AnsibleModule(
        argument_spec=dict(
            host=dict(required=False),
            username=dict(required=False, type='str'),
            password=dict(required=False, type='str', no_log=True),
            port=dict(required=False),
            secret=dict(required=False, type='str', no_log=True),
            platform=dict(required=False),
            commands=dict(type='list', required=True),   # 複数のshowコマンドを実行できるよう、データ型をリストに指定
            delay=dict(default=1, required=False),
            provider=dict(type='dict', required=False),
        ),
        supports_check_mode=False
    )

    provider = module.params['provider'] or {}

    no_log = ['password', 'secret']
    for param in no_log:
        if provider.get(param):
            module.no_log_values.update(return_values(provider[param]))

    # allow local params to override provider
    for param, pvalue in provider.items():
        if module.params.get(param) != False:
            module.params[param] = module.params.get(param) or pvalue

    # AnsibleのInventory/Playbook/Variableファイルで定義された値を取得し、変数に格納
    ip = module.params['host']
    username = module.params['username']
    password = module.params['password']
    port = module.params['port']
    secret = module.params['secret']
    device_type = module.params['platform']
    command = module.params['commands']
    delay = int(module.params['delay'])

    # netmikoがインストールされていない場合、Failメッセージを出力
    if not HAS_NETMIKO:
        module.fail_json(msg='This module requires netmiko.')
    else:
        # ここからは前回記事と同様
        device_args = dict(
            device_type = 'terminal_server',
            ip = ip,
            port = port,
            username = username,
            password = password,
            secret = secret,
          )

        net_connect = ConnectHandler(**device_args)
        net_connect.write_channel('\r')
        time.sleep(1)
        net_connect.write_channel('\r')
        time.sleep(1)
        net_connect.read_channel()

        redispatch(net_connect, device_type = device_type)

        if secret:
            net_connect.enable()

        output_list = []

        for cmd in command:
            output = net_connect.send_command_timing(cmd, delay_factor=delay)
            output_list.append(output)

        net_connect.disconnect()
        # ここまで

    # 実行結果をJSON形式で標準出力
    results = {}
    results['response_list'] = [output_list]

    module.exit_json(**results)


from ansible.module_utils.basic import *
if __name__ == "__main__":
    main()
```

自作モジュールをAnsibleが認識できるよう、[Best Practices > Directory Layout]
(https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html#directory-layout)に従って`library`ディレクトリを作成し、直下に`console_command.py`を格納しました。
また、`ansible.cfg`ファイルに`library`ディレクトリへのパスを追加しました。

```ini:ansible.cfg
[defaults]
library = /home/centos/ansible/library
```

`ansible.cfg`の格納先は、`ansible --version`コマンドの`config file`で確認可能です。

```ini:
[centos@localhost ~]$ ansible --version
ansible 2.7.9
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/home/centos/ansible/library']
  ansible python module location = /usr/lib/python3.6/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.6.7 (default, Dec  5 2018, 15:02:05) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]
```

pip（Pythonのパッケージ管理システム）を使ってansibleをインストールした場合は、`ansible.cfg`が作成されないため、新規作成する必要があるようです。

# Playbook
Ansible2.5以降は、CLIベースのNW機器を操作する際、コネクションプラグイン`network_cli`を使用するのが標準的ですが、今回はNW機器への接続・ログイン処理も自作したため、`local`を指定しています。
簡単のため、showコマンドの出力結果は`|`で限定しています。

```yaml:playbook_consv.yml
---

- hosts: all
  gather_facts: no
  connection: local
  
  tasks:
    - name: execute show command
      console_command:
        commands:
          - show version | inc Cisco
          - show running-config | section interface FastEthernet1 
        port: "{{ port }}"
        provider: "{{ cli }}"
      register: result

    - name: display the output of show command
      debug:
        var: result.response_list

  vars:
    cli:
      platform: "{{ platform }}"
      host: "{{ inventory_hostname }}"
      username: "{{ username }}"
      password: "{{ password }}"
      secret: "{{ secret | default(omit) }}"
```

# Inventoryファイル
今回はCisco 1812J×1台を対象機器としています。

```yaml:inventory_consv2
[all] 
# ターミナルサーバのIPアドレス　SSHログイン用のポート番号　NW機器のenableパスワード
192.168.100.56 port=3001 secret=test

[all:vars]
platform=cisco_ios   # NW機器のOS名(=netmikoのdevice_type)
username=admin   # ターミナルサーバのユーザ名
password=password   # ターミナルサーバのパスワード
```

# 実行結果
`"result.response_list": `を見ると、2つのshowコマンド結果が問題なく表示されている事が分かります。
(ただ、改行はうまくできませんでした。。)

```yaml:
[centos@localhost ansible]$ ansible-playbook -i inventory_consv2 playbook_consv.yml

PLAY [all] ******************************************************************************

TASK [execute show command] *************************************************************
ok: [192.168.100.56]

TASK [display the output of show command] ***********************************************
ok: [192.168.100.56] => {
    "result.response_list": [
        [
            "Cisco IOS Software, C181X Software (C181X-ADVIPSERVICESK9-M), Version XX.X(X)XX, RELEASE SOFTWARE (fc5)\nCopyright (c) 1986-2007 by Cisco Systems, Inc.\nuse. Delivery of Cisco cryptographic products does not imply\nA summary of U.S. laws governing Cisco cryptographic products may be found at:\nCisco 1812-J (MPC8500) processor (revision 0x400) with 118784K/12288K bytes of memory.",
            "interface FastEthernet1\n description << border2 - dist1 Segment >>\n ip address 192.168.200.9 255.255.255.252\n speed 100\n full-duplex"
        ]
    ]
}

PLAY RECAP ******************************************************************************
192.168.100.56             : ok=2    changed=0    unreachable=0    failed=0   
```

# 最後に
今回は最低限の機能のみ実装しましたが、参考にした`ntc_show_command`モジュールには、textFSMによるパース機能や、出力結果のファイル出力機能もありますので、併せて取り込めば、かなり使えるモジュールになると思います。