Фильтры Ansible: превращаем сложное в простое

от автора

Используя Ansible в качестве инструмента автоматизации, часто приходится сталкиваться с задачей обработки и фильтрации структурированных данных. Как правило, это набор фактов, полученных с управляемых серверов, или ответ на запрос к внешним API, которые возвращают данные в виде стандартного json. Многие неопытные инженеры, используя Ansible в таких случаях, начинают прибегать к помощи привычных консольных команд и начинают городить то, что среди специалистов получило название bashsible. В общем, вспоминается известный мем:

не надо так!

не надо так!

В этой статье мы покажем, как легко и просто можно обрабатывать данные прямо в Ansible, используя его собственные мощные возможности. Речь идет о фильтрах Jinja2, которые представляют собой мощный, но в то же время интуитивно понятный инструмент для трансформации данных. Эти фильтры позволяют эффективно сортировать, выбирать и преобразовывать данные, устраняя необходимость в сложных внешних командах и скриптах.

В обычных языках программирования задача обработки данных обычно решается с помощью циклов (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

Из чего состоит код в данном примере:

  1. Выборка разделов диска:

    • ansible_mounts | selectattr('device', 'match', '/dev/sda')

    • Здесь используется фильтр selectattr для выбора тех элементов из списка ansible_mounts, у которых атрибут device соответствует регулярному выражению /dev/sda. Это позволяет выбрать информацию о монтировании только для определенного устройства.

  2. Преобразование словарей в списки элементов:

    • | map('dict2items')

    • Фильтр dict2items применяется к каждому элементу списка (каждому словарю), преобразуя его в список пар ключ-значение.

  3. Выборка определенных ключей:

    • | map('selectattr', 'key', 'in', ['mount', 'size_available', 'size_total'])

    • Далее, для каждого списка пар ключ-значение используется map совместно с selectattr, чтобы выбрать только те пары, ключи которых включают ‘mount’, ‘size_available’ или ‘size_total’.

  4. Преобразование обратно в словари:

    • | map('items2dict')

    • Наконец, после фильтрации нужных ключей, каждый список пар ключ-значение преобразуется обратно в словарь с помощью map и фильтра items2dict.

Заключение

Мы познакомились с наглядными примерами использования фильтров Ansible, и, надеюсь, теперь они кажутся вам не такими уж сложными. Ведь часто всё кажется сложным, пока не попробуешь. С фильтрами Ansible точно так же: попробуйте, поэкспериментируйте, и вы увидите, как они могут упростить вашу жизнь. А еще напишите в комментариях, хотели бы вы увидеть продолжение статьи с другими интересными примерами фильтров? Если тема будет интересна, то в следующих статьях попробую в том числе раскрыть тему написания своих собственных кастомных фильтров.


ссылка на оригинал статьи https://habr.com/ru/articles/778206/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *