zaki work log

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

[Ansible] jc parserのFilterを使った構造化データ化 (community.generalのフィルタ)

jc parserをAnsibleで使ってみる。

github.com

2021.07.06追記: community.generalコレクションのフィルタのドキュメントがいつのまにか追加されてた -> community.general Filter Guide — Ansible Documentation

jc parserについて

jc parserは、Ansibleで使用できるFilterの一つ。
jcで用意されているparserを指定することで、様々なLinuxコマンドの出力などを自動でparseし、構造化データに変換することができる。

Ansible 2.10だとcommunity.general collectionにFilterとして含まれる。

Community.General Plugin Index

ただ、Ansible DocumentationサイトではFilterのリファレンス(Filterのリスト)が無いため、2021.02.09時点でドキュメントはちょっと見当たらず。。 → 2021.07.06 現在、フィルターガイドというドキュメントが(6月頃に?)新設されていて、community.general コレクションで提供されるフィルタープラグインはここにまとめられている。

docs.ansible.com

ソースはplugins/filter/jc.pyで確認でき、コメントを見ればだいたい使い方はわかるはず。

また、jcそのものについては、jc parser本体のリポジトリに詳細がある

github.com

使えるparserの種類

これはjcのGitHubのリポジトリのREADMEのParsersの章を見るのが早い。
ここの一覧はCLIツールとしてのjcで使用するparserのリストになっているが、Ansibleでこれらのparser名をFilterの引数に指定して使う。

簡単な使い方

環境

確認した環境は以下の通り。

  • Ansible: 2.10.5
  • community.general: 1.3.5
  • jc: 1.14.1

準備

jcパッケージが必要なのでpipでインストールしておく。

$ pip install jc

ps

例えばps auxの結果を構造化データに変換したい場合。

    - name: jc parser
      vars:
        result: "{{ lookup('pipe', 'ps aux') }}"
      debug:
        msg: "{{ result | community.general.jc('ps') }}"

この内容のplaybookを実行すると、実行結果は以下の通り。

