zaki work log

作業ログやら生活ログやらなんやら

[NetBox / Ansible] ダイナミックインベントリを使ってNetBoxに登録されたホスト情報をターゲットノードにAnsibleを実行

Ansibleのダイナミックインベントリ機能を使って、NetBoxに登録しているホスト情報をAnsibleのターゲットホストとして利用してみる。
(ちなみにダイナミックインベントリ機能は書籍とかで存在は知ってはいたけど、実際に手を動かして使ってみるのが今回初めてという…)

NetBoxのdynamic inventory

docs.ansible.com

NetBoxのAPIを使ったダイナミックインベントリは最低限以下の通り。

plugin: netbox.netbox.nb_inventory
api_endpoint: http://192.168.0.19:28080
token: 0123456789abcdef0123456789abcdef01234567

まずはこれらが設定されていればOK

  • pluginの指定。
  • api_endpointにNetBoxのエンドポイント
  • tokenトーク

playbook

とりあえずallに対してpingモジュールを実行。

---
- name: dynamic inventory sample
  hosts: all
  gather_facts: false

  tasks:
    - name: ping
      ping:

実行

ここまでとても雑だけど、この内容でansible-playbookを実行するとこの通り。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml  | cat

PLAY [dynamic inventory sample] ************************************************

TASK [ping] ********************************************************************
fatal: [esxi]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: Permission denied (publickey,keyboard-interactive).'
  unreachable: true
fatal: [instance-20200126-1250]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname instance-20200126-1250: Name or service not known'
  unreachable: true
fatal: [instance-20200127-2223]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: Could not resolve hostname instance-20200127-2223: Name or service not known'
  unreachable: true
[DEPRECATION WARNING]: Distribution Ubuntu 18.04 on host cheddar should use 
/usr/bin/python3, but is using /usr/bin/python for backward compatibility with 
prior Ansible releases. A future Ansible release will default to using the 
discovered platform python for this host. See https://docs.ansible.com/ansible/
2.10/reference_appendices/interpreter_discovery.html for more information. This
 feature will be removed in version 2.12. Deprecation warnings can be disabled 
by setting deprecation_warnings=False in ansible.cfg.
ok: [cheddar]
ok: [cloud-dev]
fatal: [client-dev]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.17 port 22: No route to host'
  unreachable: true
