Ansibleで物理NICのリストを取得する方法

# Ansibleで物理NICのリストを取得する方法

## 何がしたいのか

大昔は、 `eth.*` を狙えば全ての物理ネットワークインタフェース名に当たるという時代でした。が、Consistent Network Device NamingやらPredictable Network Interface Namesが入り乱れる現代では、ネットワークインタフェース名を正規表現で当てに行くのは怪しいと言えます(もちろんCNDNやPNINを無効にすれば過去には戻れます)。ということで、現代風に物理ネットワークインタフェース名のリストをAnsibleで作ってみます。

前提として `ansible.cfg` とインベントリは予め作っておきます。

``` dosini:ansible.cfg
[defaults]
inventory = inventory
```
``` dosini:inventory
localhost ansible_connection=local
```


## `ansible_interfaces` を見てみる

```
[root@localhost ~]# ansible all -m setup -a 'filter=ansible_interfaces'
localhost | success >> {
    "ansible_facts": {
        "ansible_interfaces": [
            "lo",
            "docker0",
            "eth1",
            "eth0"
        ]
    },
    "changed": false
}
```

とりあえず全てのネットワークインタフェース名は取れましたが、 `lo` とか `docker0` とかが邪魔です。物理ネットワークインタフェースだけを抽出する手がかりがないか、setupモジュールで取れるfactsをひと通り眺めてみます。


## setupモジュールで取れるfacts

``` plain:長いのでだいぶ省略しています
[root@localhost ~]# ansible all -m setup
localhost | success >> {
    "ansible_facts": {
...
        "ansible_docker0": {
            "active": false,
            "device": "docker0",
            "id": "8000.56847afe9799",
            "interfaces": [],
            "ipv4": {
                "address": "192.168.123.1",
                "netmask": "255.255.255.0",
                "network": "192.168.123.0"
            },
            "macaddress": "56:84:7a:fe:97:99",
            "mtu": 1500,
            "promisc": false,
            "stp": false,
            "type": "bridge"
        },
...
        "ansible_eth0": {
            "active": true,
            "device": "eth0",
            "ipv4": {
                "address": "10.0.2.15",
                "netmask": "255.255.255.0",
                "network": "10.0.2.0"
            },
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fe96:8299",
                    "prefix": "64",
                    "scope": "link"
                }
            ],
            "macaddress": "08:00:27:96:82:99",
            "module": "virtio_net",
            "mtu": 1500,
            "promisc": false,
            "type": "ether"
        },
        "ansible_eth1": {
            "active": true,
            "device": "eth1",
            "ipv4": {
                "address": "192.168.56.2",
                "netmask": "255.255.255.0",
                "network": "192.168.56.0"
            },
            "ipv6": [
                {
                    "address": "fe80::a00:27ff:fe66:2832",
                    "prefix": "64",
                    "scope": "link"
                }
            ],
            "macaddress": "08:00:27:66:28:32",
            "module": "virtio_net",
            "mtu": 1500,
            "promisc": false,
            "type": "ether"
        },
...
        "ansible_interfaces": [
            "lo",
            "docker0",
            "eth1",
            "eth0"
        ],
...
        "ansible_lo": {
            "active": true,
            "device": "lo",
            "ipv4": {
                "address": "127.0.0.1",
                "netmask": "255.0.0.0",
                "network": "127.0.0.0"
            },
            "ipv6": [
                {
                    "address": "::1",
                    "prefix": "128",
                    "scope": "host"
                }
            ],
            "mtu": 65536,
            "promisc": false,
            "type": "loopback"
        },
...
    },
    "changed": false
}
```

`ansible_{{ ネットワークインタフェース名 }}.module` が存在するもの、が一番良さそうです。

上の例では `ansible_{{ ネットワークインタフェース名 }}.type` が `ether` であるものも物理インタフェースっぽいですが、反例が存在します。

