AnsibleでCisco IOSに対して冪等性のある「no shutdown」をする


# 要約
ログ類が少し長いので最初にまとめです。

- ios_configモジュールでは、show run all にしか表示されないコンフィグは何回実行しても「changed=1」になってしまう
- 実行前のshow run と linesオプションに指定されたコンフィグを比較した差分を実行するため
- なので show run ではなく show run all を実行前の事前コンフィグとして扱わせる必要がある
- そのためには、ios_comamndモジュールであらかじめ show run all を取得する方法と、ios_templateモジュールに変更するという2つの対処がある
- 【2018/10/23追記】Ansible 2.2 以降では `defaults` オプションを `yes` にすることで対応可能（デフォルト `no`）

# 1. 「no shutdown」を何回実行しても「changed=1」になってしまう
見出しのような経験はないでしょうか。

インターフェースに対する「no shutdown」のように「show run」で表示されない
デフォルトコンフィグの類は、「ios_config」モジュールの処理の中で
常に差分コンフィグとして扱われます。

そのため、タスク実行のたびに「no shutdown」コマンド が実行され、
実質コンフィグには差分がないのに、Playbook実行結果は
「changed=1」（変更あり）になってしまいます。
冪等性がない、という言い方もできると思います。

ここでは Catalyst 3750のVLANインターフェースのデフォルトが
「no shutdown」であることを利用して、例を見てみます。
（環境 ansible2.1/Ubuntu 16.04）

# 2. 困る例
## 2.1. 1回目の「no shutdown」
最初は「shutdown」を意図的に入れている状態です。

```text:実行前
interface Vlan999
 no ip address
 shutdown
end
```

この状態から以下のPlaybookを実行します。

```yaml:cat_yaml.yml
- hosts: cisco   # イベントファイルで 192.168.1.1を定義済み。
  gather_facts: no
  connection: local

  tasks:
    - name: config
      ios_config:
        parents:
         - interface Vlan999      # 対象のインターフェース
        lines:
         - no shutdown            # 実行したいコンフィグ
        provider: "{{ cli }}"
      register: result

  vars:
    cli:   # 接続情報を辞書で定義（usernameとそれ以降はインベントリファイルから取得定義）
      host:     "{{ inventory_hostname }}"    # ホスト対象ホスト
      username: "{{ ansible_user }}"          # ログインユーザー
      password: "{{ ansible_password }}"      # ログインパスワード
      authorize: true                         # 特権モードに移行
      auth_pass: "{{ cisco_enable_secret }}"  # 特権パスワード
```

1回目のPlaybookを実行します。

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

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

TASK [config] ******************************************************************
changed: [192.168.1.1]

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

「shutdown」から「no shutdown」へ変更されたのでPlaybook実行結果は
「changed=1」となっています。ここまでは想定通りです。

スイッチ側は以下のコンフィグになりました。

```
interface Vlan999
 no ip address
end
```
「shutdown」が消えて、VLANインターフェースのデフォルト「no shutdown」
になったの分かります。

念のため「show run all」も確認しておきます。

```
interface Vlan999
 no ip address
 no shutdown
```

## 2.2. 2回目の「no shutdown」
もう一度同じPlaubookを実行します。

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

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

TASK [config] ******************************************************************
changed: [192.168.1.1]

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

```
状態に変わりがないはずなのに「changed=1」になってしまいました。
念のためスイッチ側のコンフィグを確認します。

```
interface Vlan999
 no ip address
end
```
やはり1回目の実行後の状態と変わりはありません。

これは、ios_config モジュールが「no shutodwn」のような「show run」結果に
表示されないコマンドを毎回差分コンフィグ（＝実行すべきコンフィグ）として
扱うためです。

これでは困るという場合の2つの対処例を試します。

# 3. 対処例その1：ios_commandで「show run all」取得処理を追加する

デフォルト値に戻ったコンフィグと、「ios_config」モジュールの「lines」オプション
で指定したコンフィグを比較させれば期待する結果になるので、
あらかじめ「ios_comamnd」モジュールで「show run all」の実行し、
結果を保存しておき、それを「ios_cofing」モジュールの「config」オプション
に指定してあげます。
このように「config」オプションで何かを指定すると、実行前の「show run」ではなく
「config」オプションで指定したものを事前コンフィグとして扱います。
参考：[ios.py L118付近](https://github.com/ansible/ansible/blob/stable-2.1/lib/ansible/module_utils/ios.py#L118)、[ios.py L161付近](https://github.com/ansible/ansible/blob/stable-2.1/lib/ansible/module_utils/ios.py#L161)

Playbookは以下のようになります。

```yaml:cat_yaml.yml
- hosts: cisco
  gather_facts: no
  connection: local

  tasks:
    - name: command
      ios_command:
        commands:
         - show running-config all       # show run allを実行する
        provider: "{{ cli }}"
      register: config                   # 変数configに show run all が入る

    - name: config
      ios_config:
        parents:
         - interface Vlan999
        lines:
         - no shutdown
        provider: "{{ cli }}"
        config: "{{ config.stdout[0] }}"   # show run allが展開される
      register: result

  vars:
    cli:
      host:     "{{ inventory_hostname }}"
      username: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      authorize: true
      auth_pass: "{{ cisco_enable_secret }}"

```

このPlaybookを実行してみます。

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

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

TASK [command] ******************************************************************
ok: [192.168.1.1]

TASK [config] *********************************************************************
ok: [192.168.1.1]

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

```
今度は「changed=0」になってくれました。


# 4. 対処例その2：ios_templateに変更する

「ios_template」モジュールは前述の「ios_config」モジュールと違って、
デフォルトで「show run all」を事前コンフィグとして扱います。
そのため、今回でいうと、事前コンフィグ「no shutdown」と
実行させたいコンフィグ「no shutdown」を比較し、その結果、
変更なし「changed=0」とすることができます。
例を見てみます。

## 4.1. テンプレートファイルの用意
「ios_template」モジュールではYAMLに実行させたいコンフィグを書くのではなく、
jinja2テンプレートファイルをあらかじめ用意しておき、
それを「ios_template」モジュール内の「src」オプションで指定します。

今回は以下のテンプレートファイルになります。

```text:int.j2
interface Vlan999
 no shutdown
```

## 4.2. Playbookの修正

対処例その1では、「ios_command」モジュールと「ios_cofing」モジュールを
組み合わせていましたが、対処例その2では「ios_templaete」モジュールだけで
よいので以下のように書き換えます。

```yaml:cat_yaml.yml
- hosts: cisco
  gather_facts: no
  connection: local

  tasks:
    - name: tempale              # ios_templateモジュールを使用
      ios_template:
        src: int.j2              # あらかじめ作成したテンプレートファイル
        provider: "{{ cli }}"
      register: result

  vars:
    cli:
      host:     "{{ inventory_hostname }}"
      username: "{{ ansible_user }}"
      password: "{{ ansible_password }}"
      authorize: true
      auth_pass: "{{ cisco_enable_secret }}"
```



## 4.3. Playbook実行

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

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

TASK [template]*****************************************************************
ok: [192.168.1.1]

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

「changed=0」になってくれました。


# 5. 参考資料

- 動画:[EXECUTING CONFIGURATION CHANGES ON NETWORK DEVICES](https://www.ansible.com/webinars-training/executing-configuration-changes-on-network-devices)
  - 公式の動画です。対処例1が16:00頃から、対処例その2が18:30頃から紹介されています。