Ansibleのplaybookのjunos commandモジュールで実行した結果に応じてtry-catch的に後続タスクを続けたい

# 0.はじめに
こんにちは。都内でエンジニアをしている、@gkzvoiceです。
今回は、AnsibleのplaybookでAnsibleっぽい？プログラマブルなエラーハンドリングを知ったので、早速アウトプットしていきます。

# 1.やりたかったこと
- 実行結果に応じてどのタスクを行うか決めたい
- 具体的には、直前に行われたタスクの成否問わず、必ず実行結果をslackに通知する
  - Ansibleでは直前のタスクが失敗した場合、後続タスクは実行されないですが、これは避けたい！！
- 直前のタスクが失敗した場合、実行結果を通知する際`@here` をつけて、`try-catch的にエラーキャッチ`したい

## こんなかんじ！
- 異常時のみ`@here`とグレーがかった`Failed`がついている
![show_ver.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/457f4dee-f528-e9c6-a93a-c962991dc685.png)




# 2. 環境/バージョン情報
- Ansible実行環境(ローカル)
  - ubuntu20.04
  - ansible 2.9.6
  - python 3.8.2　
- Ansibleでコマンドを流す対象機器
  - Junos 15.1X53-D63.9

※Juniper社のvlabsを使って検証した。
[Juniper Vlabsホームページ](https://jlabs.juniper.net/vlabs/) 

# 3. playbookで実行するコマンドを実行した様子とそのコマンドの成否判定

- コマンドの出力結果のサンプル

```
jcluser@Template-vQFX-Light> show version | no-more                             
fpc0:
--------------------------------------------------------------------------
Hostname: Template-vQFX-Light
Model: vqfx-10000
Junos: 15.1X53-D63.9
JUNOS Base OS boot [15.1X53-D63.9]
JUNOS Base OS Software Suite [15.1X53-D63.9]
JUNOS Online Documentation [15.1X53-D63.9]
JUNOS Crypto Software Suite [15.1X53-D63.9]
JUNOS Packet Forwarding Engine Support (qfx-10-f) [15.1X53-D63.9]
JUNOS Kernel Software Suite [15.1X53-D63.9]
JUNOS Web Management [15.1X53-D63.9]
JUNOS Enterprise Software Suite [15.1X53-D63.9]
JUNOS SDN Software Suite [15.1X53-D63.9]
JUNOS Routing Software Suite [15.1X53-D63.9]
JUNOS py-base-i386 [15.1X53-D63.9]

{master:0}
jcluser@Template-vQFX-Light> 
```

- コマンドの成否判定
  - `"Junos: 15.1|Junos: 15.2 "`であれば成功
  - 異なる場合、失敗とする

```
jcluser@Template-vQFX-Light> show version | match "Junos: 15.1|Junos: 15.2 "    
Junos: 15.1X53-D63.9

{master:0}
jcluser@Template-vQFX-Light> 
```

# 4.学んだこと
 - 4-1. コマンドの結果を判定するときに使うのはwhenではなく`failed_when: ${OR条件}`
 - 4-2. ${コマンドの実行結果}.stdoutは文字列ではなく`リスト型`
 - 4-3. エラーキャッチをするなら`Block/Rescue/Alwaysディレクティブ`
   - 今回は`Block/Rescue`を使用

## 4-1. コマンドの実行実行結果を判定するときに使うのはwhenではなく`failed_when: ${OR条件}`

※resultはコマンドの実行結果を格納した変数

```
- when: "{{ result is search("xxx|aaa") }}"
+ failed_when: result.stdout[0] is not search("xxx|aaa")
```

@akira6592 さんに教えていただきました。
ありがとうございます。

>when は、そのタスクを実行するかの条件なので、まだ register: result が評価されておらず、result が未定義になっている、ということだと思います。
when ではなく、failed_when でいかがでしょうか。

出所：[Ansibleユーザーグループのslack #qa](https://ansiblejp.slack.com/archives/CBV59MFV0/p1591670008196300)

### whenは、変数の値確認のときに使う

```yaml

vars:
  url: "http://example.com/users/foo/resources/bar"

tasks:
    - debug:
        msg: "matched pattern 1"
      when: url is match("http://example.com/users/.*/resources/.*")

    - debug:
        msg: "matched pattern 2"
      when: url is search("/users/.*/resources/.*")

    - debug:
        msg: "matched pattern 3"
      when: url is search("/users/")

    - debug:
        msg: "matched pattern 4"
      when: url is regex("example.com/\w+/foo")
```
出所：[Tests &mdash; Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_tests.html)


## 4-2. ${コマンドの実行結果}.stdoutは文字列ではなくリスト型

```
- {{ result.stdout }}"
- {{ result.stdout_lines }}"
+ {{ result.stdout[0] }}"
```

こちらも@akira6592 さんに教えていただきました。
ありがとうございます。
>result.stdout の値は文字列ではなくリストなので、うまく解釈できなかったのかもしれません。

参考：[junos commandモジュールで実行した結果をslackに通知出来ない](https://teratail.com/questions/268722)

Ansibleの公式ドキュメントにもリスト形式で返却と書いてありました。

![Screenshot from 2020-06-11 23-15-31.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/341653/64fa1056-dae9-d2cf-88b4-24c9f20ae50e.png)


出所：[junos_command – Run arbitrary commands on an Juniper JUNOS device &mdash; Ansible Documentation](https://docs.ansible.com/ansible/latest/modules/junos_command_module.html)

※ ↑で使っているslackモジュールや`"{{ result.stdout[0] | default('None', true)}}" `の書き方については、手前味噌ですが、こちらの記事をご参照ください。
[Ansibleのslackモジュールを使う際に必要な"token"でハマった話他[token/トークンの取り扱いと変数のdefault値など]](https://qiita.com/gkzz/items/16029b85844b26b5b8d9)

## 4-3.エラーキャッチをするなら`Block/Rescue/Alwaysディレクティブ`

- `try-catch的？`に、エラーハンドリングをすることがBlock/Rescue/Alwaysディレクティブでは可能です。

>Blocks also introduce the ability `to handle errors in a way similar to exceptions` in most programming languages. 

```yaml

tasks:
 - name: Handle the error
   block:
     - debug:
         msg: 'I execute normally'
     - name: i force a failure
       command: /bin/false
     - debug:
         msg: 'I never execute, due to the above task failing, :-('
   rescue:
     - debug:
         msg: 'I caught an error, can do stuff here to fix it, :-)'
```
出所：[Blocks &mdash; Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html)

### 注意点

- Block/Rescue/Alwaysディレクティブがエラーハンドリングできるタスクは、`failed`となるタスクのみです。
- 未定義なタスクや不到達なホストがあるためにエラーとなった場合、Block/Rescue/Alwaysディレクティブでもエラーハンドリングすることはできません。
 

>Blocks only deal with `‘failed’ status of a task`. `A bad task definition or an unreachable host are not ‘rescuable’ errors`.

出所：[Blocks &mdash; Ansible Documentation](https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html)

### Block/Rescue/Alwaysディレクティブ v.s. try-catch

|Block/Rescue/Always             |try-catch|
|--------------------------------|---------|
|Block                           |try      |
|rescue                          |except   |
|always                         |finally   |

なお、Block/Rescue/Alwaysディレクティブについても @akira6592 さんに教えていただきました。
@akira6592 さんすごすぎです。
ありがとうございます！

>五月雨ですみません。
block/rescue/always 使うのも Ansible っぽくていいかもしれません。
https://docs.ansible.com/ansible/latest/user_guide/playbooks_blocks.html#blocks-error-handling

出所：[Ansibleユーザーグループのslack #qa](https://ansiblejp.slack.com/archives/CBV59MFV0/p1591670008196300)

# 5.サンプルコードとplaybookの実行コマンド

- invetntoryファイル各種とplaybookのサンプルコードです。

## inventory

```inventory/hosts

[junos]
***.***.***.***
↑対象機器のipアドレス
```

```inventory/group_vars/junos
---
# file: group_vars/junos
ansible_connection: network_cli 
ansible_network_os: junos
ansible_port: <JUNOS_PORT>
ansible_ssh_user: <JUNOS_USERNAME>
ansible_ssh_pass: <JUNOS_PASSWORD>
```

```playbook/show_version.yaml
---
- hosts: junos
  connection: local
  gather_facts: no
  become: yes

  vars:
    ptn: "Junos: 15.1|Junos: 15.2"
    #ptn: "Junos: 14.1|Junos: 14.2"

  tasks:
    - name: main
      block:
        - name: display version
          junos_command:
            commands:
              - show version | no-more
          when: ansible_network_os == "junos"
          register: result
          failed_when: result.stdout[0] is not search(ptn)
          changed_when: false
        - name: debug result.stdout_lines
          debug:
            msg: "{{ result.stdout_lines }}"
        
        - name: Use the attachments API
          slack:
            username: "{{ lookup('env', 'SLACKBOT_NAME') | default('ansibleBot')  }}"
            token: "{{ lookup('env', 'INCOMING_WEBHOOK_URL_SLACKFORMAT') }}"
            channel: "{{ lookup('env', 'DST_CHANNEL') }}"
            attachments:
            - title: display version [ " {{ ptn }} "]
              text: "Successful"
              color: "#ff00dd"
              fields:
              - title: Hostname
                value: "{{ inventory_hostname }}"
                short: False
              - title: "The output"
                value: "{{ result.stdout[0] | default('None', true)}}"
                short: False

      rescue:
        - name: Use the attachments API
          slack:
            username: "{{ lookup('env', 'SLACKBOT_NAME') | default('ansibleBot')  }}"
            token: "{{ lookup('env', 'INCOMING_WEBHOOK_URL_SLACKFORMAT') }}"
            channel: "{{ lookup('env', 'DST_CHANNEL') }}"
            attachments:
            - title: display current time [ " {{ ptn }} "]
              text: <!here> `Failed`
              color: "#ff00dd"
              fields:
              - title: Hostname
                value: "{{ inventory_hostname }}"
                short: False
              - title: "The output"
                value: "{{ result.stdout[0] | default('None', true)}}"
                short: False

```

※サンプルコードは、Githubでも公開しております。
 [ansible-sample](https://github.com/gkzz/ansible-sample)

- playbookの実行コマンド

```
$ ansible-playbook -i inventory/hosts playbook/show_varsion.yaml
```


- デバッグしたい場合は`-vオプション`
- vの数だけデバッグの量が増える点はsshの-`vオプション`と同じ
 
```
$ ansible-playbook -v -i inventory/hosts playbook/show_varsion.yaml
```

# 6. 要改善点

- slackモジュールがblockディレクティブとrescueディレクティブの2箇所に書いてあるところ。
  - うまいことひとつにまとめられないか。 
  - Ansibleがタスクの成否の結果によって、どのように通知するテキストを変えるか。

- slackモジュールがblockディレクティブとrescueディレクティブの2箇所に書いてある理由
　- タスクが成功すれば、`Successful`、失敗すれば`<!here> Failed`とslackに通知したいので、blockディレクティブとrescueディレクティブそれぞれにテキストでベタ書きとした。


P.S. Twitterもやってるのでフォローしていただけると泣いて喜びます:)
[@gkzvoice](https://twitter.com/gkzvoice)