TASK [jc parser] **************************************************************
ok: [localhost] => {
    "msg": [
        {
            "command": "/usr/lib/systemd/systemd --switched-root --system --deserialize 22",
            "cpu_percent": 0.0,
            "mem_percent": 0.0,
            "pid": 1,
            "rss": 6048,
            "start": "2020",
            "stat": "Ss",
            "time": "9:40",
            "tty": null,
            "user": "root",
            "vsz": 194036
        },
        {
            "command": "[kthreadd]",
            "cpu_percent": 0.0,
            "mem_percent": 0.0,
            "pid": 2,
            "rss": 0,
            "start": "2020",
            "stat": "S",
            "time": "0:00",
            "tty": null,
            "user": "root",
            "vsz": 0
        },

        [...]

ちなみに、ps -efの結果でもちゃんとparseされる。
ただし、ps auxf見れる階層情報についてはparse対象外のため、プロセス名(command)の部分に階層表示のための|\\_が入り込む。

free

    - name: jc parser free
      vars:
        free: "{{ lookup('pipe', 'free') }}"
      debug:
        msg: "{{ free | community.general.jc('free') }}"
TASK [jc parser free] *********************************************************
ok: [localhost] => {
    "msg": [
        {
            "available": 3237832,
            "buff_cache": 3034204,
            "free": 528604,
            "shared": 27256,
            "total": 7990060,
            "type": "Mem",
            "used": 4427252
        },
        {
            "free": 3655420,
            "total": 3670012,
            "type": "Swap",
            "used": 14592
        }
    ]
}

freeは人の目に優しいhuman optionを付けてると(おそらく数値のみを拾ってるっぽくて)parseできない。
(オプション確認してhumanhだとわかったけど、ずっとhugehだと思ってた…)

$ free -h
              total        used        free      shared  buff/cache   available
Mem:           7.6G        4.2G        584M         26M        2.9G        3.2G
Swap:          3.5G         14M        3.5G

こうなる状態で、以下のplaybookを実行。

    - name: jc parser free
      vars:
        free: "{{ lookup('pipe', 'free -h') }}"
      debug:
        msg: "{{ free | community.general.jc('free') }}"

結果は以下の通り。

TASK [jc parser free] *********************************************************
ok: [localhost] => {
    "msg": [
        {
            "available": null,
            "buff_cache": null,
            "free": null,
            "shared": null,
            "total": null,
            "type": "Mem",
            "used": null
        },
        {
            "free": null,
            "total": null,
            "type": "Swap",
            "used": null
        }
    ]
}

他にもiptablesrouteなど、OSの設定系を構造化データとして取得することで加工・再利用しやすくなると思う。
ssはあるのに、ipは一覧に無さげなのは何故だろう。(なのでTTP使ったparseを試した)

2021.07.03 追記:

なるほど、ip -j afrom_jsonフィルタに突っ込んだりすれば簡単にオブジェクトに出来るから、jc parserが無くても大丈夫ですね。情報ありがとうございます!

ini

個人的に面白いと思ったのがiniのフィルタ。

[ansible]
command = ansible
filetype = yaml
name = playbook

[kubernetes]
command = kubectl
filetype = yaml
name = manifest

こんな形式のテキストデータに対して、

    - name: jc parser ini
      vars:
        ini: "{{ lookup('file', 'jc-sample.ini') }}"
      debug:
        msg: "{{ ini | community.general.jc('ini') }}"

iniを指定してフィルタをかますと、

ok: [localhost] => {
    "msg": {
        "ansible": {
            "command": "ansible",
            "filetype": "yaml",
            "name": "playbook"
        },
        "kubernetes": {
            "command": "kubectl",
            "filetype": "yaml",
            "name": "manifest"
        }
    }
}

このように、セクション名をキーにとしたディクショナリ型に変換してくれる。

kv (key / value)

セクション名の無い単なるkey=value形式のファイルの場合。

command = ansible
filetype = yaml
name = playbook

kvというparserもあるが、実はiniでもparseできる。
デリミタは=でなく:でもOK

command: ansible
filetype: yaml
name: playbook

ソース確認するとkvの中身は実はiniをコールしているだけ。

サンプル

github.com

まとめ

jc parserに対応している書式のテキストデータを扱う場合は簡単に処理できるのでオススメです。

[Ansible] cli_parseとTTP(Template Text Parser)を使った任意のテキストのparse処理おためし入門

Ansibleのcli_parseモジュールで、PythonのTTP(Template Text Parser)とテンプレートファイルを使ったテキスト解析を試してみた。

ttp.readthedocs.io

cli_parseは、2.10時点で標準のansible.netcommon.cli_parseと、Ansible 3(3.0.0b1)に新たに標準に含まれているansible.utils.cli_parseがある。
現状では中身は同じだが、ansible.utilsコレクション側でメンテされる(こっちに移管される?)のかな?

今回はひとまず標準で使用可能なansible.netcommon.cli_parseを使ってみる。
ansible.utils.cli_parseの場合はnetcommonの場所を読み替えればOK。(のハズ)

環境

  • Ansible: 2.10.5
  • ansible.netcommon 1.4.1
  • ttp: 0.6.0

お題

ip aの結果をparseしてみる。

1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 00:0c:29:22:86:2f brd ff:ff:ff:ff:ff:ff
    inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192
       valid_lft forever preferred_lft forever
    inet6 fe80::20c:29ff:fe22:862f/64 scope link 
       valid_lft forever preferred_lft forever
3: br-7a62608ea9c3: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:78:cd:77:68 brd ff:ff:ff:ff:ff:ff
    inet 172.18.0.1/16 brd 172.18.255.255 scope global br-7a62608ea9c3
       valid_lft forever preferred_lft forever
4: docker0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default 
    link/ether 02:42:12:db:d5:fd brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:12ff:fedb:d5fd/64 scope link 
       valid_lft forever preferred_lft forever
5: br-ec4e8609788c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:a0:58:03:b3 brd ff:ff:ff:ff:ff:ff
    inet 172.26.0.1/16 brd 172.26.255.255 scope global br-ec4e8609788c
       valid_lft forever preferred_lft forever
6: br-f28d1c1cb87b: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:03:19:f1:f8 brd ff:ff:ff:ff:ff:ff
    inet 172.19.0.1/16 brd 172.19.255.255 scope global br-f28d1c1cb87b
       valid_lft forever preferred_lft forever
7: br-11a5b9bc6a2c: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:aa:9c:c7:a8 brd ff:ff:ff:ff:ff:ff
    inet 172.20.0.1/16 brd 172.20.255.255 scope global br-11a5b9bc6a2c
       valid_lft forever preferred_lft forever
    inet6 fc00:f853:ccd:e793::1/64 scope global 
       valid_lft forever preferred_lft forever
    inet6 fe80::42:aaff:fe9c:c7a8/64 scope link 
       valid_lft forever preferred_lft forever
    inet6 fe80::1/64 scope link 
       valid_lft forever preferred_lft forever
8: br-1e7a6f36bebc: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:5f:6e:38:28 brd ff:ff:ff:ff:ff:ff
    inet 172.21.0.1/16 brd 172.21.255.255 scope global br-1e7a6f36bebc
       valid_lft forever preferred_lft forever
9: br-4221181b1c51: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:de:a0:d1:67 brd ff:ff:ff:ff:ff:ff
    inet 172.22.0.1/16 brd 172.22.255.255 scope global br-4221181b1c51
       valid_lft forever preferred_lft forever
44: br-d51584ee1064: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:83:4a:1d:18 brd ff:ff:ff:ff:ff:ff
    inet 172.23.0.1/16 brd 172.23.255.255 scope global br-d51584ee1064
       valid_lft forever preferred_lft forever
    inet6 fe80::42:83ff:fe4a:1d18/64 scope link 
       valid_lft forever preferred_lft forever
71: br-378c264dc6ee: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default 
    link/ether 02:42:21:be:06:4f brd ff:ff:ff:ff:ff:ff
    inet 172.24.0.1/16 brd 172.24.255.255 scope global br-378c264dc6ee
       valid_lft forever preferred_lft forever
    inet6 fe80::42:21ff:febe:64f/64 scope link 
       valid_lft forever preferred_lft forever
117: vethc853031@if116: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue master docker0 state UP group default 
    link/ether ea:d7:90:4c:62:be brd ff:ff:ff:ff:ff:ff link-netnsid 0
    inet6 fe80::e8d7:90ff:fe4c:62be/64 scope link 
       valid_lft forever preferred_lft forever

準備

ttp

TTPの利用にはPythonモジュールのttpが必要。
無いとエラーになる。

TASK [cli parse] **************************************************************
fatal: [localhost]: FAILED! => changed=false 
  msg: Failed to import the required Python library (ttp) on cloud-dev's Python /home/zaki/src/ansible-sample/venv/a2.10/bin/python3. Please read the module documentation and install it in the appropriate location. If the required library is installed, but Ansible is using the wrong Python interpreter, please consult the documentation on ansible_python_interpreter

$ pip install ttpする。

ansible.utils

ansible.netcommon.cli_parseでなくansible.utils.cli_parseを使用する場合は追加でコレクションをインストールする。

$ ansible-galaxy collection install ansible.utils

pip install ansibleでインストールしたAnsible環境であればansible.netcommon.cli_parseは標準で利用可能。

cli_parse

入力テキスト

cli_parseでテキストをparseする上で、よくあるケースとしては「ネットワークコマンドの実行結果のparse」で、これはcommandパラメタでコマンドを指定することで実現できる。
今回のip aについても同様にcommandで指定する。

    - name: cli parse
      ansible.netcommon.cli_parse:
        command: ip a

なお、すでにコマンド結果をテキストで保持しており、その変数を対象にparseしたい場合はtextで指定することもできる。

      ansible.netcommon.cli_parse:
        text: "{{ ip_a_result }}"

TTPの指定

parseのエンジンにTTPを指定する。
それ加えて、parse用のテンプレートファイルを指定する。

        parser:
          name: ansible.netcommon.ttp
          template_path: "./ttp-template/ip-a.ttp"

ここでは./ttp-template/ip-a.ttpにあるファイルを指定。

TTPテンプレートファイル

ip aの結果に対して何をピックアップしたいかをテンプレートファイルに定義する。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}

このテンプレートを使うことで、ip aの結果の以下の部分をparseすることができる。

2: ens192: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 ...
    inet 192.168.0.18/24 brd 192.168.0.255 ...

parseした結果は、{{ hogehoge }}で定義した箇所をhogehoge: 元テキストの値という構造化データとして扱うことができる。

ignore()

ttp.readthedocs.io

テンプレートファイルの定義は先頭一致ではなく行全体をマッチングするため、取り込みたい部分以降についても記載が必要。
その際、特に処理が不要であればignoreを指定すれば無視される。

ignoreはデフォルトで正規表現\Sにマッチでスペースは含まれないため、「行の残り全てを無視」であれば、正規表現.*を指定する。

サンプル実行

    - name: cli parse
      ansible.netcommon.cli_parse:
        command: ip a
        parser:
          name: ansible.netcommon.ttp
          template_path: "./ttp-template/ip-a.ttp"
      register: result
    - debug: print result
      debug:
        msg: "{{ result.parsed }}"

registerを使って結果を参照している。
これを実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - - interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'
      - addr: 172.17.0.1/16
        broadcast: 172.17.255.255
        interface: docker0
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '4'
      - addr: 172.26.0.1/16
        broadcast: 172.26.255.255
        interface: br-ec4e8609788c
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '5'
      - addr: 172.19.0.1/16
        broadcast: 172.19.255.255
        interface: br-f28d1c1cb87b
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '6'
      - addr: 172.20.0.1/16
        broadcast: 172.20.255.255
        interface: br-11a5b9bc6a2c
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '7'
      - addr: 172.21.0.1/16
        broadcast: 172.21.255.255
        interface: br-1e7a6f36bebc
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '8'
      - addr: 172.22.0.1/16
        broadcast: 172.22.255.255
        interface: br-4221181b1c51
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '9'
      - addr: 172.23.0.1/16
        broadcast: 172.23.255.255
        interface: br-d51584ee1064
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '44'
      - addr: 172.24.0.1/16
        broadcast: 172.24.255.255
        interface: br-378c264dc6ee
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '71'
      - interface: vethc853031@if116
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '117'

何となくいい感じに構造化データにできた。

テンプレートの書き方いろいろ

行内の項目が有ったり無かったり

例えばinetの行、ループバック(lo)の場合は以下の通り。

    inet 127.0.0.1/8 scope host lo

対して、ループバック以外のインタフェースについては以下の通り。

    inet 192.168.0.18/24 brd 192.168.0.255 scope global noprefixroute ens192

この通り、brd ブロードキャストアドレスの項目が有ったり無かったりする。

この場合は、無理に1行でテンプレートを定義せずに(というかわからなかった)、2行に分けると簡単に定義できる。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} {{ ignore(".*") }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

このテンプレートを使用すれば、parse結果は以下の通り。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'

        [...snip...]

この通り、ブロードキャストアドレスの記載の無いループバックのインタフェースも、inetの行のIPアドレスを取得できている。

重要なのはbrd ブロードキャストアドレスのテンプレートから先に記述すること。
ブロードキャストアドレスの定義がない行を先に書いてしまうと、全ての項目がこちらにマッチしてしまい、ブロードキャストアドレスのテンプレートまで処理が到達しない。

ただし、先頭行はこの方法は使えないので別の対処が必要。

先頭行の複数定義

テンプレートの先頭行を、

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}

