【Ansible】validateモジュールで変数チェックしたい

### はじめに
<バージョン>
ansible 2.9.7
jsonschema 3.2.0

　変数が正しく入力されているかの確認は、以下のようにassertモジュールで行えますが、
validateモジュールでも可能らしいので検証します。

```yaml
- name: "check string"
  assert:
    that:
      - sample_string is defined
      - sample_string is string
  vars:
    sample_string: 'abc'
```

### 0. 下準備
　validate モジュールを使うには、ansible.utils collectionとJSON Schemaが必要になりますので、
以下のコマンドでインストールしてください。

```text
ansible-galaxy collection install ansible.utils
pip install jsonschema
```

### 1. 参考playbook
　今回は以下のplaybookで7パターンの検証を行います。

|データ型 |変数名 |例 |
|---|---|---|
|string型 |sample_string |"aaa" |
|integer型(整数)  |sample_integer |22 |
|double型(小数) |sample_double |10.5 |
|array型(配列) |sample_array |["aaa", 10] |
|dict型 |sample_dict |{"name": "Taro", "age": 10 } |
|boolean型 |sample_boolean |true |
|ipv4 |sample_ipv4 |192.168.0.1/24（または192.168.0.1） |


```yaml:validate.yml
---
- name: schema test
  hosts: localhost
  gather_facts: false
  vars:
    sample_data:
      sample_string: "aaa"
      sample_integer: 22
      sample_double: 10.5
      sample_array:
        - "aaa"
        - 10
      sample_dict:
        name: "Taro"
        age: 10
      sample_boolean: true
      sample_ipv4: "192.168.0.1/24"
  tasks:
    - name: validate
      ansible.utils.validate:
        data: "{{ sample_data }}"
        criteria: "{{ lookup('file', 'validate.json') }}"
        engine: ansible.utils.jsonschema
      vars:
        ansible_jsonschema_draft: draft7
```

