【Ansible】入力がIPv4アドレスであることを確認する最適解とは

### はじめに
<バージョン>
ansible 2.9.1
jinja2 2.11

playbook実行時に、assertモジュールで入力が正しいことを確認することは大変重要です。
今回は、入力がIPv4アドレスであることを確認するにはどうすればよいかを調査します。

### Playbook1 ～debugによる確認～
想定される入力を色々と書いてみました。

```yaml:ipv4_sample_1.yml
---
- name: "check ip address"
  hosts: localhost
  gather_facts: no
  vars:
    input_vars:
      - "192.168.0.1"
      - "192.168.0.1/24"
      - ["192.168.0.1", "192.168.0.2"]
      - ["192.168.0.1", "a"]
      - "192.168.0.256"
      - 1.2
      - "1.2"
      - -255
      - 255
      - "a"
      - ""

  tasks:
    - name: "debug result of filter"
      debug:
        msg:
         - "{{ item }}"
         - "{{ item | type_debug }}"
         - "{{ item | ipv4 }}"
      loop: "{{ input_vars }}"
```

### Playbook1 実行結果
実行結果をまとめると以下のようになり、わかることとしては
（1）ipv4フィルタで変換が出来なかった場合はfalseになる
（2）ipv4フィルタにはlistで渡すことも可能で、変換できた変数だけが戻ってくる（項番4より）
（3）ipv4フィルタは、__後半に0を補填する機能がある__ため、"1.2" -> "1.2.0.0"
　　になり、結果がfalseにならない（項番7より）
（4）ipv4フィルタは正の整数をIPv4アドレスに変換する（項番9より）
　　→10進数を2進数に変換しているので誤りではない

|項番|入力 |データ型 |ipv4変換結果 |
|---|---|---|---|
|1 |"192.168.0.1" |unicode  |192.168.0.1  |
|2 |"192.168.0.1/24" |unicode  |192.168.0.1/24  |
|3 |["192.168.0.1", "192.168.0.2"] |list | ["192.168.0.1", "192.168.0.2"]
|4 |["192.168.0.1", "a"]|list | __["192.168.0.1"]__
|5 |"192.168.0.256" |unicode | false
|6 |1.2 | float | false
|7 |"1.2" |unicode | __1.2.0.0__
|8 |-255 |int | false
|9 |255 |int |__0.0.0.255__
|10 |"a" |unicode | false
|11 |"" |unicode | false

ちなみに、実行ログは以下になります。

```text:出力
[ec2-user@ip-<ip addr> ansible]$ ansible-playbook ipv4_sample_1.yml

PLAY [check ip address] ************************************************************************************************

TASK [debug result of filter] ******************************************************************************************
ok: [localhost] => (item=192.168.0.1) =>
  msg:
  - 192.168.0.1
  - unicode
  - 192.168.0.1
ok: [localhost] => (item=192.168.0.1/24) =>
  msg:
  - 192.168.0.1/24
  - unicode
  - 192.168.0.1/24
ok: [localhost] => (item=[u'192.168.0.1', u'192.168.0.2']) =>
  msg:
  - - 192.168.0.1
    - 192.168.0.2
  - list
  - - 192.168.0.1
    - 192.168.0.2
ok: [localhost] => (item=[u'192.168.0.1', u'a']) =>
  msg:
  - - 192.168.0.1
    - a
  - list
  - - 192.168.0.1
ok: [localhost] => (item=192.168.0.256) =>
  msg:
  - 192.168.0.256
  - unicode
  - false
ok: [localhost] => (item=1.2) =>
  msg:
  - 1.2
  - float
  - false
ok: [localhost] => (item=1.2) =>
  msg:
  - '1.2'
  - unicode
  - 1.2.0.0
ok: [localhost] => (item=-255) =>
  msg:
  - -255
  - int
  - false
ok: [localhost] => (item=255) =>
  msg:
  - 255
  - int
  - 0.0.0.255
ok: [localhost] => (item=a) =>
  msg:
  - a
  - unicode
  - false
ok: [localhost] => (item=) =>
  msg:
  - ''
  - unicode
  - false

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

### Playbook2 ～IPv4であることの確認～
上記の結果を元に、「IPv4アドレスが文字列で入力されたこと（表の項番1と項番2）」を
assertで確認するplaybookを作成しました。
assertでは以下3点を確認します。
（1）入力が文字列であること
　　->前提条件。「項番9：入力が正の整数だった場合」の対策にもなる。
（2）splitの結果、要素数が4つになること
　　->「項番7：入力が "." を含む文字列だった場合」の対策
（3）ipv4フィルタの結果がfalseにならないこと

```yaml:ipv4_sample_2.yml
---
- name: "check ip address"
  hosts: localhost
  gather_facts: no
  vars:
    input_vars:
      - "192.168.0.1"
      - "192.168.0.1/24"
      - ["192.168.0.1", "192.168.0.2"]
      - ["192.168.0.1", "a"]
      - "192.168.0.256"
      - 1.2
      - "1.2"
      - -255
      - 255
      - "a"
      - ""

  tasks:
    - name: "check ipv4 address"
      assert:
        that:
          - item is string
          - item.split('.') | length == 4
          - item | ipv4 is not false
      loop: "{{ input_vars }}"
      ignore_errors: true