と記述すると、ループバックとens192はマッチするが、残りのインタフェースはqlenの記載がないためそもそも処理対象にならない。
実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP

そしてこの場合、前述と同じように以下のように2行安直に書いても動作しない。(2行目の記述が機能しない)

※ ダメな例
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

これは、TTPのテンプレートにおける1行目の記述は「グループの開始」を表す意味も持っており、デフォルトでは1行目のみがその効果を持つ。
そこで、グループの開始の定義を複数行で行いたい場合は_start_識別子を使う。

{{ number }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: {{ mode }} mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

このように2行目の記述した定義に| _start_を追加(これは試した限り行内のどの変数に入れても大丈夫そう。ドキュメントへの記載は見当たらず)する。
このテンプレートで実行すると以下の通り、qlenの有るインタフェースも無いインタフェースもparseできている。

ok: [localhost] => 
  msg:
  - - - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: <LOOPBACK,UP,LOWER_UP>
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode: <NO-CARRIER,BROADCAST,MULTICAST,UP>
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN
      - addr: 172.17.0.1/16
        broadcast: 172.17.255.255
        group: default
        interface: docker0
        mode: <BROADCAST,MULTICAST,UP,LOWER_UP>
        mtu: '1500'
        number: '4'
        qdisc: noqueue
        state: UP

        [...snip...]

<>

{{ mode }}で取っている mode: <BROADCAST,MULTICAST,UP,LOWER_UP> の部分、普通に考えて<>を取り除きたいけど、単純に

{{ number }}: {{ interface }}: <{{ mode }}> mtu {{ mtu }} ...

のようにテンプレート内に<>を直接記述すると、

fatal: [localhost]: FAILED! => changed=false 
  msg: 'Template Text Parser returned an error while parsing. Error: not well-formed (invalid token): line 2, column 32'

とTTPの書式のエラーになってしまう。
これは、TTPのテンプレート内ではXML書式として<>がメタ文字として扱われるためで、たとえば1テンプレートファイルで複数の書式を記述したい場合に

<group name="interfaces">
{{ number }}: {{ interface }}: {{ mode }} ...
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

このような使い方をするため、TTPの処理で<>が期待する使われ方をしていないというエラーになっている。

回避策としてはHTMLの実体参照で記載する(これ見つけるのに半日かかった)。。。んだけど、これバグじゃね?って感じが少しするが、とりあえず現バージョン(ansible.netcommon 1.4.1 + ttp 0.6.0)だと以下の通りの動作。

ただし、HTML実体参照で動くというのもドキュメントには記載を見つけられなかったので、正しい使い方なのか未対応の処理を貫通した動作なのかは不明。

<group>で囲む

全体を<group name="...">などで囲む。

ttp.readthedocs.io

その上で、以下のように&lt;{{ mode }}&gt;と記載する。

<group name="interfaces">
{{ number }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

そうすると、結果の構造がnameで指定したキーの辞書型になるが、HTMLの実体参照を使って&lt;&gt;を指定したものが<>にマッチして正しく動く。

結果は以下の通り。

ok: [localhost] => 
  msg:
  - - interfaces:
      - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode: LOOPBACK,UP,LOWER_UP
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode: BROADCAST,MULTICAST,UP,LOWER_UP
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode: NO-CARRIER,BROADCAST,MULTICAST,UP
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN

        [...snip...]

直接HTML実体参照を記述

以下のように<group>などで囲まずに直接HTML実体参照を記述すると、

※ ダメな例
{{ number }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}

<>を直接使用した場合と同じくinvalid tokenのエラーとなる。
ここでちょっと小細工して、&をさらに実体参照で記述して、

{{ number }}: {{ interface }}: &amp;lt;{{ mode }}&gt; ...

と記述すると正しく動作する。
TTPの処理のソース見ればわかるかもしれないけどそこまではチェックしてないが、どうやら&を見つけたところから処理が変わってると思われ。
ちなみに&gt;の方はそのままでも動作するし、試した感じでは以下のように>と直接書いても大丈夫だった。

{{ number }}: {{ interface }}: &amp;lt;{{ mode }}> ...

紛らわしいので、現状では<group>で囲んだ方が良さそう。

部分的にsplit

<>を除いたらこの内部の,区切りの文字列をバラシてリストにしたいですが、はい、splitも利用可。

<group name="interfaces">
{{ number }}: {{ interface }}: &lt;{{ mode | split(",") }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }} qlen {{ qlen }}
{{ number | _start_ }}: {{ interface }}: &lt;{{ mode | split(",") }}&gt; mtu {{ mtu }} qdisc {{ qdisc }} state {{ state }} group {{ group }}
    inet {{ addr }} brd {{ broadcast }} {{ ignore(".*") }}
    inet {{ addr }} {{ ignore(".*") }}
</group>

これ実行すれば以下の通り。

ok: [localhost] => 
  msg:
  - - interfaces:
      - addr: 127.0.0.1/8
        group: default
        interface: lo
        mode:
        - LOOPBACK
        - UP
        - LOWER_UP
        mtu: '65536'
        number: '1'
        qdisc: noqueue
        qlen: '1000'
        state: UNKNOWN
      - addr: 192.168.0.18/24
        broadcast: 192.168.0.255
        group: default
        interface: ens192
        mode:
        - BROADCAST
        - MULTICAST
        - UP
        - LOWER_UP
        mtu: '1500'
        number: '2'
        qdisc: mq
        qlen: '1000'
        state: UP
      - addr: 172.18.0.1/16
        broadcast: 172.18.255.255
        group: default
        interface: br-7a62608ea9c3
        mode:
        - NO-CARRIER
        - BROADCAST
        - MULTICAST
        - UP
        mtu: '1500'
        number: '3'
        qdisc: noqueue
        state: DOWN

        [...snip...]

splitはドキュメントに記載がない。
joinはあるのに何故だろう。。

まとめ

ということで、自前のテンプレートを用意することで、Ansibleのcli_parseとTTPを使って任意の書式の解析と構造化データとしての取り込みはできそう。
なお、cli_parseはTTP以外にもtextfsmなどほかのparserを使用することができるので、そのうち試したい。

関連ドキュメント

netcommon.cli_parseのドキュメントはこちら。

docs.ansible.com

コレクションのGitHubプロジェクトのドキュメントはこちら。

github.com

github.com

構造化でデータのparseについて

docs.ansible.com

TTP(Template Text Parser)

ttp.readthedocs.io

てくなべcli_parse

tekunabe.hatenablog.jp

サンプルコード

github.com

Ansibleでsplitとjoinを使った文字列・配列操作

文字列を指定文字で分割して配列にバラすsplitと、配列を指定文字で結合して文字列に合成するjoinは、文字列と配列操作において大変有用。
主要なプログラミング言語には大抵この関数/メソッドは用意されており(※俺調べ)、フィルタ系の処理を書くときには重宝する。

qiita.com

Ansibleでこれらを活用するシチュエーションがどれくらいあるかはともかく、Ansibleでもsplitjoinを使うことができるのでその方法についてまとめ。
なお、環境はAnsible 2.10.5 時点。

split

Ansibleとしてはちょっとトリッキーかもしれないが、string型の変数に対してPythonstr#split()が使用できる。
時期バージョンではフィルターとしてsplitが使えるようになる模様

docs.python.org

  - name: split
    vars:
      string: 'tokyo, yokohama, fukuoka'
    debug:
      msg: "{{ string.split(',') }}"

実行結果

ok: [localhost] => 
  msg:
  - tokyo
  - ' yokohama'
  - ' fukuoka'

このとおり、,で分割して配列化できている。
が、,の後ろのスペースを除去できてないので配列要素にスペースが入っており綺麗な状態になっていない。

,の後ろの必ずスペース1文字しかないという書式であれば正規表現を使うまでもないが、そこが不明瞭であれば、やっぱり m/,\s*/ を使いたいところ。

Pythonstr#split()正規表現を直接指定できずreと連携する必要があるため、Ansibleから使うのはちょっと厳しい(やりかたがわからなかった)
なので一旦正規表現を使った文字列置換を一度行い、その後にsplitをすれば大体良い感じにできる。
(これは状況に因るので要件によって適切な方法を検討すること)

  - name: split
    vars:
      string: 'tokyo, yokohama, fukuoka,sapporo,  sendai, tokorozawa'
    debug:
      msg: "{{ (string | regex_replace(',\\s*', ',')).split(',') }}"

この例のように、対象文字列stringのデリミタで、,の後にスペースがあったりなかったり、あっても2つ以上のスペースだったり、、、という場合、regex_replaceを使って「綺麗に」してあげれば、実行結果は以下の通り。

ok: [localhost] => 
  msg:
  - tokyo
  - yokohama
  - fukuoka
  - sapporo
  - sendai
  - tokorozawa

余計なスペースが入っておらず、綺麗になった。

フィルターとしてのsplitについての情報はこちら。

github.com

join

Jinja2テンプレートのフィルタとして用意されているjoin()を使用できる。

jinja.palletsprojects.com

    - name: join sample
      vars:
        sample_items:
          - foo
          - bar
          - baz
      debug:
        msg: 
          - "{{ sample_items }}"
          - "{{ sample_items | join('<>') }}"

['foo', 'bar', 'baz']という配列に対して、joinを使って<>で連結」という内容。
実行すると以下の通り。

ok: [localhost] => 
  msg:
  - - foo
    - bar
    - baz
  - foo<>bar<>baz

まとめ

splitはファイルパスやURLの文字列を操作するためのsplit系フィルタは用意されてるが、任意の文字列に対する汎用処理を行うフィルタはない。ただし、Pythonのメソッドを使用することが可能。
joinはJinaj2のフィルタを使えばOK


splitjoinは(ログファイル解析などで)めっちゃ応用が利くので、インフラの人も正規表現とセットで覚えるべき。

[Ansible] intフィルターで16進数表記の文字列を基数変換

AnsibleではJinja2のint()フィルターで文字列を整数へ変換できるが、0xdeadbeafみたいな16進数表記の文字列をint(base=16)のようにパラメタを指定することで基数変換することもできる。

16進表記の文字列を比較する処理を使う機会があったのでまとめ。

確認した環境は以下の通り。

(a2.10) [zaki@cloud-dev filter (master)]$ ansible --version
ansible 2.10.5
  config file = /home/zaki/src/ansible-sample/filter/ansible.cfg
  configured module search path = ['/home/zaki/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
  ansible python module location = /home/zaki/src/ansible-sample/venv/a2.10/lib64/python3.6/site-packages/ansible
  executable location = /home/zaki/src/ansible-sample/venv/a2.10/bin/ansible
  python version = 3.6.8 (default, Nov 16 2020, 16:55:22) [GCC 4.8.5 20150623 (Red Hat 4.8.5-44)]

int(base=16)

16進数表記の文字列をintegerに変換。

  - name: hex to dec
    vars:
      hex_values:
      - '0xa'
      - '0x0a'
      - '0xff'
      - '0xffff'
      - '0xdeadbeaf'
      - '0b0'  # 0x0b0 
      - 'b'    # 0x0b
      - 'ff'   # 0xff
      - 'z'    # 0
      - '0o11' # 0
    debug:
      msg: "{{ item | int(base=16) }}"
    with_items: "{{ hex_values }}"

実行結果は以下の通り。

TASK [hex to dec] *************************************************************
ok: [localhost] => (item=0xa) => 
  msg: '10'
ok: [localhost] => (item=0x0a) => 
  msg: '10'
ok: [localhost] => (item=0xff) => 
  msg: '255'
ok: [localhost] => (item=0xffff) => 
  msg: '65535'
ok: [localhost] => (item=0xdeadbeaf) => 
  msg: '3735928495'
ok: [localhost] => (item=0b0) => 
  msg: '176'
ok: [localhost] => (item=b) => 
  msg: '11'
ok: [localhost] => (item=ff) => 
  msg: '255'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0o11) => 
  msg: '0'

16進表記になっていない文字列は0になっている。
2進数表記の0b00b01010101のような文字列を変換すると、2進数でなく0xがあるとみなされた16進数として処理されるので注意。

int(baes=8)

8進数表記の文字列をintegerに変換。

  - name: oct to dec
    vars:
      oct_values:
      - '0o11'
      - '77'   # 0o77 -> 63
      - 'z'    # 0
      - '0x0a' # 0
    debug:
      msg: "{{ item | int(base=8) }}"
    with_items: "{{ oct_values }}"

実行結果は以下の通り。

TASK [oct to dec] *************************************************************
ok: [localhost] => (item=0o11) => 
  msg: '9'
ok: [localhost] => (item=77) => 
  msg: '63'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0x0a) => 
  msg: '0'