### 2. validate用変数ファイル
　今回は以下のようなJSONファイルを作成しました。。
「required」で変数自体の存在確認を行い、「properties」で変数ごとに判定を行います。
「properties」直下には確認したい変数名を記載し、さらにその下に確認内容を記載します。
データ型ごとのオプションの説明は以下にまとめますが、他にも様々なオプションがありますので、
[Understanding JSON Schema](https://json-schema.org/understanding-json-schema/index.html)をご参照ください。

__string型__
「"minLength": 1」-> 文字数は1文字以上
__integer型(整数)/double型(小数)__
「"exclusiveMinimum": 0」-> 0より大きい数字
__array型(配列)__
「"minItems": 1」-> 要素数は1以上
「items」配下に要素ごとの条件を記載。今回は1要素目がstring型、2要素目がinteger型。 
__dict型__
「properties」配下にkeyごとの条件を記載。今回はnameがstring型、ageがinteger型。 
__ipv4__
「pattern」にipv4を[正規表現](https://json-schema.org/understanding-json-schema/reference/regular_expressions.html)にて記載。
正規表現だと結構複雑になるので別の方法を検討した方がよいかもしれません。
書き方としては[Let'sプログラミング-IPアドレス(IPv4)の正規表現](https://www.javadrive.jp/regex-basic/sample/index4.html)を参考にしつつ、
192.168.0.1/24と192.168.0.1の両方に対応できるように修正。
マスクの部分は0-32しか入らないので、以下のように表現している。
ちなみに、「"format": "ipv4"」が使えるようですが[バグ](https://cloud6.net/so/python/1513497)がありうまく動作しません。

|正規表現 |対応範囲 |
|---|---|
|/3[0-2] |/30 ～ /32 |
|/[1-2]?[0-9] | /0 ～ /29 |


```json:validate.json
{
  "required": [
    "sample_string",
    "sample_integer",
    "sample_double",
    "sample_array",
    "sample_dict",
    "sample_boolean",
    "sample_ipv4"
  ],
  "properties": {
    "sample_string": {
      "type": "string",
      "minLength": 1 
    },
    "sample_integer": {
      "type": "integer",
      "exclusiveMinimum": 0
    },
    "sample_double": {
      "type": "number",
      "exclusiveMinimum": 10.49
    },
    "sample_array": {
      "type": "array",
      "minItems": 1,
      "items": [
        {
          "type": "string",
          "minLength": 1
        },
        {
          "type": "integer",
          "exclusiveMinimum": 0
        }
      ]
    },
    "sample_dict": {
      "type": "object",
      "properties": {
        "name": { 
          "type": "string"  
        },
        "age": {
          "type": "integer"
        }
      }
    },
    "sample_boolean": {
      "type": "boolean"
    },
    "sample_ipv4": {
      "type": "string",
      "pattern": "^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(/3[0-2]|/[1-2]?[0-9])?$"
    },
  }
}

```
### 3. Playbook動作確認
### 3-1. 変数sample_stringが未定義だった場合
　実行結果の最後にメッセージとして、
「At 'required' u'sample_string' is a required property.」
と出力されており、未定義の変数があることを見つけることが出来ました。

```text
[ec2-user@ip-<ip addr> ansible]$ ansible-playbook validate.yml 

PLAY [schema test] ***********************************************************************************************************************************

TASK [validate] **************************************************************************************************************************************
fatal: [localhost]: FAILED! => changed=false 
  errors:
  - data_path: ''
    expected:
    - sample_string
    - sample_integer
    - sample_double
    - sample_array
    - sample_dict
    - sample_boolean
    - sample_ipv4
    found:
      sample_array:
      - aaa
      - 10
      sample_boolean: true
      sample_dict:
        age: 10
        name: Taro
      sample_double: 10.5
      sample_integer: 22
      sample_ipv4: 192.168.0.1/24
    json_path: $
    message: u'sample_string' is a required property
    relative_schema:
      properties:
        sample_array:
          items:
          - minLength: 1
            type: string
          - exclusiveMinimum: 0
            type: integer
          minItems: 1
          type: array
        sample_boolean:
          type: boolean
        sample_dict:
          properties:
            age:
              type: integer
            name:
              type: string
          type: object
        sample_double:
          exclusiveMinimum: 10.49
          type: number
        sample_integer:
          exclusiveMinimum: 0
          type: integer
        sample_ipv4:
          pattern: ^((25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])(/3[0-2]|/[1-2]?[0-9])?$
          type: string
        sample_string:
          minLength: 1
          type: string
      required:
      - sample_string
      - sample_integer
      - sample_double
      - sample_array
      - sample_dict
      - sample_boolean
      - sample_ipv4
    schema_path: required
    validator: required
  msg: |-
    Validation errors were found.
    At 'required' u'sample_string' is a required property.

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

### 3-2. 変数sample_stringがinteger型(今回は100)だった場合
　実行結果の最後にメッセージとして、
「At 'properties.sample_string.type' 100 is not of type u'string'.」
と出力されており、データ型に誤りがあることを見つけることが出来ました。

```text
[ec2-user@ip-<ip addr> ansible]$ ansible-playbook validate.yml 

PLAY [schema test] ***********************************************************************************************************************************

TASK [validate] **************************************************************************************************************************************
fatal: [localhost]: FAILED! => changed=false 
  errors:
  - data_path: sample_string
    expected: string
    found: 100
    json_path: $.sample_string
    message: 100 is not of type u'string'
    relative_schema:
      minLength: 1
      type: string
    schema_path: properties.sample_string.type
    validator: type
  msg: |-
    Validation errors were found.
    At 'properties.sample_string.type' 100 is not of type u'string'.

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

### まとめ
　データ型ごとの確認用にさまざまなオプションがあるので今後もいろいろ使ってみましょう。

### 参考URL
[[Ansible] 「つまずき Ansible 【Part21】ansible.utils collection を試す」ふりかえり](https://tekunabe.hatenablog.jp/entry/2020/12/12/ansible_stumble_21)
[json schemaのよく利用する機能まとめ](https://qiita.com/dorachan1029/items/57b86116ae67e94ee1ff)
[Understanding JSON Schema](https://json-schema.org/understanding-json-schema/index.html)
[Understanding JSON Schema - Regular Expressions](https://json-schema.org/understanding-json-schema/reference/regular_expressions.html)
[Let'sプログラミング-IPアドレス(IPv4)の正規表現](https://www.javadrive.jp/regex-basic/sample/index4.html)
[python - IPアドレスを検証するJSONスキーマが機能していません](https://cloud6.net/so/python/1513497)