```
[root@localhost ~]# ip link add name veth_left type veth peer name veth_right
[root@localhost ~]# ansible all -m setup -a 'filter=ansible_veth_left'
localhost | success >> {
    "ansible_facts": {
        "ansible_veth_left": {
            "active": false,
            "device": "veth_left",
            "macaddress": "aa:19:3b:6d:4f:b6",
            "mtu": 1500,
            "promisc": false,
            "type": "ether"
        }
    },
    "changed": false
}

[root@localhost ~]# ip link delete dev veth_left
```

`ansible_{{ ネットワークインタフェース名 }}.module` が存在する ネットワークインターフェース名 をリストにする事にします。


## `set_fact` と `with_items`

試しに、 `ansible_interfaces` を `with_items` で回しながら、 `ansible_{{ ネットワークインタフェース名 }}.module` が存在するものを `set_fact` してみます。

``` yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      when: "ansible_{{ item }}.module is defined"
      with_items: ansible_interfaces

    - debug: var=pif_name
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)
ok: [localhost] => (item=eth1)
ok: [localhost] => (item=eth0)

TASK: [debug var=pif_name] ****************************************************
ok: [localhost] => {
    "var": {
        "pif_name": "eth0"
    }
}

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

`set_fact` タスクは `when` に従って `lo` と `docker0` をskipしていますが、 `with_items` で回したら `set_fact` が勝手にリストを作るなんて事はないようで、 `pif_name` の中身は期待通りではありません。


## `set_fact` の結果を見てみる

`set_fact` タスクの結果を `register` して見てみましょう。

```yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      register: set_fact_result
      when: "ansible_{{ item }}.module is defined"
      with_items: ansible_interfaces

    - debug: var=set_fact_result
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)
ok: [localhost] => (item=eth1)
ok: [localhost] => (item=eth0)

TASK: [debug var=set_fact_result] *********************************************
ok: [localhost] => {
    "var": {
        "set_fact_result": {
            "changed": false,
            "msg": "All items completed",
            "results": [
                {
                    "changed": false,
                    "skipped": true
                },
                {
                    "changed": false,
                    "skipped": true
                },
                {
                    "ansible_facts": {
                        "pif_name": "eth1"
                    },
                    "invocation": {
                        "module_args": "",
                        "module_name": "set_fact"
                    },
                    "item": "eth1"
                },
                {
                    "ansible_facts": {
                        "pif_name": "eth0"
                    },
                    "invocation": {
                        "module_args": "",
                        "module_name": "set_fact"
                    },
                    "item": "eth0"
                }
            ]
        }
    }
}

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

余計なものが多いですが、 `register` した結果をどうにかすれば出来そうです。


## `set_fact` の結果から要るものだけを取り出す

まず、前のタスクで `when` にヒットしたものだけを取り出します。方針として

1. `skipped` が定義されていないもの
2. `skipped` が定義されていないもの、または `skipped` が定義されていて `true` でないもの
3. `ansible_facts` が定義されているもの

の3つがありそうですが、1はもしかしたら将来 *skipしなかったタスクに `skipped` が `false` で付く* という仕様になるかもしれないのでパス、2は確実ですが条件式の記述が3よりめんどくさそうなので、3を使います。

```yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      register: set_fact_result
      when: "ansible_{{ item }}.module is defined"
      with_items: ansible_interfaces

    - set_fact:
        pif_names: "{{ set_fact_result.results|selectattr('ansible_facts', 'defined') }}"

    - debug: var=pif_names
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)
ok: [localhost] => (item=eth1)
ok: [localhost] => (item=eth0)

TASK: [set_fact ] *************************************************************
ok: [localhost]

TASK: [debug var=pif_names] ***************************************************
ok: [localhost] => {
    "var": {
        "pif_names": "<generator object _select_or_reject at 0x22995a0>"
    }
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

ジェネレータが返ってきてしまったのでリストにしてみます。
ついでに長くなってきたので改行しましょう。

```yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      register: set_fact_result
      when: "ansible_{{ item }}.module is defined"
      with_items: ansible_interfaces

    - set_fact:
        pif_names: >-
          {{ set_fact_result.results
             | selectattr('ansible_facts', 'defined')
             | list }}

    - debug: var=pif_names
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)
ok: [localhost] => (item=eth1)
ok: [localhost] => (item=eth0)