そのまま。

int(base=2)

2進数表記の文字列をintegerに変換。

  - name: bin to dec
    vars:
      bin_values:
      - '0b11'
      - '101'  # 0b101 -> 5
      - 'z'    # 0
      - '0x0a' # 0
    debug:
      msg: "{{ item | int(base=2) }}"
    with_items: "{{ bin_values }}"

実行結果は以下の通り。

TASK [bin to dec] *************************************************************
ok: [localhost] => (item=0b11) => 
  msg: '3'
ok: [localhost] => (item=101) => 
  msg: '5'
ok: [localhost] => (item=z) => 
  msg: '0'
ok: [localhost] => (item=0x0a) => 
  msg: '0'

参考

jinja.palletsprojects.com

int(value, default=0, base=10)

You can also override the default base (10) in the second parameter, which handles input with prefixes such as 0b, 0o and 0x for bases 2, 8 and 16 respectively.

サンプル

github.com


(´-`).。oO(これintegerにしてるだけだし基数変換って言って良いのかな…

[Oracle Cloud] CLIツールのセットアップメモ

Oracle CloudをCLIで操作するためのツールのインストールとセットアップについて。
以前何事もなくセットアップしていたんだけど、新しい環境に入れようとしたらいろいろ(鍵設定周りで)詰まったのでメモ。
(全自動でAPIキー設定出来る方法があるかもしれないけど分からなかったので、公開鍵を手動で登録してる)

環境

[zaki@manager-dev ~]$ oci --version
2.20.0
[zaki@manager-dev ~]$ python3 --version
Python 3.6.8
[zaki@manager-dev ~]$ cat /etc/centos-release
CentOS Linux release 7.9.2009 (Core)

インストール

OCI CLIのインストールは以下。

docs.oracle.com

インストールにはPython3が必要なため、Python2しか入っていないLinux OSだとアップグレードするか?と聞かれる。事前にPython3を入れておくと無難。

bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"

デフォルトだと$HOME/lib$HOME/binへインストールされる。
$HOME/localとか使いたい場合はインストール時に対話的に(3か所)聞かれるのでパスを入力する。

[zaki@manager-dev ~]$ bash -c "$(curl -L https://raw.githubusercontent.com/oracle/oci-cli/master/scripts/install/install.sh)"
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 16053  100 16053    0     0  54223      0 --:--:-- --:--:-- --:--:-- 54416

    ******************************************************************************
    You have started the OCI CLI Installer in interactive mode. If you do not wish
    to run this in interactive mode, please include the --accept-all-defaults option.
    If you have the script locally and would like to know more about
    input options for this script, then you can run:
    ./install.sh -h
    If you would like to know more about input options for this script, refer to:
    https://github.com/oracle/oci-cli/blob/master/scripts/install/README.rst
    ******************************************************************************
Downloading Oracle Cloud Infrastructure CLI install script from https://raw.githubusercontent.com/oracle/oci-cli/v2.14.4/scripts/install/install.py to /tmp/oci_cli_install_tmp_zAYF.
######################################################################## 100.0%
Running install script.
python3 /tmp/oci_cli_install_tmp_zAYF 
-- Verifying Python version.
-- Python version 3.6.8 okay.

===> In what directory would you like to place the install? (leave blank to use '/home/zaki/lib/oracle-cli'): /home/zaki/local/oracle-cli/lib
-- Creating directory '/home/zaki/local/oracle-cli/lib'.
-- We will install at '/home/zaki/local/oracle-cli/lib'.

===> In what directory would you like to place the 'oci' executable? (leave blank to use '/home/zaki/bin'): /home/zaki/local/oracle-cli/bin
-- Creating directory '/home/zaki/local/oracle-cli/bin'.
-- The executable will be in '/home/zaki/local/oracle-cli/bin'.

===> In what directory would you like to place the OCI scripts? (leave blank to use '/home/zaki/bin/oci-cli-scripts'): /home/zaki/local/oracle-cli/bin/oci-cli-scripts
-- Creating directory '/home/zaki/local/oracle-cli/bin/oci-cli-scripts'.
-- The scripts will be in '/home/zaki/local/oracle-cli/bin/oci-cli-scripts'.

===> Currently supported optional packages are: ['db (will install cx_Oracle)']
What optional CLI packages would you like to be installed (comma separated names; press enter if you don't need any optional packages)?: 
-- The optional packages installed will be ''.
-- Trying to use python3 venv.
-- Executing: ['/usr/bin/python3', '-m', 'venv', '/home/zaki/local/oracle-cli/lib']
-- Executing: ['/home/zaki/local/oracle-cli/lib/bin/pip', 'install', '--upgrade', 'pip']
Cache entry deserialization failed, entry ignored
Collecting pip
  Downloading https://files.pythonhosted.org/packages/de/47/58b9f3e6f611dfd17fb8bd9ed3e6f93b7ee662fb85bdfee3565e8979ddf7/pip-21.0-py3-none-any.whl (1.5MB)
    100% |████████████████████████████████| 1.5MB 1.3MB/s 
Installing collected packages: pip
  Found existing installation: pip 9.0.3
    Uninstalling pip-9.0.3:
      Successfully uninstalled pip-9.0.3
Successfully installed pip-21.0

  [snip]

Successfully built PyYAML retrying terminaltables
Installing collected packages: pycparser, six, cffi, cryptography, pytz, python-dateutil, pyOpenSSL, configparser, certifi, terminaltables, retrying, PyYAML, oci, jmespath, click, arrow, oci-cli
Successfully installed PyYAML-5.1.2 arrow-0.17.0 certifi-2020.12.5 cffi-1.14.4 click-6.7 configparser-4.0.2 cryptography-3.2.1 jmespath-0.10.0 oci-2.29.0 oci-cli-2.20.0 pyOpenSSL-19.1.0 pycparser-2.20 python-dateutil-2.8.1 pytz-2020.5 retrying-1.3.3 six-1.14.0 terminaltables-3.1.0

===> Modify profile to update your $PATH and enable shell/tab completion now? (Y/n): y

===> Enter a path to an rc file to update (file will be created if it does not exist) (leave blank to use '/home/zaki/.bashrc'): 
-- Backed up '/home/zaki/.bashrc' to '/home/zaki/.bashrc.backup'
-- Tab completion set up complete.
-- If tab completion is not activated, verify that '/home/zaki/.bashrc' is sourced by your shell.
-- 
-- ** Run `exec -l $SHELL` to restart your shell. **
-- 
-- Installation successful.
-- Run the CLI with /home/zaki/local/oracle-cli/bin/oci --help
[zaki@manager-dev ~]$

.bashrcにPATH設定が追加されるのでsourceとかすればociが使えるようになる。
インストールできたら初期設定を行う。

初期設定

CLIをインストールしたら設定。
途中入力する各値は以下で確認できる。

ユーザーのOCIDを確認

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

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

テナンシのOCIDを確認

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

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

リージョンは

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

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

この辺で確認。

[zaki@manager-dev ~]$ oci setup config
    This command provides a walkthrough of creating a valid CLI config file.

    The following links explain where to find the information required by this
    script:

    User API Signing Key, OCID and Tenancy OCID:

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#Other

    Region:

        https://docs.cloud.oracle.com/Content/General/Concepts/regions.htm

    General config documentation:

        https://docs.cloud.oracle.com/Content/API/Concepts/sdkconfig.htm


Enter a location for your config [/home/zaki/.oci/config]: 
Enter a user OCID: ★ユーザーのOCIDを入力
Enter a tenancy OCID: ★テナンシのOCIDを入力
Enter a region by index or name(e.g.
1: ap-chiyoda-1, 2: ap-chuncheon-1, 3: ap-hyderabad-1, 4: ap-melbourne-1, 5: ap-mumbai-1,
6: ap-osaka-1, 7: ap-seoul-1, 8: ap-sydney-1, 9: ap-tokyo-1, 10: ca-montreal-1,
11: ca-toronto-1, 12: eu-amsterdam-1, 13: eu-frankfurt-1, 14: eu-zurich-1, 15: me-dubai-1,
16: me-jeddah-1, 17: sa-santiago-1, 18: sa-saopaulo-1, 19: uk-cardiff-1, 20: uk-gov-cardiff-1,
21: uk-gov-london-1, 22: uk-london-1, 23: us-ashburn-1, 24: us-gov-ashburn-1, 25: us-gov-chicago-1,
26: us-gov-phoenix-1, 27: us-langley-1, 28: us-luke-1, 29: us-phoenix-1, 30: us-sanjose-1): ★使用しているリージョンを入力
Do you want to generate a new API Signing RSA key pair? (If you decline you will be asked to supply the path to an existing key.) [Y/n]: y
Enter a directory for your keys to be created [/home/zaki/.oci]: 
Enter a name for your key [oci_api_key]: 
Public key written to: /home/zaki/.oci/oci_api_key_public.pem
Enter a passphrase for your private key (empty for no passphrase): 
Private key written to: /home/zaki/.oci/oci_api_key.pem
Fingerprint: ........
Config written to /home/zaki/.oci/config


    If you haven't already uploaded your API Signing public key through the
    console, follow the instructions on the page linked below in the section
    'How to upload the public key':

        https://docs.cloud.oracle.com/Content/API/Concepts/apisigningkey.htm#How2


[zaki@manager-dev ~]$ 

これでキーペアが作成される。

[zaki@manager-dev ~]$ ls -l ~/.oci/
合計 12
-rw-------. 1 zaki zaki  301  1月 31 11:02 config
-rw-------. 1 zaki zaki 1679  1月 31 11:02 oci_api_key.pem
-rw-------. 1 zaki zaki  451  1月 31 11:02 oci_api_key_public.pem

次は、作成された公開鍵(oci_api_key_public.pem)をクラウドに登録する。

公開鍵設定

docs.oracle.com

webの管理コンソールを開き、右上の「ユーザー設定」からユーザーのOCIDを確認したのと同じ画面下部にある「APIキー」の「APIキーの追加」で、作成した公開鍵(oci_api_key_public.pem)の中身(catなどで確認)を貼り付ける。

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

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

貼り付けるとプレビュー画面になるので、ダイアログを閉じれば公開鍵が登録される。
登録した公開鍵が有効になるまで若干のタイムラグがあるけど、認証できるようになれば、

[zaki@manager-dev ~]$ oci os ns get
{
  "data": "........"
}

という感じで、認証エラーにならず正常にレスポンスを得られる。

コマンド例

コンパートメントの一覧

$ oci iam compartment list

コンパートメント一覧から(1件目の)compartment-idをピックアップ

$ oci iam compartment list --query 'data[0]."compartment-id"' 

可用性ゾーン一覧

oci iam availability-domain list

使用可能イメージ一覧

$ oci compute image list -c $(oci iam compartment list --query 'data[0]."compartment-id"' | sed -e 's/"//g')

これは~/.oci/configに設定されてるコンパートメントID見てくれなくて(?)、-cオプションが必須みたい。

docs.oracle.com

docs.oracle.com

kind on Podman on RHEL8でK8sを入れて、Bookinfo on Istioが動くことを確認

先日のContainer Runtime Meetup #3で「Podmanでkind動かせるよ」という話になり、どういうわけか「Podmanではkindは(まだ)動かせない」と思い込んでいたため、実際に試してみました。

runtime.connpass.com

(少し前にRHEL環境で仕事してて動作確認用のKubernetesクラスタが必要になった時に「RHELだとDocker使えなくてPodmanになるけどそうするとkind使えないのでマネージドK8s使いましょう!」なんて言ってしまってスミマセン)

ちなみに、Docker Composeについては、Podman ver3.0から使える見込みのようです。

環境

[zaki@rhel8 ~]$ uname -a
Linux rhel8 4.18.0-240.10.1.el8_3.x86_64 #1 SMP Wed Dec 16 03:30:52 EST 2020 x86_64 x86_64 x86_64 GNU/Linux
[zaki@rhel8 ~]$ cat /etc/redhat-release 
Red Hat Enterprise Linux release 8.3 (Ootpa)
  • Podman 2.0.5
  • kind 0.9.0
  • MetalLB 0.9.5
  • Istio 1.8.2

Podmanのインストール

[zaki@rhel8 ~]$ sudo dnf install podman
Updating Subscription Management repositories.
メタデータの期限切れの最終確認: 0:20:09 時間前の 2021年01月28日 08時17分37秒 に実施しました。
依存関係が解決しました。
==================================================================================================================================================
 パッケージ                         Arch          バージョン                                        リポジトリー                            サイズ
==================================================================================================================================================
インストール:
 podman                             x86_64        2.0.5-5.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         13 M
依存関係のインストール:
 conmon                             x86_64        2:2.0.20-2.module+el8.3.0+8221+97165c3f           rhel-8-for-x86_64-appstream-rpms         49 k
 containernetworking-plugins        x86_64        0.8.6-2.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         20 M
 containers-common                  x86_64        1:1.1.1-3.module+el8.3.0+8221+97165c3f            rhel-8-for-x86_64-appstream-rpms         66 k
 criu                               x86_64        3.14-2.module+el8.3.0+8221+97165c3f               rhel-8-for-x86_64-appstream-rpms        500 k
 fuse-overlayfs                     x86_64        1.1.2-3.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         67 k
 fuse3-libs                         x86_64        3.2.1-12.el8                                      rhel-8-for-x86_64-baseos-rpms            94 k
 libnet                             x86_64        1.1.6-15.el8                                      rhel-8-for-x86_64-appstream-rpms         67 k
 libslirp                           x86_64        4.3.1-1.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         69 k
 libvarlink                         x86_64        18-3.el8                                          rhel-8-for-x86_64-baseos-rpms            44 k
 podman-catatonit                   x86_64        2.0.5-5.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms        308 k
 protobuf-c                         x86_64        1.3.0-4.el8                                       rhel-8-for-x86_64-appstream-rpms         37 k
 runc                               x86_64        1.0.0-68.rc92.module+el8.3.0+8221+97165c3f        rhel-8-for-x86_64-appstream-rpms        3.7 M
 slirp4netns                        x86_64        1.1.4-2.module+el8.3.0+8221+97165c3f              rhel-8-for-x86_64-appstream-rpms         50 k
弱い依存関係のインストール:
 container-selinux                  noarch        2:2.144.0-1.module+el8.3.0+8221+97165c3f          rhel-8-for-x86_64-appstream-rpms         49 k
モジュールストリームの有効化中:
 container-tools                                  rhel8                                                                                          

トランザクションの概要
==================================================================================================================================================
インストール  15 パッケージ

ダウンロードサイズの合計: 39 M
インストール済みのサイズ: 133 M
これでよろしいですか? [y/N]: 
[zaki@rhel8 ~]$ podman version
Version:      2.0.5
API Version:  1
Go Version:   go1.14.7
Built:        Wed Sep 23 12:18:02 2020
OS/Arch:      linux/amd64

インストールされたので一応挨拶。

[zaki@rhel8 kind]$ podman ps -a
CONTAINER ID  IMAGE   COMMAND  CREATED  STATUS  PORTS   NAMES
[zaki@rhel8 kind]$ podman run hello-world
Trying to pull registry.access.redhat.com/hello-world...
  name unknown: Repo not found
Trying to pull registry.redhat.io/hello-world...
  unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
Trying to pull docker.io/library/hello-world...
Getting image source signatures
Copying blob 0e03bdcc26d7 done  
Copying config bf756fb1ae done  
Writing manifest to image destination
Storing signatures

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ podman ps -a
CONTAINER ID  IMAGE                                 COMMAND  CREATED         STATUS                     PORTS   NAMES
8847ed15d8b3  docker.io/library/hello-world:latest  /hello   30 seconds ago  Exited (0) 29 seconds ago          gracious_pare

何も考えずに実行したけど、root権限あるいはグループ設定無くても動くというか、この場合はrootlessモードで動作し、ホストOS上ではpodmanを実行しているユーザーの実行権限で動作する。

Podmanのrootlessモード

Podmanを使ってalpineコンテナでコマンドを実行した場合。

[zaki@rhel8 kind]$ podman run -it alpine sh
Trying to pull registry.access.redhat.com/alpine...
  name unknown: Repo not found
Trying to pull registry.redhat.io/alpine...
  unable to retrieve auth token: invalid username/password: unauthorized: Please login to the Red Hat Registry using your Customer Portal credentials. Further instructions can be found here: https://access.redhat.com/RegistryAuthentication
Trying to pull docker.io/library/alpine...
Getting image source signatures
Copying blob 4c0d98bf9879 done  
Copying config e50c909a8d done  
Writing manifest to image destination
Storing signatures
/ # id
uid=0(root) gid=0(root)
/ # ps
PID   USER     TIME  COMMAND
    1 root      0:00 sh
    7 root      0:00 ps
/ # tail -f /dev/null

コンテナ内ではuid=0で動いているが、ホストOS上でpsで確認すると、

[zaki@rhel8 kind]$ ps auxf
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

...

zaki      405746  0.0  0.0 143748  2664 ?        Ssl  23:17   0:00 /usr/bin/conmon --api-version 1 -c d ...
zaki      405756  0.0  0.0   1660  1004 pts/0    Ss   23:17   0:00  \_ sh
zaki      406164  0.0  0.0   1584     4 pts/0    S+   23:20   0:00      \_ tail -f /dev/null

このように、実行ユーザー権限で動作している。

一方、同じことをDocker(v20.10.2)でやると、

(a2.10) [zaki@cloud-dev ~]$ docker run -it alpine sh
/ # id
uid=0(root) gid=0(root) groups=0(root),1(bin),2(daemon),3(sys),4(adm),6(disk),10(wheel),11(floppy),20(dialout),26(tape),27(video)
/ # tail -f /dev/null

コンテナ内ではPodmanの場合と同じようにuid=0で動いているが、ホストOS上でpsすると、

[zaki@cloud-dev ~]$ ps auxf 
USER         PID %CPU %MEM    VSZ   RSS TTY      STAT START   TIME COMMAND

...

root     1699148  0.0  0.0 113244  4900 ?        Sl   13:27   0:00 /usr/bin/containerd-shim-runc-v2 -names ...
root     1699168  0.1  0.0   1636   512 pts/0    Ss   13:27   0:00  \_ sh
root     1699219  0.0  0.0   1560   244 pts/0    S+   13:27   0:00      \_ tail -f /dev/null

root権限で動作している。
(このホスト上では、実行ユーザーはdockerグループに所属させてsudoなしで実行できるように設定している)

kindのインストール

kind.sigs.k8s.io

CLIのインストール

kindのインストール自体は通常通り。

[zaki@rhel8 ~]$ mkdir -p local/kind
[zaki@rhel8 ~]$ cd $_
[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.9.0/kind-linux-amd64
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    97  100    97    0     0    243      0 --:--:-- --:--:-- --:--:--   243
100   642  100   642    0     0    825      0 --:--:-- --:--:-- --:--:--  1893
100 7247k  100 7247k    0     0   928k      0  0:00:07  0:00:07 --:--:-- 1408k
[zaki@rhel8 kind]$ chmod +x ./kind
[zaki@rhel8 kind]$ sudo mv ./kind /usr/local/bin/
[zaki@rhel8 kind]$ 
[zaki@rhel8 kind]$ kind version
kind v0.9.0 go1.15.2 linux/amd64

K8sクラスタのデプロイ

せっかくなのでマルチノードで。

[zaki@rhel8 kind]$ cat multinode.yaml 
# three node (two workers) cluster config
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

これをパラメタに、

KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yamlを実行…できればよかったんだけど、

[zaki@rhel8 kind]$ KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yaml
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
podman provider does not work properly in rootless mode

Podmanのrootlessモードだとkindは動作しない模様。。 ということで、「『kindを使う場合は』podman実行時にroot権限が必要」という解釈でいいのかな?sudo付けて実行してみる。

[zaki@rhel8 kind]$ sudo KIND_EXPERIMENTAL_PROVIDER=podman kind create cluster --config multinode.yaml
using podman due to KIND_EXPERIMENTAL_PROVIDER
enabling experimental podman provider
Creating cluster "kind" ...
 ✓ Ensuring node image (kindest/node:v1.19.1) 🖼
 ✓ Preparing nodes 📦 📦 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
 ✓ Joining worker nodes 🚜 
Set kubectl context to "kind-kind"
You can now use your cluster with:

kubectl cluster-info --context kind-kind

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

OKです。 自動生成されるクレデンシャル情報ファイル(kubeconfigファイル)は、sudoでrootユーザーで動作した影響でrootの$HOME/.kube/configにあるので、通常ユーザーの$HOME/.kube/configにもクレデンシャル情報を作成する。

[zaki@rhel8 kind]$ mkdir ~/.kube
[zaki@rhel8 kind]$ sudo kind get kubeconfig > ~/.kube/config
enabling experimental podman provider

あ、これもしかしてKIND_EXPERIMENTAL_PROVIDER=podmanは設定しなくても自動認識するのかも?(未確認)

[zaki@rhel8 kind]$ kubectl get node -o wide
NAME                 STATUS   ROLES    AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                                     KERNEL-VERSION                 CONTAINER-RUNTIME
kind-control-plane   Ready    master   2m3s   v1.19.1   10.88.0.12    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0
kind-worker          Ready    <none>   90s    v1.19.1   10.88.0.13    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0
kind-worker2         Ready    <none>   90s    v1.19.1   10.88.0.11    <none>        Ubuntu Groovy Gorilla (development branch)   4.18.0-240.10.1.el8_3.x86_64   containerd://1.4.0

できました。

Bookinfo

クラスターが動いてることの確認といえばWordPressあたりが妥当だけど、せっかく(何がせっかくなんだろう)なので、MetalLBとIstio入れてBookinfoをデプロイしてみる。
Istioの動作にMetalLB(type:LoadBalancer Service)は必須ではないけど、デフォルトでデプロイされるIngressGateway Serviceがtype:LoadBalancerに設定されてるので、用意されてるものをそのまま動くようにするために使う感じ。

MetalLB

metallb.universe.tf

以前もやってるし、手順は同じ。
ただし、2021.01.30時点で、バージョン v0.9.5 になってる。

まず本体のデプロイのため以下実行。

kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/namespace.yaml
kubectl apply -f https://raw.githubusercontent.com/metallb/metallb/v0.9.5/manifests/metallb.yaml
# On first install only
kubectl create secret generic -n metallb-system memberlist --from-literal=secretkey="$(openssl rand -base64 128)"

次にtype:LoadBalancer Serviceに割り当てるIPアドレスの設定。

…さて、そういえばPodmanで動いてるコンテナのIPアドレスってどうなってるんだろう?

使用状況はDockerと同じくpodman inspectで確認できた。

[zaki@rhel8 kind]$ sudo podman ps --quiet | sudo xargs podman inspect -f "{{.Name}} {{.NetworkSettings.IPAddress }}"
kind-worker2         10.88.0.11
kind-worker          10.88.0.13
kind-control-plane   10.88.0.12

使用中のコンテナネットワークは以下。

[zaki@rhel8 kind]$ sudo podman network ls
NAME    VERSION  PLUGINS
podman  0.4.0    bridge,portmap,firewall,tuning

サブネット(sudo podman network inspect podmanの該当箇所)を取り出すと、

[zaki@rhel8 kind]$ sudo podman network inspect podman -f '{{(index (index (index .plugins 0).ipam.ranges 0) 0).subnet}}'
10.88.0.0/16

こんな感じ?
(Go Templateぜんぜんわからん。特に配列)

/16なんで第3オクテットを大きい値にしておけばよさげ。(適当です)

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: metallb-system
  name: config
data:
  config: |
    address-pools:
    - name: default
      protocol: layer2
      addresses:
      - 10.88.250.10-10.88.250.20

この内容でConfigMap作成。

[zaki@rhel8 metallb]$ sudo kubectl apply -f config.yaml 
configmap/config created

これでtype:LoadBalancer Serviceをデプロイしたときに、ここで設定したIPアドレスが自動でExternal-IPに割り当てられる。

Istio

istio.io

Istioのインストールも以前やったときとほぼ同じ。
2021.01.30時点でバージョンは1.8.2になっている。

[zaki@rhel8 istio]$ curl -L https://istio.io/downloadIstio | sh -
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   102  100   102    0     0    228      0 --:--:-- --:--:-- --:--:--   228
100  4579  100  4579    0     0   5536      0 --:--:-- --:--:-- --:--:-- 37227

Downloading istio-1.8.2 from https://github.com/istio/istio/releases/download/1.8.2/istio-1.8.2-linux-amd64.tar.gz ...

Istio 1.8.2 Download Complete!

Istio has been successfully downloaded into the istio-1.8.2 folder on your system.

Next Steps:
See https://istio.io/latest/docs/setup/install/ to add Istio to your Kubernetes cluster.

To configure the istioctl client tool for your workstation,
add the /home/zaki/local/istio/istio-1.8.2/bin directory to your environment path variable with:
         export PATH="$PATH:/home/zaki/local/istio/istio-1.8.2/bin"

Begin the Istio pre-installation check by running:
         istioctl x precheck 

Need more information? Visit https://istio.io/latest/docs/setup/install/ 

istioctl/usr/local/binに配置。

[zaki@rhel8 istio]$ sudo cp -a istio-1.8.2/bin/istioctl /usr/local/bin/
[zaki@rhel8 istio]$ 
[zaki@rhel8 istio]$ istioctl version
no running Istio pods in "istio-system"
1.8.2

demoプロファイルでインストール。

[zaki@rhel8 istio]$ istioctl install --set profile=demo -y
Detected that your cluster does not support third party JWT authentication. Falling back to less secure first party JWT. See https://istio.io/v1.8/docs/ops/best-practices/security/#configure-third-party-service-account-tokens for details.
✔ Istio core installed
✔ Istiod installed
✔ Egress gateways installed
✔ Ingress gateways installed
✔ Installation complete
[zaki@rhel8 istio]$ kubectl get pod,svc -n istio-system 
NAME                                        READY   STATUS    RESTARTS   AGE
pod/istio-egressgateway-c9c55457b-tpj6k     1/1     Running   0          3m6s
pod/istio-ingressgateway-865d46c7f5-9gsc6   1/1     Running   0          3m6s
pod/istiod-7f785478df-svc62                 1/1     Running   0          3m39s

NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP    PORT(S)                                                                      AGE
service/istio-egressgateway    ClusterIP      10.96.125.248   <none>         80/TCP,443/TCP,15443/TCP                                                     3m6s
service/istio-ingressgateway   LoadBalancer   10.96.221.246   10.88.250.10   15021:32424/TCP,80:30905/TCP,443:32177/TCP,31400:31323/TCP,15443:31192/TCP   3m6s
service/istiod                 ClusterIP      10.96.29.215    <none>         15010/TCP,15012/TCP,443/TCP,15014/TCP                                        3m38s

デプロイできました。
(type:LoadBalancerが設定されているIstio IngressGatewayも設定どおり10.88.250.*のアドレスが付いている)

Bookinfo本体

namespaceとlabel設定

[zaki@rhel8 istio]$ kubectl create namespace bookinfo
namespace/bookinfo created
[zaki@rhel8 istio]$ kubectl label namespaces bookinfo istio-injection=enabled
namespace/bookinfo labeled

Bookinfoのデプロイ

[zaki@rhel8 istio]$ kubectl apply -f istio-1.8.2/samples/bookinfo/platform/kube/bookinfo.yaml -n bookinfo 
service/details created
serviceaccount/bookinfo-details created
deployment.apps/details-v1 created
service/ratings created
serviceaccount/bookinfo-ratings created
deployment.apps/ratings-v1 created
service/reviews created
serviceaccount/bookinfo-reviews created
deployment.apps/reviews-v1 created
deployment.apps/reviews-v2 created
deployment.apps/reviews-v3 created
service/productpage created
serviceaccount/bookinfo-productpage created
deployment.apps/productpage-v1 created
[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS            RESTARTS   AGE
details-v1-79c697d759-kllz9       0/2     PodInitializing   0          25s
productpage-v1-65576bb7bf-w7h79   0/2     Init:0/1          0          24s
ratings-v1-7d99676f7f-kschh       0/2     Init:0/1          0          25s
reviews-v1-987d495c-59hh6         0/2     PodInitializing   0          25s
reviews-v2-6c5bf657cf-l6q49       0/2     PodInitializing   0          25s
reviews-v3-5f7b9f4f77-q2wnn       0/2     Init:0/1          0          25s

この通りデプロイが始まるのでしばらく待つ。
(数十秒レベルでは完了せず、5分近く時間かかった)

[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS            RESTARTS   AGE
details-v1-79c697d759-kllz9       2/2     Running           0          4m10s
productpage-v1-65576bb7bf-w7h79   0/2     PodInitializing   0          4m9s
ratings-v1-7d99676f7f-kschh       0/2     PodInitializing   0          4m10s
reviews-v1-987d495c-59hh6         2/2     Running           0          4m10s
reviews-v2-6c5bf657cf-l6q49       1/2     Running           0          4m10s
reviews-v3-5f7b9f4f77-q2wnn       0/2     PodInitializing   0          4m10s
[zaki@rhel8 istio]$ kubectl get pod -n bookinfo 
NAME                              READY   STATUS    RESTARTS   AGE
details-v1-79c697d759-kllz9       2/2     Running   0          5m2s
productpage-v1-65576bb7bf-w7h79   2/2     Running   0          5m1s
ratings-v1-7d99676f7f-kschh       2/2     Running   0          5m2s
reviews-v1-987d495c-59hh6         2/2     Running   0          5m2s
reviews-v2-6c5bf657cf-l6q49       2/2     Running   0          5m2s
reviews-v3-5f7b9f4f77-q2wnn       2/2     Running   0          5m2s

全Podが2/2でRunningになったのを確認。

外部アクセス設定(Gateway + VirtualService)

[zaki@rhel8 istio]$ kubectl apply -f istio-1.8.2/samples/bookinfo/networking/bookinfo-gateway.yaml -n bookinfo 
gateway.networking.istio.io/bookinfo-gateway created
virtualservice.networking.istio.io/bookinfo created

HTTPアクセス

[zaki@rhel8 istio]$ curl http://10.88.250.10/productpage
<!DOCTYPE html>
<html>
  <head>
    <title>Simple Bookstore App</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
:
:

アクセスOK

ただしこの10.88.0.0/16、Podmanのネットワーク上のアドレスなので、ホストOSの外部からはアクセスできない。
外からアクセスしたい場合、DockerやPodman単体であれば-pでポートをpublishすればポートフォワードされるけど、kindの場合はextraPortMappingsを使えば良い。
が、今回この設定は入れなかったので、sshのポートフォワードで回避。

ブラウザを使うローカルPCからkindを動かしているホスト(ここではrhel8というホスト)へ-L ローカルでlistenするポート:リモート上でアクセスするホスト:リモート上でアクセスするポートを追加してsshログイン。

PS C:\Users\zaki> ssh rhel8 -L 25080:10.88.250.10:80
Activate the web console with: systemctl enable --now cockpit.socket

This system is not registered to Red Hat Insights. See https://cloud.redhat.com/
To register this system, run: insights-client --register

Last login: Sat Jan 30 01:35:15 2021 from 192.168.0.10
[zaki@rhel8 ~]$

この状態で、ブラウザでhttp://localhost:25080/productpageにアクセス。
(パスの/productpageはBookinfoのVirtualServiceによるもの)

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

関連情報

rheb.hatenablog.com

www.slideshare.net

qiita.com

rheb.hatenablog.com

medium.com

[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