Ansibleで物理NICのリストを取得する方法
何がしたいのか
大昔は、 eth.*
を狙えば全ての物理ネットワークインタフェース名に当たるという時代でした。が、Consistent Network Device NamingやらPredictable Network Interface Namesが入り乱れる現代では、ネットワークインタフェース名を正規表現で当てに行くのは怪しいと言えます(もちろんCNDNやPNINを無効にすれば過去には戻れます)。ということで、現代風に物理ネットワークインタフェース名のリストをAnsibleで作ってみます。
前提として ansible.cfg
とインベントリは予め作っておきます。
[defaults]
inventory = 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
[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
してみます。
---
- 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
して見てみましょう。
---
- 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
にヒットしたものだけを取り出します。方針として
-
skipped
が定義されていないもの -
skipped
が定義されていないもの、またはskipped
が定義されていてtrue
でないもの -
ansible_facts
が定義されているもの
の3つがありそうですが、1はもしかしたら将来 skipしなかったタスクに skipped
が false
で付く という仕様になるかもしれないのでパス、2は確実ですが条件式の記述が3よりめんどくさそうなので、3を使います。
---
- 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
ジェネレータが返ってきてしまったのでリストにしてみます。
ついでに長くなってきたので改行しましょう。
---
- 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
を取り出してそれだけをリストにすれば良さそうです。
---
- 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
している為、最初の set_fact
の when
にヒットしなくなってしまうからです。探すキーを replace
したものにしてあげましょう。
---
- 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 のパクリです。