うさラボ

お勉強と備忘録

Ansibleでリスト内辞書のデータをIPアドレス順にソートしたい

Ansible Advent Calendar 2023の4日目の記事です。

qiita.com

概要

リスト内辞書に定義されたIPアドレスの情報をKeyにしてソートしたかったのでやり方を考えてみました。

例えば下記のようなデータです

destination:
  - ip: 10.0.1.1
    name: 宛先2
  - ip: 2.0.1.1
    name: 宛先1

リストを2.0.1.1 -> 10.0.1.1の順番に並び替えをするようなものです。

環境

Python 3.9.16  
ansible [core 2.15.6]
jinja 3.1.2
netaddr 0.9.0
jmespath 1.0.1

手順

オリジナル

まずはsortフィルタのみを使って確認してみます。
用意したPlaybookは以下です、データip以外の要素は使わないので省略しています。

mapフィルタを利用しipの値のみを抜き出したリスト生成しソートしています

- name: 2023 Advent Carender
  hosts: localhost
  gather_facts: false
  tasks:
    - name: set sample data
      ansible.builtin.set_fact:
        data: 
          - ip: 10.0.0.1
          - ip: 10.0.0.11
          - ip: 1.0.0.1
          - ip: 2.0.0.1
          - ip: 2.0.1.1
          - ip: 0.0.0.2
          - ip: 0.0.0.1
          - ip: 0.0.0.10
          - ip: 255.255.255.255
          - ip: 0.0.1.161

    - name: original
      ansible.builtin.debug:
        msg: "{{ data | map(attribute='ip') | sort }}"

結果を見ると、文字列としてソートされているのIPアドレス順にはなっていません。
1.0.0.1のあとは2.0.1.1が来てほしいところです。

PLAY [2023 Advent Carender] ***************************************************************************************************************

TASK [set sample data] ********************************************************************************************************************
ok: [localhost]

TASK [original] ***************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        "0.0.0.1",
        "0.0.0.10",
        "0.0.0.2",
        "0.0.1.161",
        "1.0.0.1",
        "10.0.0.1",
        "10.0.0.11",
        "2.0.0.1",
        "2.0.1.1",
        "255.255.255.255"
    ]
}

PLAY RECAP ********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

カスタムフィルターを作る

IPアドレスをソートするためにまず思いつたのがカスタムフィルタを作るでした。
pythonの標準ライブラリであるipaddressを使えばIPアドレスを数値に変換することができるので、数値に変換後にソートすることで
IPアドレスをソートができると考えました

import ipaddress


def ip_sort(data):
    return sorted(data, key=lambda x: int(ipaddress.ip_address(x['ip'])))


class FilterModule(object):
    def filters(self):
        return {
            "ip_sort": ip_sort,
        }

カスタムフィルタはplaybookが存在する場所に作成したcustom_filterディレクトリに配置し ansible.cfgを以下のように設定しフィルタを利用可能にします

[defaults]
filter_plugins = custom_filter/

作成したPlaybookは以下です、ip_sortフィルタをかけた時点でソートが完了しているので その後mapフィルタでipの値をリストに抽出し表示します。

---
- name: 2023 Advent Carender Custom Filter
  hosts: localhost
  gather_facts: false
  tasks:
    - name: set sample data
      ansible.builtin.set_fact:
        data: 
          - ip: 10.0.0.1
          - ip: 10.0.0.11
          - ip: 1.0.0.1
          - ip: 2.0.0.1
          - ip: 2.0.1.1
          - ip: 0.0.0.2
          - ip: 0.0.0.1
          - ip: 0.0.0.10
          - ip: 255.255.255.255
          - ip: 0.0.1.161

    - name: custom filter
      ansible.builtin.debug:
        msg: "{{ data | ip_sort | map(attribute='ip') }}"

実行したところ、想定通りにソートされていることが確認できました。 やったね

PLAY [2023 Advent Carender Custom Filter] *************************************************************************************************

TASK [set sample data] ********************************************************************************************************************
ok: [localhost]

TASK [custom filter] **********************************************************************************************************************
ok: [localhost] => {
    "msg": [
        "0.0.0.1",
        "0.0.0.2",
        "0.0.0.10",
        "0.0.1.161",
        "1.0.0.1",
        "2.0.0.1",
        "2.0.1.1",
        "10.0.0.1",
        "10.0.0.11",
        "255.255.255.255"
    ]
}

PLAY RECAP ********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

ちなみに、IPアドレスを数値に変換すると0.0.0.0が0に255.255.255.255は4294967295になります。

