Ansibleのネットワークモジュールに「設定したくないコマンド」を指定するオプションを追加してみた

# はじめに
先日、セイコーソリューションズ株式会社が提供するコンソールサーバー「SmartCS」で、Ansibleの他社ベンダー用モジュールが実行可能になったとのニュースリリースがありました。
[コンソールサーバーSmartCSがITインフラの運用自動化対応を強化](https://www.seiko-sol.co.jp/archives/46086/)

これにより、例えばCisco IOS機器の初期設定作業を、`ios_config`や`ios_command`を使ってできるようになると思います。
ただ、注意が必要なのは、機器故障等で前回取得したConfigファイルを使って初期設定を行う場合、冒頭にはじかれるコマンドがあることです。Ansibleでは以下3つのコマンドをデフォルトで無視し、設定対象から除外しているようです。
[GitHub - ansible/lib/ansible/module_utils/network/common/config.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/module_utils/network/common/config.py)
![無題1101_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/310082/c405598f-475e-fe59-ea28-970d200c8179.png)

機種や設定によっては、これ以外にもはじかれるコマンドがあります。そのため、お試しで`ios_config`モジュールに`ignore_commands`オプションを追加し、指定した文字列を含むコマンドを、設定対象外にできるようにしてみました。


# 既存コードの変更

ローカル環境の以下ファイルを2箇所変更しています。
[GitHub - ansible/lib/ansible/modules/network/ios/ios_config.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/network/ios/ios_config.py)

1. 冒頭のargument_specにignore_commandsを定義
2. コメント「[新規追加] ignore unnecessary commands」で、無視するコマンド(ignore_commands)が設定コマンド(commands)の中に含まれる場合、リストから除外


```python:ios_config.py抜粋
def main():
    """ main entry point for module execution
    """
    # ～ 省略 ～
    argument_spec = dict(
        # ～ 省略 ～
        ignore_commands=dict(type='list'),   # [新規追加]
    )
    # ～ 省略 ～
    if any((module.params['lines'], module.params['src'])):
        match = module.params['match']
        replace = module.params['replace']
        path = module.params['parents']

        candidate = get_candidate_config(module)
        running = get_running_config(module, contents, flags=flags)
        try:
            response = connection.get_diff(candidate=candidate, running=running, diff_match=match, diff_ignore_lines=diff_ignore_lines, path=path,
                                           diff_replace=replace)
        except ConnectionError as exc:
            module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))

        config_diff = response['config_diff']
        banner_diff = response['banner_diff']

        if config_diff or banner_diff:
            commands = config_diff.split('\n')

            if module.params['before']:
                commands[:0] = module.params['before']

            if module.params['after']:
                commands.extend(module.params['after'])

            # [新規追加] ignore unnecessary commands -START-
            if module.params["ignore_commands"]:
                import re

                ignore_commands = module.params["ignore_commands"]
                commands_tmp = []

                for cmd in commands:
                    for ignore in ignore_commands:
                        if re.search(ignore, cmd):
                            break
                    else:
                        commands_tmp.append(cmd)

                commands = commands_tmp
            # -END-

            result['commands'] = commands
            result['updates'] = commands
            result['banners'] = banner_diff

            # send the configuration commands to the device and merge
            # them with the current running config
            if not module.check_mode:
                if commands:
                    edit_config_or_macro(connection, commands)
                if banner_diff:
                    connection.edit_banner(candidate=json.dumps(banner_diff), multiline_delimiter=module.params['multiline_delimiter'])

            result['changed'] = True
```

# 設定Configファイル
簡単のため、Config一式でなく、冒頭のコマンドとACL設定のみ投入します。
冒頭の3行はデフォルトで無視されるコマンドです。続く2行を設定対象から除外し、最後にACL2行を追加してみます。

```text:test3_config.txt
Using 845634 out of 33645387 bytes
Building configuration...
Current configuration : 1047 bytes

Load for five secs: 1%/0%; one minute: 0%; five minutes: 0%
Time source is NTP, 10:45:34.567 JST Sat Jan 19 2019

ip access-list extended test
 permit ip host 192.168.1.1 host 10.1.1.1
 permit ip host 192.168.1.2 host 10.1.1.2
```

# Playbook
1つ目のタスクで、`ios_config`の`src`オプションで上記のConfigファイルを指定し、続く`ignore_commands`オプションで「Load for five secs:」もしくは「Time source is NTP,」を除外対象にしています。
2つめと3つ目のタスクで、目的のACLが問題なく投入されたかを確認します。

```playbook1_config.yml
---

- hosts: cisco
  gather_facts: no
  connection: network_cli

  tasks:
    - name: change configuration from config file
      ios_config:
        src: test3_config.txt
        ignore_commands:
          - "Load for five secs:"
          - "Time source is NTP,"
        save_when: modified

    - name: run show command
      ios_command:
        commands: show running-config | section ip access-list extended test
      register: result

    - name: display ACLs
      debug:
        msg: "{{ result.stdout_lines[0] }}"
```

# 実行結果①（ignore_commandsオプション無しの場合）
想定通り、コマンド「Load for five secs:～」ではじかれFailとなっています。

```
$ ansible-playbook -i inventory playbook1_config.yml

PLAY [cisco] ******************************************************************************************************************************************************************************************************

TASK [change configuration from config file] **********************************************************************************************************************************************************************
An exception occurred during task execution. To see the full traceback, use -vvv. The error was: test3(config)#
fatal: [test3]: ( ～省略～ ) line 186, in __rpc__\nansible.module_utils.connection.ConnectionError: Load for five secs: 1%/0%; one minute: 0%; five minutes: 0%\r\nLoad for five secs: 1%/0%; one minute: 0%; five minutes: 0%\r\n     ^\r\n% Invalid input detected at '^' marker.\r\n\r\ntest3(config)#\n", "module_stdout": "", "msg": "MODULE FAILURE\nSee stdout/stderr for the exact error", "rc": 1}

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

# 実行結果②（ignore_commandsオプション有りの場合）
問題なくACLが設定されました！:relaxed:

```
$ ansible-playbook -i inventory playbook1_config.yml

PLAY [cisco] ******************************************************************************************************************************************************************************************************

TASK [change configuration from config file] **********************************************************************************************************************************************************************
changed: [test3]

TASK [run show command] *******************************************************************************************************************************************************************************************
ok: [test3]

TASK [display ACLs] ***********************************************************************************************************************************************************************************************
ok: [test3] => {
    "msg": [
        "ip access-list extended test",
        " permit ip host 192.168.1.1 host 10.1.1.1",
        " permit ip host 192.168.1.2 host 10.1.1.2"
    ]
}

PLAY RECAP ********************************************************************************************************************************************************************************************************
test3                      : ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
```

# 最後に
設定前にConfigファイルを編集すればいいじゃん！と思った方、その通りです。でも、何でも自動化してみたいお年頃なので、お許しください:relaxed: