Ansibleのios_configモジュールでinterfaceコマンドはparentsに指定するのが良さそう（検証付き）

# ●はじめに
Ansibleの Cisco IOS の設定変更に対応するモジュール「ios_config」にはコンフィグを指定するオプションが複数あります。
`lines`はメイン処理、`parents`は「interface Gi1/0/1」などの階層指定、`before`は事前処理、`after`は事後処理といった具合です。

このうち、`lines`と`parents`について
「`lines`にinterfaceコマンド指定しても特に困らなそうだけど、`parents`とどう使い分けたらよいのだろう」
という疑問があったため動作検証して挙動を探りました。環境はansible 2.1です。（一部はモジュールのソースも確認）

個人的な結論としては「interfaceコマンドのような階層指定ははフルスペルで`parents`に指定するのが良い」と思っています。

# ●パターン別検証

以下の条件を組み合わせて4パターン検証しました。

 - interfaceコマンドを`parents`に指定するか、`lines`に指定するか
 - interfaceコマンドを「interface」のようにフルスペルで指定するか、「int」のように省略するか

事前のコンフィグは以下の通りです。

```
interface GigabitEthernet1/0/1
 switchport access vlan 192
 switchport mode access
end
```
このインターフェースに対して`lines`でdescription を指定し、Playbookを2回実行して冪等性含めて検証します。


## 【パターン1】parents にフルスペルで指定する
### Playbookの内容
```yaml:pattern01.yml
---
- hosts: cisco    # インベントリファイルで 192.168.1.1（とりあえず1台）を[cisco]グループで定義済み
  gather_facts: no
  connection: local

  tasks:
    - name : set description
      ios_config:
        parents:    # parents にフルスペルで指定
          - int interface GigabitEthernet1/0/1
        lines:
          - description TESTTESTTEST
        provider: "{{ cli }}"
      notify: save config           # 変更があったら以下で定義するhandler「save config」を呼び出す
      register: result

    - name: DEBUG   # debug表示
      debug: var=result

  handlers:
    - name: save config             # コンフィグを保存する
      ios_command:
        commands:
          - write memory
        provider: "{{ cli }}"
      register: result

    - name: DEBUG   # debug表示
      debug: var=result

  vars:
    cli:   # 認証情報を定義しておく
      host:     "{{ inventory_hostname }}"
      username: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      authorize: true
      auth_pass: "{{ cisco_enable_secret }}"
```


### Plyaybooの実行(1回目)
``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
changed: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": true,
        "responses": [
            "",
            ""
        ],
        "updates": [
            "interface GigabitEthernet1/0/1",
            "description TESTTESTTEST"
        ]
    }
}

RUNNING HANDLER [save config] **************************************************
ok: [192.168.1.1]

PLAY RECAP *********************************************************************
192.168.1.1               : ok=3    changed=1    unreachable=0    failed=0
```

無事にdescriptionが設定されました。

```text:コンフィグ
interface GigabitEthernet1/0/1
 description TESTTESTTEST
 switchport access vlan 192
 switchport mode access
end
```

### Plyaybookの実行(2回目)

```
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
ok: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": false,
        "updates": []
    }
}

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

2貝目は変更なしとみなされ、descriptionコマンドは実行されなかったかたちとなります。
冪等性があるといえます。


## 【パターン2】parents に省略して指定する
### Playbookの内容
抜粋です。他はパターン1と同じです。

```yaml:pattern02.yml
      ios_config:
        parents:    # parents に省略して指定
          - int gi1/0/1
        lines:
          - description TESTTESTTEST
```


### Plyaybooの実行(1回目)

``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
changed: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": true,
        "responses": [
            "",
            ""
        ],
        "updates": [
            "int gi1/0/1",
            "description TESTTESTTEST"
        ]
    }
}

RUNNING HANDLER [save config] **************************************************
ok: [192.168.1.1]

PLAY RECAP *********************************************************************
192.168.1.1               : ok=3    changed=1    unreachable=0    failed=0
```

無事にdescriptionが設定されました。

```text:コンフィグ
interface GigabitEthernet1/0/1
 description TESTTESTTEST
 switchport access vlan 192
 switchport mode access
end
```


### Plyaybookの実行(2回目)

``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
changed: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": true,
        "responses": [
            "",
            ""
        ],
        "updates": [
            "int gi1/0/1",
            "description TESTTESTTEST"
        ]
    }
}

RUNNING HANDLER [save config] **************************************************
ok: [192.168.1.1]

PLAY RECAP *********************************************************************
192.168.1.1               : ok=3    changed=1    unreachable=0    failed=0
```

