Ansibleでファイル・ディレクトリの存在確認

## この記事は
Ansibleのstatモジュールで、ファイル・ディレクトリの存在確認がどのように行えるかテストしています。
いつも忘れるので書き留めておきます。
2019年2月の時点で作業を行っています。

## テストした環境
- CentOS Linux release 7.6.1810
- python version 2.7.5（`ansible --version`で表示された）
- ansible 2.4.2.0
- ansible-playbook 2.4.2.0

## 取得できるステータスの確認方法

ファイル存在確認を行うには、statモジュールで対象のパスを指定します。
``register: result``を記述することで、result変数にステータスが格納されます。
変数の値を判定することでファイルの存在確認を行います。

まずは、実際にどのようなステータスを受け取るのか確認します。
下記のplaybookファイルを作成します。

```yaml:chktest.yml
- hosts: 127.0.0.1
  connection: local
  gather_facts: False

  tasks:
    - name: check test
      stat:
        path: "test"
      register: result

    - name: debug
      debug: var=result
```

## 対象がファイルの場合のステータス

対象のファイルを作成して、playbookを実行します。

```shell:
$ touch test
$ ansible-playbook chktest.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [127.0.0.1] ***************************************************************

TASK [check test] **************************************************************
ok: [127.0.0.1]

TASK [debug result var] ********************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "failed": false,
        "stat": {

        -- 略 --

            "exists": true,

        -- 略 --

            "isblk": false,
            "ischr": false,
            "isdir": false,
            "isfifo": false,
            "isgid": false,
            "islnk": false,
            "isreg": true,
            "issock": false,
            "isuid": false,

        -- 略 --

        }
    }
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0

```

`exists` と `isreg` が `true` になっています。
対象がディレクトリだった場合、``isreg``ではなく ``isdir`` が ``true`` になります。

## 対象が存在しない場合のステータス

対象のファイルを削除して、playbookを実行します。

```shell:
$ rm test
$ ansible-playbook chktest.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [127.0.0.1] ***************************************************************

TASK [check test] **************************************************************
ok: [127.0.0.1]

TASK [debug result var] ********************************************************
ok: [localhost] => {
    "result": {
        "changed": false,
        "failed": false,
        "stat": {
            "exists": false
        }
    }
}

PLAY RECAP *********************************************************************
localhost                  : ok=3    changed=0    unreachable=0    failed=0

```

出力がだいぶ少なくなりました。
``exists`` が ``false`` になっています。

## 実際に判定する場合

playbookファイルを作成します。

```yaml:chkfile.yml
$ cat chkfile.yml
- hosts: 127.0.0.1
  connection: local
  gather_facts: False

  tasks:
    - name: check test
      stat:
        path: "test"
      register: chk_file

    - name: isreg check
      debug:
        msg: "complete"
      when: chk_file.stat.isreg == true
      ignore_errors: yes

    - name: isdir check
      debug:
        msg: "complete"
      when: chk_file.stat.isdir == true
      ignore_errors: yes

    - name: exists check
      debug:
        msg: "complete"
      when: chk_file.stat.exists == true
      ignore_errors: yes

    - name: exists and isreg check
      debug:
        msg: "complete"
      when: chk_file.stat.exists == true and chk_file.stat.isreg == true
      ignore_errors: yes

    - name: exists and isdir check
      debug:
        msg: "complete"
      when: chk_file.stat.exists == true and chk_file.stat.isdir == true
      ignore_errors: yes
```

途中で ``FAILED`` になっても止まらないように、``ignore_errors: yes``をつけています。

## 対象がファイルの場合

対象のファイルを作成して、playbookを実行します。

```shell:
$ touch test
$ ansible-playbook chkfile.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [127.0.0.1] ***************************************************************

TASK [check test] **************************************************************
ok: [127.0.0.1]

TASK [isreg check] *************************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

TASK [isdir check] *************************************************************
skipping: [127.0.0.1]

TASK [exists check] ************************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

TASK [exists and isreg check] **************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

TASK [exists and isdir check] **************************************************
skipping: [127.0.0.1]

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=4    changed=0    unreachable=0    failed=0

```

対象がファイルの場合に実行される判定方法

```shell:
when: chk_file.stat.isreg == true
when: chk_file.stat.exists == true
when: chk_file.stat.exists == true and chk_file.stat.isreg == true
```

対象がファイルの場合にスキップされる判定方法

