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に対応している書式のテキストデータを扱う場合は簡単に処理できるのでオススメです。