おっと、変更ありとみなされ、descpriptionコマンドがまた実行されてしまいました。
これは、`lines` に指定した「description TESTTESTTEST」を投入しようとして、
現状コンフィグの「description TESTTESTTEST」の親階層のコンフィグである
「interface GigabitEthernet1/0/1」が`parents`に指定した「gi1/0/1」と
文字列的に一致しないため、「実行すべきコマンド」と判断され

```
 configure terminal
 int gi1/0/1
 description TESTTESTTEST
```
が実行されたかたちとなります。冪等性がないといえます。


## 【パターン3】lines にフルスペル指定する




### Playbookの内容
抜粋です。他はパターン1と同じです。

```yaml:pattern03.yml
      ios_config:
        lines:
          - interface GigabitEthernet1/0/1
          - description TESTTESTTEST
```

### Plyaybooの実行(1回目)
``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
fatal: [192.168.1.1]: FAILED! => {"changed": false, "commands": ["configure terminal", "description TESTTESTTEST"], "failed": true, "msg": "matched error in response: description TESTTESTTEST\r\n                ^\r\n% Invalid input detected at '^' marker.\r\n\r\nSW104(config)#"}


PLAY RECAP *********************************************************************
192.168.1.1               : ok=0    changed=0    unreachable=0    failed=1
```

おっと、エラーになってしまい、description コマンドが実行されませんでした。

このパターンでは「interface GigabitEthernet1/0/1」を `lines`で指定しているため
現状コンフィグに「interface GigabitEthernet1/0/1」自身がある場合は省略されてしまいます。
その結果、debug結果にあるように、

```
 configure terminal
 description TESTTESTTEST
```
の2行のみを実行しようます。
グローバルコンフィグレーションモードに description コマンドはありませんので
エラーとなっています。

このパターンは使用できないことが分かりました。


## 【パターン4】lines に省略して指定する



### Playbookの内容
抜粋です。他はパターン1と同じです。

```yaml:pattern04.yml
      ios_config:
        lines:
          - int　gi1/0/1
          - description TESTTESTTEST
```


### Plyaybookの実行(1回目)
``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
changed: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": true,
        "responses": [
            "",
            ""
        ],
        "updates": [
            "int gi1/0/1",
            "description TESTTESTTEST"
        ]
    }
}

RUNNING HANDLER [save config] **************************************************
ok: [192.168.1.1]

PLAY RECAP *********************************************************************
192.168.1.1               : ok=3    changed=1    unreachable=0    failed=0
```

無事にdescriptionが設定されました。

```text:コンフィグ
interface GigabitEthernet1/0/1
 description TESTTESTTEST
 switchport access vlan 192
 switchport mode access
end
```


### Plyaybookの実行(2回目)
``` 実行結果
user@ubuntu-vm:/etc/ansible$ ansible-playbook cat_int.yml

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

TASK [set description] *********************************************************
changed: [192.168.1.1]

TASK [DEBUG] *******************************************************************
ok: [192.168.1.1] => {
    "result": {
        "changed": true,
        "responses": [
            "",
            ""
        ],
        "updates": [
            "int gi1/0/1",
            "description TESTTESTTEST"
        ]
    }
}

RUNNING HANDLER [save config] **************************************************
ok: [192.168.1.1]

PLAY RECAP *********************************************************************
192.168.1.1               : ok=3    changed=1    unreachable=0    failed=0
```

おっと、パターン2と同様に2回目も description コマンドが実行されてしまいました。
これは、`lines`で指定した「int gi1/0/1」を投入しようとして、
現状コンフィグの「interface GigabitEthernet1/0/1」と一致しないため、「実行すべきコマンド」と判断され

```
 configure terminal
 int gi1/0/1
 description TESTTESTTEST
```
が実行されたかたちとなります。冪等性がないといえます。


# ●まとめ
4パターンの検証結果から以下のことが分かりました。

| パターンNo. | 階層コンフィグ指定場所 | コマンド | 正常実行 | 冪等性 |
|:---:|:-:|:-:|:-:|:-:|
|【パターン1】| parents | フルスペル(interface)   | ○  | ○  |
|【パターン2】| parents | 省略(int)   | ○  | ×  |   |
|【パターン3】| lines | フルスペル(interface)   | ×  | -  |
|【パターン4】| lines | 省略(int)   |  ○ |  ×  |

これらの結果から、interfaceのような階層指定のコンフィグは
`parents`にフルスペルで指定するのが良い、という個人的なまとめとなりました。