Используя Ansible в качестве инструмента автоматизации, часто приходится сталкиваться с задачей обработки и фильтрации структурированных данных. Как правило, это набор фактов, полученных с управляемых серверов, или ответ на запрос к внешним API, которые возвращают данные в виде стандартного json. Многие неопытные инженеры, используя Ansible в таких случаях, начинают прибегать к помощи привычных консольных команд и начинают городить то, что среди специалистов получило название bashsible. В общем, вспоминается известный мем:
В обычных языках программирования задача обработки данных обычно решается с помощью циклов (for, while, for_each и т. п.) и различных функций преобразования типов объектов (массивы, коллекции, замыкания и т. п.) Ansible использует упрощенную модель данных, используя по сути лишь два варианта объекта с данными, список (list) и словарь (dictionary). Если вы не очень хорошо понимаете, что это такое и чем они отличаются, рекомендую для начала прочесть вот эту короткую статью.
Начнем с базовых примеров использования фильтров для работы со списками и словарями. Это позволит вам видеть, насколько мощными и гибкими могут быть эти инструменты в правильных руках.
На практике часто приходится иметь дело с фактами или результатом выполнения определенных модулей, которые представляют собой структуру в виде json/yaml. Например, давайте возьмем такой факт, как ansible_mounts и попробуем с ним поработать.
Попробуйте запустить у себя следующий плейбук, выводящий значения ansible_mounts:
--- - name: Test ansible_mounts hosts: localhost gather_facts: true connection: local tasks: - name: Show ansible_mounts debug: var: ansible_mounts
В выводе получится что-то вроде такого:
TASK [Show ansible_mounts] ********************************************* ok: [localhost] => ansible_mounts: - block_available: 9906494 block_size: 4096 block_total: 16305043 block_used: 6398549 device: /dev/sda3 fstype: ext4 inode_available: 3666700 inode_total: 4161536 inode_used: 494836 mount: / options: rw,relatime,errors=remount-ro size_available: 40576999424 size_total: 66785456128 uuid: e24606ee-2b07-4de0-a3c3-63c605f627ff - block_available: 0 block_size: 131072 block_total: 1 block_used: 1 device: /dev/loop0 fstype: squashfs inode_available: 0 inode_total: 29 inode_used: 29 mount: /snap/bare/5 options: ro,nodev,relatime,errors=continue,threads=single size_available: 0 size_total: 131072 uuid: N/A - block_available: 0 block_size: 131072 block_total: 507 block_used: 507 device: /dev/loop1 fstype: squashfs inode_available: 0 inode_total: 11906 inode_used: 11906 mount: /snap/core20/1822 options: ro,nodev,relatime,errors=continue,threads=single size_available: 0 size_total: 66453504 uuid: N/A ...
Давайте попробуем отфильтровать из этого длинного списка только те значения, для которых значение ключа device содержит /dev/sda. Это можно сделать с помощью фильтра selectattr и теста match:
--- - name: Show ansible_mounts filtered hosts: localhost gather_facts: true connection: local tasks: - name: Show ansible_mounts debug: var: ansible_mounts | selectattr('device', 'match', '/dev/sda')
Получим примерно такое:
TASK [Show ansible_mounts] ********************************* ok: [localhost] => ansible_mounts | selectattr('device', 'match', '/dev/sda'): - block_available: 9906486 block_size: 4096 block_total: 16305043 block_used: 6398557 device: /dev/sda3 fstype: ext4 inode_available: 3666696 inode_total: 4161536 inode_used: 494840 mount: / options: rw,relatime,errors=remount-ro size_available: 40576966656 size_total: 66785456128 uuid: e24606ee-2b07-4de0-a3c3-63c605f627ff - block_available: 9906486 block_size: 4096 block_total: 16305043 block_used: 6398557 device: /dev/sda3 fstype: ext4 inode_available: 3666696 inode_total: 4161536 inode_used: 494840 mount: /var/snap/firefox/common/host-hunspell options: ro,noexec,noatime,errors=remount-ro,bind size_available: 40576966656 size_total: 66785456128 uuid: e24606ee-2b07-4de0-a3c3-63c605f627ff - block_available: 129508 block_size: 4096 block_total: 131063 block_used: 1555 device: /dev/sda2 fstype: vfat inode_available: 0 inode_total: 0 inode_used: 0 mount: /boot/efi options: rw,relatime,fmask=0077,dmask=0077,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro size_available: 530464768 size_total: 536834048 uuid: 7041-A883
Теперь давайте предположим, что нам из всего набора ключей каждого словаря нужно только значение ключа mount. Как выбрать из полученного списка лишь значения определенного ключа? Тут нам на помощь придет фильтр map. Это довольно мощный фильтр, суть которого сводится к тому, что он применяет фильтр с аргументами, которые сами переданы ему в качестве аргументов, к каждому элементу списка словарей, которые приходят на вход. В простейшем случае, если нам нужно просто получить значение конкретного ключа из каждого элемента списка словарей, использование данного фильтра будет очень простым. Нужно просто указать значение нужного имени ключа в виде атрибута фильтра с соответствующей командой. В нашем случае это будет map(attribute='mount'). В результате получим следующий код:
--- - name: Show ansible_mounts filtered hosts: localhost gather_facts: true connection: local tasks: - name: Show mounts debug: var: >- ansible_mounts | selectattr('device', 'match', '/dev/sda') | map(attribute='mount')
В выводе получим следующее:
TASK [Show ansible_mounts] ***************************************
ok: [localhost] =>
? |-
ansible_mounts
| selectattr('device', 'match', '/dev/sda')
| map(attribute='mount')
: - /
- /var/snap/firefox/common/host-hunspell
- /boot/efi
Как видим, получить нужный нам набор данных оказывается весьма просто даже без использования программирования и циклов. Давайте усложним задачу. Скажем, нам нужно получить в выводе значения не только ключа mount, но также значения ключей size_available и size_total. Идущие в комплекте фильтры так не умеют. Фильтры Ansible умеют фильтровать списки и списки словарей, но не сами ключи словаря. Выход прост: нужно превратить словарь в список и уже его отфильтровать имеющимися инструментами. К счастью, в Ansible есть подходящие фильтры для такой задачи. С их помощью можно превращать словари в списки, а списки — обратно в словари. Называются эти фильтры, соответственно, dict2items и items2dict.
Например, у нас есть следующий словарь:
server_config: apache: version: "2.4" modules: ["mod_ssl", "mod_rewrite"] php: version: "7.4" extensions: ["curl", "json", "pdo"] mysql: version: "5.7" databases: ["db1", "db2", "db3"] system: os: "Ubuntu" os_version: "20.04"
И мы хотим отфильтровать только те элементы словаря, где есть ключ version. Сделать это мы сможем так. Сначала превращаем словать в список с помощью фильтра dict2items:
server_config | dict2items
Получим:
ok: [localhost] => server_config | dict2items: - key: apache value: modules: - mod_ssl - mod_rewrite version: '2.4' - key: php value: extensions: - curl - json - pdo version: '7.4' - key: mysql value: databases: - db1 - db2 - db3 version: '5.7' - key: system value: os: Ubuntu os_version: '20.04'
Теперь отфильтруем только те элементы списка, которые содержат дочерний ключ version:
server_config | dict2items | selectattr('value.version', 'defined')
Получим:
ok: [localhost] => server_config | dict2items | selectattr('value.version', 'defined'): - key: apache value: modules: - mod_ssl - mod_rewrite version: '2.4' - key: php value: extensions: - curl - json - pdo version: '7.4' - key: mysql value: databases: - db1 - db2 - db3 version: '5.7'
Теперь превратим обратно список в словарь исходного вида. Для этого добавим в конец конвейера фильтр items2dict. Получаем:
ok: [localhost] => server_config | dict2items | selectattr('value.version', 'defined') | items2dict: apache: modules: - mod_ssl - mod_rewrite version: '2.4' mysql: databases: - db1 - db2 - db3 version: '5.7' php: extensions: - curl - json - pdo version: '7.4'
Теперь давайте вспомним про нашу исходную задачу со списком словарей ansible_mounts из которого мы хотим извлечь только некоторые ключи. Сам список мы уже отфильтровали по нужному нам условию, теперь нам нужно выбрать только определенные ключи из списка. Получается, что к каждому элементу списка словарей нужно применить фильтр dict2items, потом отфильтровать этот список по списку ключей, а потом обратно превратить каждый дочерний список обратно в словарь. Сложно? На самом деле не очень. Как работать со словарем, мы уже видели на примере выше. Теперь нам нужно проделать то же самое со списком словарей. Тут нам как раз поможет упоминавшийся выше фильтр map, только уже в более продвинутом варианте применения. Покажем сразу итоговый результат. Код:
- name: Show mounts data debug: var: >- ansible_mounts | selectattr('device', 'match', '/dev/sda') | map('dict2items') | map('selectattr', 'key', 'in', ['mount', 'size_available', 'size_total']) | map('items2dict')
И результат:
ok: [localhost] => ? |- ansible_mounts | selectattr('device', 'match', '/dev/sda') | map('dict2items') | map('selectattr', 'key', 'in', ['mount', 'size_available', 'size_total']) | map('items2dict') : - mount: / size_available: 40576851968 size_total: 66785456128 - mount: /var/snap/firefox/common/host-hunspell size_available: 40576851968 size_total: 66785456128 - mount: /boot/efi size_available: 530464768 size_total: 536834048
Из чего состоит код в данном примере:
ansible_mounts | selectattr('device', 'match', '/dev/sda') selectattr для выбора тех элементов из списка ansible_mounts, у которых атрибут device соответствует регулярному выражению /dev/sda. Это позволяет выбрать информацию о монтировании только для определенного устройства. | map('dict2items') | map('selectattr', 'key', 'in', ['mount', 'size_available', 'size_total']) map совместно с selectattr, чтобы выбрать только те пары, ключи которых включают 'mount', 'size_available' или 'size_total'. | map('items2dict') map и фильтра items2dict.