```

### Playbook2 実行結果
想定通りのassert結果を得ることが出来ました。

```text:出力
[ec2-user@ip-<ip addr> ansible]$ ansible-playbook ipv4_sample_2.yml

PLAY [check ip address] ************************************************************************************************

TASK [check ipv4 address] **********************************************************************************************
ok: [localhost] => (item=192.168.0.1) => changed=false
  ansible_loop_var: item
  item: 192.168.0.1
  msg: All assertions passed
ok: [localhost] => (item=192.168.0.1/24) => changed=false
  ansible_loop_var: item
  item: 192.168.0.1/24
  msg: All assertions passed
failed: [localhost] (item=[u'192.168.0.1', u'192.168.0.2']) => changed=false
  ansible_loop_var: item
  assertion: item is string
  evaluated_to: false
  item:
  - 192.168.0.1
  - 192.168.0.2
  msg: Assertion failed
failed: [localhost] (item=[u'192.168.0.1', u'a']) => changed=false
  ansible_loop_var: item
  assertion: item is string
  evaluated_to: false
  item:
  - 192.168.0.1
  - a
  msg: Assertion failed
failed: [localhost] (item=192.168.0.256) => changed=false
  ansible_loop_var: item
  assertion: item | ipv4 is not false
  evaluated_to: false
  item: 192.168.0.256
  msg: Assertion failed
failed: [localhost] (item=1.2) => changed=false
  ansible_loop_var: item
  assertion: item is string
  evaluated_to: false
  item: 1.2
  msg: Assertion failed
failed: [localhost] (item=1.2) => changed=false
  ansible_loop_var: item
  assertion: item.split('.') | length == 4
  evaluated_to: false
  item: '1.2'
  msg: Assertion failed
failed: [localhost] (item=-255) => changed=false
  ansible_loop_var: item
  assertion: item is string
  evaluated_to: false
  item: -255
  msg: Assertion failed
failed: [localhost] (item=255) => changed=false
  ansible_loop_var: item
  assertion: item is string
  evaluated_to: false
  item: 255
  msg: Assertion failed
failed: [localhost] (item=a) => changed=false
  ansible_loop_var: item
  assertion: item.split('.') | length == 4
  evaluated_to: false
  item: a
  msg: Assertion failed
failed: [localhost] (item=) => changed=false
  ansible_loop_var: item
  assertion: item.split('.') | length == 4
  evaluated_to: false
  item: ''
  msg: Assertion failed
...ignoring

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

### 参考資料
[Ansible Documentation - ipaddr filter](https://docs.ansible.com/ansible/latest/user_guide/playbooks_filters_ipaddr.html)