fatal: [k8s-master01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.121 port 22: No route to host'
  unreachable: true
fatal: [k8s-master02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.122 port 22: No route to host'
  unreachable: true
ok: [kubernetes-master]
ok: [kubernetes-worker0]
ok: [kubernetes-worker1]
ok: [manager]
fatal: [k8s-master03]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.123 port 22: No route to host'
  unreachable: true
ok: [manager-dev]
ok: [registry]
fatal: [k8s-worker01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.125 port 22: No route to host'
  unreachable: true
ok: [rhel8]
fatal: [k8s-worker02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.126 port 22: No route to host'
  unreachable: true
fatal: [rhel7]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.26 port 22: No route to host'
  unreachable: true
fatal: [wensley]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.10 port 22: Connection timed out'
  unreachable: true

PLAY RECAP *********************************************************************
cheddar                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
client-dev                 : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
esxi                       : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
instance-20200126-1250     : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
instance-20200127-2223     : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master03               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-master          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker0         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker1         : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
rhel7                      : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel8                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
wensley                    : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   

色が付いてないので見づらいけど、okになっている(疎通の確認ができた)ホストは以下の通り。

f:id:zaki-hmkc:20210127222205p:plain

電源入れてるVMは以下の通り。 あとcheddarVMでなく物理のサーバーで別途動いている。

f:id:zaki-hmkc:20210127222052p:plain

これはNetBoxに情報登録されている全てのDevicesと全てのVirtual Machinesの情報がターゲットホストになるように、Ansibleのダイナミックインベントリ機能でホスト情報が取得できている。

f:id:zaki-hmkc:20210127221943p:plain

f:id:zaki-hmkc:20210127222017p:plain

ちなみに、NetBoxにIPアドレスが設定されていないデバイスまたはVMの場合は、登録しているNameが名前解決できれば(あるいは$HOME/.ssh/configに定義されていれば)アクセスできる。
要は、NetBoxに登録しているNameの内容でインベントリファイルが構成されるのと同等になる。 IPアドレスが付与されていればansible_host=でアドレスが設定されている状態。
(動きから考えておそらく)

allの場合の対象ホストを確認

allに対するpingモジュール実行時のPLAY RECAPに出てるので明確だけど、追試。
以下のタスクを追加して実行。

    - name: print ansible_play_hosts_all
      debug:
        msg: "{{ ansible_play_hosts_all }}"
      run_once: true

出力結果はこの通り。

TASK [print ansible_play_hosts_all] *******************************************
ok: [cheddar] => 
  msg:
  - cheddar
  - esxi
  - wensley
  - client-dev
  - cloud-dev
  - instance-20200126-1250
  - instance-20200127-2223
  - k8s-master01
  - k8s-master02
  - k8s-master03
  - k8s-worker01
  - k8s-worker02
  - kubernetes-master
  - kubernetes-worker0
  - kubernetes-worker1
  - manager
  - manager-dev
  - registry
  - rhel7
  - rhel8

ansible_play_hosts_allについては以下参照。

docs.ansible.com

ホストのグループ指定の設定

実際にはNetBoxに登録した全てのホストに同じ処理をすることはそうそう無く、「登録ホストのうち特定条件の一部のホスト群」という使い方をするはず。
例えば「〇〇Clusterのホストのうち、Roleに□□が設定されているもの」という具合。

まずインベントリファイルにgroup_byパラメタを追加。

plugin: netbox.netbox.nb_inventory
api_endpoint: http://192.168.0.19:28080
token: 0123456789abcdef0123456789abcdef01234567

group_by:
  - cluster

これで、Clusters情報を使ったホストのグルーピングが可能になる。
ただ、プレイブックのhostsにどんなグループ名を指定すれば良いかが、ドキュメントをざっと見渡しても書き方やサンプルが載っていない。

そこで(ググった結果、金魚の人が1年以上前に実施していたやり方が見つかり)ansible-inventory --listを使って指定のインベントリファイルだとグループ情報がどのように定義されているか確認。

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-inventory -i netbox-dynamic-inventory.yml --list
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
{
    "_meta": {
        "hostvars": {
            ..snip..
        }
    },
    "all": {
        "children": [
            "cluster_oci always free",
            "cluster_private-network-1",
            "cluster_vm network",
            "ungrouped"
        ]
    },
    "cluster_oci always free": {
        "hosts": [
            "instance-20200126-1250",
            "instance-20200127-2223"
        ]
    },
    "cluster_private-network-1": {
        "hosts": [
            "zzz"
        ]
    },
    "cluster_vm network": {
        "hosts": [
            "client-dev",
            "cloud-dev",
            "k8s-master01",
            "k8s-master02",
            "k8s-master03",
            "k8s-worker01",
            "k8s-worker02",
            "kubernetes-master",
            "kubernetes-worker0",
            "kubernetes-worker1",
            "manager",
            "manager-dev",
            "registry",
            "rhel7",
            "rhel8"
        ]
    },
    "ungrouped": {
        "hosts": [
            "cheddar",
            "esxi",
            "wensley"
        ]
    }
}
(a2.10) [zaki@cloud-dev netbox (master)]$ 

これでグループ名が判明……ん、、、

    "cluster_vm network": {

cluster_vm network って、、、

これはもしや、、、

グループ名のスペースは不可

---
- name: dynamic inventory sample
  hosts: 'cluster_vm network'
  gather_facts: false

みたいに指定しても、、、

[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm
[WARNING]: Could not match supplied host pattern, ignoring: network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched

REST使ってるのでワンチャンいけるかな、と、+とか%20とか指定してみても、、、

[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm+network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched
[WARNING]: Could not match supplied host pattern, ignoring: cluster_vm%20network

PLAY [dynamic inventory sample] ***********************************************
skipping: no hosts matched

はい、グループ名にスペースが入るためAnsibleからは指定できない。

グループ名にスペースまたはハイフンは使用しないでください。

インベントリーの構築 — Ansible Documentation

dynamic inventoryのグループ名

気を取り直し、NetBox側でCluster名のスペースを_に変更。
ちなみにDevice RolesやSitesのように、Slugを指定できるものは、そちらがスペース等入ってないようになっていればOK (Slugで指定したものがキーになる)

f:id:zaki-hmkc:20210128002443p:plain

これで再度ansible-inventory -i netbox-dynamic-inventory.yml --listを確認すれば、

    "all": {
        "children": [
            "cluster_oci_always_free",
            "cluster_private_network_1",
            "cluster_vm_network",
            "ungrouped"
        ]
    },
    "cluster_oci_always_free": {
        "hosts": [
            "instance-20200126-1250",
            "instance-20200127-2223"
        ]
    },
    "cluster_vm_network": {
        "hosts": [
            "client-dev",
            "cloud-dev",
            "k8s-master01",
            "k8s-master02",
            "k8s-master03",
            "k8s-worker01",
            "k8s-worker02",
            "kubernetes-master",
            "kubernetes-worker0",
            "kubernetes-worker1",
            "manager",
            "manager-dev",
            "registry",
            "rhel7",
            "rhel8"
        ]
    },
    "ungrouped": {
        "hosts": [
            "cheddar",
            "esxi",
            "wensley"
        ]
    }

この通り。(一部省略)

例えばcluster_vm_networkをグループに指定すれば、ターゲットノードはClusterがvm_networkのホストのみになる。
hostsに指定するグループの凡例は「"group_byで指定した要素のキー名" + _ + "指定した要素の値"」

---
- name: dynamic inventory sample
  hosts: cluster_vm_network
  gather_facts: false

  tasks:
    - name: ping
      ping:

このようにターゲットノードのグループ名を以下のように設定してansible-playbookを実行すると、

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml -v
Using /home/zaki/src/ansible-sample/netbox/ansible.cfg as config file
Fetching: http://192.168.0.19:28080/api/docs/?format=openapi
Fetching: http://192.168.0.19:28080/api/dcim/devices/?limit=0&exclude=config_context
Fetching: http://192.168.0.19:28080/api/virtualization/virtual-machines/?limit=0&exclude=config_context
Fetching: http://192.168.0.19:28080/api/dcim/sites/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/regions/?limit=0
Fetching: http://192.168.0.19:28080/api/tenancy/tenants/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/racks/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/rack-groups/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/device-roles/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/device-types/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/platforms/?limit=0
Fetching: http://192.168.0.19:28080/api/dcim/manufacturers/?limit=0
Fetching: http://192.168.0.19:28080/api/virtualization/clusters/?limit=0
Fetching: http://192.168.0.19:28080/api/ipam/services/?limit=0

PLAY [dynamic inventory sample] ***********************************************

TASK [ping] *******************************************************************
ok: [cloud-dev] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
fatal: [client-dev]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.17 port 22: No route to host'
  unreachable: true
fatal: [k8s-master01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.121 port 22: No route to host'
  unreachable: true
fatal: [k8s-master02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.122 port 22: No route to host'
  unreachable: true
fatal: [k8s-master03]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.123 port 22: No route to host'
  unreachable: true
fatal: [k8s-worker01]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.125 port 22: No route to host'
  unreachable: true
ok: [manager] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
ok: [manager-dev] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/bin/python
  ping: pong
fatal: [k8s-worker02]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.126 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-master]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.131 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-worker0]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.135 port 22: No route to host'
  unreachable: true
fatal: [kubernetes-worker1]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.136 port 22: No route to host'
  unreachable: true
ok: [rhel8] => changed=false 
  ansible_facts:
    discovered_interpreter_python: /usr/libexec/platform-python
  ping: pong
fatal: [registry]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.21 port 22: No route to host'
  unreachable: true
fatal: [rhel7]: UNREACHABLE! => changed=false 
  msg: 'Failed to connect to the host via ssh: ssh: connect to host 192.168.0.26 port 22: No route to host'
  unreachable: true

PLAY RECAP ********************************************************************
client-dev                 : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
cloud-dev                  : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-master03               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker01               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
k8s-worker02               : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-master          : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker0         : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
kubernetes-worker1         : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
manager                    : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel7                      : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   
rhel8                      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

(a2.10) [zaki@cloud-dev netbox (master)]$ 

この通り、Clusterがvm_networkのホストのみが処理対象となった。
(Clusterがoci_always_freeのホストが除外されている)

グループの複合条件

例えばcluster = vm_networkかつdevice_role = serverのホストをターゲットにしたい場合、cluster_vm_networkかつdevice_roles_serverとなる。
複数グループのandでターゲットホストを指定するには:&で繋げばOK

---
- name: dynamic inventory sample
  hosts: cluster_vm_network:&device_roles_server
  gather_facts: false

  tasks:
    - name: ping
      ping:

このターゲットホストの内容で実行すると、

(a2.10) [zaki@cloud-dev netbox (master)]$ ansible-playbook -i netbox-dynamic-inventory.yml netbox-dynamic-inventory-sample.yml -v

(中略)

PLAY RECAP ********************************************************************
manager                    : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
manager-dev                : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
registry                   : ok=0    changed=0    unreachable=1    failed=0    skipped=0    rescued=0    ignored=0   

この通り。

ORや否定の場合は以下の通り。

記述 内容
group1:group2 group1のホスト + group2のホスト (OR)
group1:!group2 group1のホストのうちgroup2には含まれないホスト
group1:&group2 group1のホストのうちgroup2にも含まれるホスト (AND)

docs.ansible.com


まとめ

NetBoxのダイナミックインベントリを使うことで、NetBoxに登録しているホスト情報をAnsibleのターゲットノードにすることを確認。

今回はClustersとDevice Rolesをフィルタに使ってみたが、group_byに指定できれば他にもtagtenantなどいろいろキーにして、自由にホスト情報をNetBoxから取り出すことができる。

E×celのIPアドレス管理台帳だと、こういったツール間連携は絶望的なので大抵の場合では手書きでアドレス情報を書き写したりする必要があったが、NetBoxとAPIを使って自動化に組み込むことが容易になる。


これまで、

とやってきて、今回はNetBoxをインプットとしてAnsibleを実行してみました。
どうでしょうか?
そろそろNetBoxを使ってみたくなったのではないでしょうか?(ニッコリ


環境

  • NetBox: 2.10.3 (on Docker Compose)
  • Ansible: 2.10.2 (Python3 on CentOS 7)
  • netbox.netbox collection: 1.1.0

参考情報

netbox-ansible-collection.readthedocs.io

docs.ansible.com

tekunabe.hatenablog.jp