TASK: [set_fact ] *************************************************************
ok: [localhost]

TASK: [debug var=pif_names] ***************************************************
ok: [localhost] => {
    "var": {
        "pif_names": [
            {
                "ansible_facts": {
                    "pif_name": "eth1"
                },
                "invocation": {
                    "module_args": "",
                    "module_name": "set_fact"
                },
                "item": "eth1"
            },
            {
                "ansible_facts": {
                    "pif_name": "eth0"
                },
                "invocation": {
                    "module_args": "",
                    "module_name": "set_fact"
                },
                "item": "eth0"
            }
        ]
    }
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

だいぶそれっぽくなってきました。あとは `pif_names` の各辞書から `item` か `ansible_facts.pif_name` を取り出してそれだけをリストにすれば良さそうです。

```yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      register: set_fact_result
      when: "ansible_{{ item }}.module is defined"
      with_items: ansible_interfaces

    - set_fact:
        pif_names: >-
          {{ set_fact_result.results
             | selectattr('ansible_facts', 'defined')
             | map(attribute='ansible_facts.pif_name')
             | list }}

    - debug: var=pif_names
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=docker0)
ok: [localhost] => (item=eth1)
ok: [localhost] => (item=eth0)

TASK: [set_fact ] *************************************************************
ok: [localhost]

TASK: [debug var=pif_names] ***************************************************
ok: [localhost] => {
    "var": {
        "pif_names": [
            "eth1",
            "eth0"
        ]
    }
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

これでリストなりましたが、場合によってはうまくいきません。


## caveat: Ansibleのfactのキー名

```
[root@localhost ~]# ifdown eth0
[root@localhost ~]# ip link set dev eth0 name bar-eth0
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
skipping: [localhost] => (item=bar-eth0)
ok: [localhost] => (item=eth1)
skipping: [localhost] => (item=docker0)

TASK: [set_fact ] *************************************************************
ok: [localhost]

TASK: [debug var=pif_names] ***************************************************
ok: [localhost] => {
    "var": {
        "pif_names": [
            "eth1"
        ]
    }
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

`bar-eth0` が出てきません。これはAnsibleがfactのキーを [`replace`](https://github.com/ansible/ansible/blob/stable-1.9/lib/ansible/module_utils/facts.py#L2689) している為、最初の `set_fact` の `when` にヒットしなくなってしまうからです。探すキーを `replace` したものにしてあげましょう。

```yaml:foo.yaml
---

- hosts: localhost
  tasks:
    - set_fact:
        pif_name: "{{ item }}"
      register: set_fact_result
      when: "ansible_{{ item|replace('-', '_') }}.module is defined"
      with_items: ansible_interfaces

    - set_fact:
        pif_names: >-
          {{ set_fact_result.results
             | selectattr('ansible_facts', 'defined')
             | map(attribute='ansible_facts.pif_name')
             | list }}

    - debug: var=pif_names
```
```
[root@localhost ~]# ansible-playbook foo.yaml

PLAY [localhost] **************************************************************

GATHERING FACTS ***************************************************************
ok: [localhost]

TASK: [set_fact ] *************************************************************
skipping: [localhost] => (item=lo)
ok: [localhost] => (item=bar-eth0)
ok: [localhost] => (item=eth1)
skipping: [localhost] => (item=docker0)

TASK: [set_fact ] *************************************************************
ok: [localhost]

TASK: [debug var=pif_names] ***************************************************
ok: [localhost] => {
    "var": {
        "pif_names": [
            "bar-eth0",
            "eth1"
        ]
    }
}

PLAY RECAP ********************************************************************
localhost                  : ok=4    changed=0    unreachable=0    failed=0
```

これで `bar-eth0` が出てきました。


## 参考

ロジックは [stack overflow](http://stackoverflow.com/questions/29399581/using-set-facts-and-with-items-together-in-ansible) のパクリです。