ipaddrフィルタで頑張る

いきなり、カスタムフィルタを使って解決を目指してしまいましたが、Ansibleが提供している機能だけでどうにかできないか?も考えてみました Ansibleにはipaddrフィルタというものが存在しており、先ほどIPアドレスをIntに変換したのと同じことができます

---
- name: 2023 Advent Carender ipaddr filter
  hosts: localhost
  gather_facts: false
  tasks:
    - name: set sample data
      ansible.builtin.set_fact:
        data: 
          - ip: 10.0.0.1
          - ip: 10.0.0.11
          - ip: 1.0.0.1
          - ip: 2.0.0.1
          - ip: 2.0.1.1
          - ip: 0.0.0.2
          - ip: 0.0.0.1
          - ip: 0.0.0.10
          - ip: 255.255.255.255
          - ip: 0.0.1.161

    - name: ipaddr filter
      ansible.builtin.debug:
        msg:  "{{ data | map(attribute='ip') | ansible.utils.ipaddr('int') | sort | ansible.utils.ipaddr }}"

実行したところ、こちらも想定通りに並び替えができました(まずはこっち思いつきたかった)

PLAY [2023 Advent Carender JQ and ipaddr filter] ******************************************************************************************

TASK [set sample data] ********************************************************************************************************************
ok: [localhost]

TASK [ipaddr filter] *************************************************************************************************************************
ok: [localhost] => {
    "msg": [
        "0.0.0.1",
        "0.0.0.2",
        "0.0.0.10",
        "0.0.1.161",
        "1.0.0.1",
        "2.0.0.1",
        "2.0.1.1",
        "10.0.0.1",
        "10.0.0.11",
        "255.255.255.255"
    ]
}

PLAY RECAP ********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

おまけ

いままではIPの値の身を抽出した並び替えましたが、ip以外のkeyが定義されていて、そのKeyを取り出したい場合も考えてます。 keyとして追加したnameにはソート後の順番を書いてます、ソートしてnameを取り出したときに1~10まできれいに並べます。
selectattrフィルタで要素を抽出しています。

---
- name: 2023 Advent Carender ipaddr filter
  hosts: localhost
  gather_facts: false
  tasks:
    - name: set sample data
      ansible.builtin.set_fact:
        data: 
          - ip: 10.0.0.1
            name: 8
          - ip: 10.0.0.11
            name: 9
          - ip: 1.0.0.1
            name: 5
          - ip: 2.0.0.1
            name: 6
          - ip: 2.0.1.1
            name: 7
          - ip: 0.0.0.2
            name: 2
          - ip: 0.0.0.1
            name: 1
          - ip: 0.0.0.10
            name: 3
          - ip: 255.255.255.255
            name: 10
          - ip: 0.0.1.161
            name: 4

    - name: ipaddr filter
      ansible.builtin.debug:
        msg: "{{ element.name }}"
      loop: "{{ data | map(attribute='ip') | ansible.utils.ipaddr('int') | sort | ansible.utils.ipaddr }}"
      vars:
        element: "{{ data | selectattr('ip','eq',item) | first }}"

実行結果

PLAY [2023 Advent Carender ipaddr filter] *************************************************************************************************

TASK [set sample data] ********************************************************************************************************************
ok: [localhost]

TASK [ipaddr filter] **********************************************************************************************************************
ok: [localhost] => (item=0.0.0.1) => {
    "msg": "1"
}
ok: [localhost] => (item=0.0.0.2) => {
    "msg": "2"
}
ok: [localhost] => (item=0.0.0.10) => {
    "msg": "3"
}
ok: [localhost] => (item=0.0.1.161) => {
    "msg": "4"
}
ok: [localhost] => (item=1.0.0.1) => {
    "msg": "5"
}
ok: [localhost] => (item=2.0.0.1) => {
    "msg": "6"
}
ok: [localhost] => (item=2.0.1.1) => {
    "msg": "7"
}
ok: [localhost] => (item=10.0.0.1) => {
    "msg": "8"
}
ok: [localhost] => (item=10.0.0.11) => {
    "msg": "9"
}
ok: [localhost] => (item=255.255.255.255) => {
    "msg": "10"
}

PLAY RECAP ********************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0 

ほかにもjson_queryフィルタを使っても同じようなこともできますね。

    - name: json_query
      ansible.builtin.debug:
        msg: "{{ data | community.general.json_query('[*].ip') | ansible.utils.ipaddr('int') | sort | ansible.utils.ipaddr }}"

以上、小ネタでした~