```shell:
when: chk_file.stat.isdir == true
when: chk_file.stat.exists == true and chk_file.stat.isdir == true
```


## 対象がディレクトリの場合

対象のディレクトリを作成して、playbookを実行します。

```shell:
$ rm test
$ mkdir test
$ ansible-playbook chkfile.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [127.0.0.1] ***************************************************************

TASK [check test] **************************************************************
ok: [127.0.0.1]

TASK [isreg check] *************************************************************
skipping: [127.0.0.1]

TASK [isdir check] *************************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

TASK [exists check] ************************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

TASK [exists and isreg check] **************************************************
skipping: [127.0.0.1]

TASK [exists and isdir check] **************************************************
ok: [127.0.0.1] => {
    "msg": "complete"
}

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=4    changed=0    unreachable=0    failed=0

```

対象がディレクトリの場合に実行される判定方法

```shell:
when: chk_file.stat.isdir == true
when: chk_file.stat.exists == true
when: chk_file.stat.exists == true and chk_file.stat.isdir == true
```

対象がディレクトリの場合にスキップされる判定方法

```shell:
when: chk_file.stat.isreg == true
when: chk_file.stat.exists == true and chk_file.stat.isreg == true
````

## 対象が存在しない場合

対象のディレクトリを削除して、playbookを実行します。

```shell:
$ rmdir test
$ ansible-playbook chkfile.yml
 [WARNING]: provided hosts list is empty, only localhost is available. Note
that the implicit localhost does not match 'all'


PLAY [127.0.0.1] ***************************************************************

TASK [check test] **************************************************************
ok: [127.0.0.1]

TASK [isreg check] *************************************************************
fatal: [127.0.0.1]: FAILED! => {"msg": "The conditional check 'chk_file.stat.isreg == true' failed. The error was: error while evaluating conditional (chk_file.stat.isreg == true): 'dict object' has no attribute 'isreg'\n\nThe error appears to have been in '/root/chkfile.yml': line 11, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: isreg check\n      ^ here\n"}
...ignoring

TASK [isdir check] *************************************************************
fatal: [127.0.0.1]: FAILED! => {"msg": "The conditional check 'chk_file.stat.isdir == true' failed. The error was: error while evaluating conditional (chk_file.stat.isdir == true): 'dict object' has no attribute 'isdir'\n\nThe error appears to have been in '/root/chkfile.yml': line 17, column 7, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n    - name: isdir check\n      ^ here\n"}
...ignoring

TASK [exists check] ************************************************************
skipping: [127.0.0.1]

TASK [exists and isreg check] **************************************************
skipping: [127.0.0.1]

TASK [exists and isdir check] **************************************************
skipping: [127.0.0.1]

PLAY RECAP *********************************************************************
127.0.0.1                  : ok=3    changed=0    unreachable=0    failed=0

```

対象が存在しない場合にスキップされる判定方法

```shell:
when: chk_file.stat.exists == true
when: chk_file.stat.exists == true and chk_file.stat.isreg == true
when: chk_file.stat.exists == true and chk_file.stat.isdir == true
```

通常、存在しないステータスを判定すると ``FAILED`` になります。
``and`` でつなげている時、左側の判定が ``false`` だった場合は右側の式は判定されません。そのため、``exists`` の判定だけで終わっており、右側の ``isreg`` や``isdir`` が判定されず ``FAILED`` を回避しています。

対象が存在しない場合に失敗する判定方法

```shell:
when: chk_file.stat.isreg == true
when: chk_file.stat.isdir == true
```


## まとめ

解かりにくいので、表に。

|     |対象が<br>ファイル|対象が<br>ディレクトリ|対象が<br>存在しない|
|:----|:--------------|:-----------------|-----------|
|chk_file.stat.isreg == true|実行|スキップ|失敗|
|chk_file.stat.isdir == true|スキップ|実行|失敗|
|chk_file.stat.exists == true|実行|実行|スキップ|
|chk_file.stat.exists == true and chk_file.stat.isreg == true|実行|スキップ|スキップ|
|chk_file.stat.exists == true and chk_file.stat.isdir == true|スキップ|実行|スキップ|


私の結論としては、対象が存在しなくても ``FAILED`` にならない、以下の判定方法が良いのかなと。

```shell:
when: chk_file.stat.exists == true
when: chk_file.stat.exists == true and chk_file.stat.isreg == true
when: chk_file.stat.exists == true and chk_file.stat.isdir == true
```