Spec-Zone.ru › Ansible

Манипулирование данными

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

Начнём с краткого определения каждого типа плагинов:
  • поисковые запросы: в основном используются для запроса «внешних данных». В Ansible они служили основной частью циклов с конструкцией with_<lookup>, но могут использоваться и независимо для возврата данных на обработку. Обычно они возвращают список, поскольку их основная функция в циклах — та, что описана выше. Используются с операторами Jinja2 lookup или query.
  • фильтры: используются для изменения и преобразования данных, применяются с оператором Jinja2 |.
  • тесты: используются для проверки данных, применяются с оператором Jinja2 is.

Циклы и генераторы списков

В большинстве языков программирования есть циклы (for, while и так далее) и генераторы списков для преобразования списков, в том числе списков объектов. В Jinja2 есть несколько фильтров, предоставляющих такие возможности: map, select, reject, selectattr, rejectattr.

  • map: простой цикл for, позволяющий изменить каждый элемент списка; с помощью ключевого слова «attribute» можно выполнять преобразование на основе атрибутов элементов списка.
  • select/reject: цикл for с условием, позволяющий создать подмножество списка, включив или исключив элементы в зависимости от результата условия.
  • selectattr/rejectattr: очень похожи на описанные выше фильтры, но для условного выражения используют определённый атрибут элементов списка.

Используйте цикл для создания экспоненциальной задержки.

- name: try wait_for_connection up to 10 times with exponential delay
  ansible.builtin.wait_for_connection:
    delay: '{{ item | int }}'
    timeout: 1
  loop: '{{ range(1, 11) | map("pow", 2) }}'
  loop_control:
    extended: true
  ignore_errors: "{{ not ansible_loop.last }}"
  register: result
  when: result is not defined or result is failed

Извлечение из словаря ключей, соответствующих элементам списка

Эквивалентный код на Python:

chains = [1, 2]
for chain in chains:
    for config in chains_config[chain]['configs']:
        print(config['type'])

В Ansible это можно сделать несколькими способами; вот лишь один из примеров:

Способ извлечения соответствующих ключей из списка словарей
 tasks:
   - name: Show extracted list of keys from a list of dictionaries
     ansible.builtin.debug:
       msg: "{{ chains | map('extract', chains_config) | map(attribute='configs') | flatten | map(attribute='type') | flatten }}"
     vars:
       chains: [1, 2]
       chains_config:
           1:
               foo: bar
               configs:
                   - type: routed
                     version: 0.1
                   - type: bridged
                     version: 0.2
           2:
               foo: baz
               configs:
                   - type: routed
                     version: 1.0
                   - type: bridged
                     version: 1.1
Результаты задачи debug — список извлечённых ключей
   ok: [localhost] => {
       "msg": [
           "routed",
           "bridged",
           "routed",
           "bridged"
       ]
   }
Получение списка уникальных значений переменной, различающихся на разных узлах
   vars:
       unique_value_list: "{{ groups['all'] | map ('extract', hostvars, 'varname') | list | unique}}"

Поиск точки монтирования

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

Используйте selectattr, чтобы отфильтровать точки монтирования в список, который затем можно отсортировать и выбрать из него последний элемент
  - hosts: all
    gather_facts: True
    vars:
       path: /var/lib/cache
    tasks:
    - name: The mount point for {{path}}, found using the Ansible mount facts, [-1] is the same as the 'last' filter
      ansible.builtin.debug:
       msg: "{{(ansible_facts.mounts | selectattr('mount', 'in', path) | list | sort(attribute='mount'))[-1]['mount']}}"

Объединение значений из одного списка словарей

Объединив положительные и отрицательные фильтры из приведённых выше примеров, можно получить «значение, если оно существует» и «резервное значение», если его нет.

Используйте selectattr и rejectattr, чтобы при необходимости получить ansible_host или inventory_hostname
   - hosts: localhost
     tasks:
       - name: Check hosts in inventory that respond to ssh port
         wait_for:
           host: "{{ item }}"
           port: 22
         loop: '{{ has_ah + no_ah }}'
         vars:
           has_ah: '{{ hostvars|dictsort|selectattr("1.ansible_host", "defined")|map(attribute="1.ansible_host")|list }}'
           no_ah: '{{ hostvars|dictsort|rejectattr("1.ansible_host", "defined")|map(attribute="0")|list }}'

Пользовательский fileglob на основе переменной

В этом примере используется распаковка списка аргументов Python для создания пользовательского списка шаблонов fileglob на основе переменной.

Использование fileglob со списком на основе переменной.
  - hosts: all
    vars:
      mygroups:
        - prod
        - web
    tasks:
      - name: Copy a glob of files based on a list of groups
        copy:
          src: "{{ item }}"
          dest: "/tmp/{{ item }}"
        loop: '{{ q("fileglob", *globlist) }}'
        vars:
          globlist: '{{ mygroups | map("regex_replace", "^(.*)$", "files/\1/*.conf") | list }}'

Преобразования сложных типов

В Jinja есть фильтры для простых преобразований типов данных (int, bool и так далее), но преобразовывать структуры данных не так просто. Можно использовать циклы и генераторы списков, как показано выше, а также комбинировать и применять другие фильтры и поисковые запросы для выполнения более сложных преобразований.

Создание словаря из списка

В большинстве языков легко создать словарь (также известный как map, ассоциативный массив, хеш-таблица и так далее) из списка пар. В Ansible это можно сделать несколькими способами; выбор подходящего может зависеть от источника данных.

Эти примеры создают {"a": "b", "c": "d"}

Преобразование простого списка в словарь, предполагая, что список имеет вид [ключ, значение, ключ, значение, …]
 vars:
     single_list: [ 'a', 'b', 'c', 'd' ]
     mydict: "{{ dict(single_list[::2] | zip_longest(single_list[1::2])) }}"
Список пар позволяет сделать это проще:
 vars:
     list_of_pairs: [ ['a', 'b'], ['c', 'd'] ]
     mydict: "{{ dict(list_of_pairs) }}"

В обоих случаях получается одно и то же: zip_longest преобразует single_list в генератор list_of_pairs.

Более сложный вариант: используйте set_fact и loop, чтобы создать или обновить словарь парами ключ-значение из двух списков:

Использование set_fact для создания словаря из набора списков
    - name: Uses 'combine' to update the dictionary and 'zip' to make pairs of both lists
      ansible.builtin.set_fact:
        mydict: "{{ mydict | default({}) | combine({item[0]: item[1]}) }}"
      loop: "{{ (keys | zip(values)) | list }}"
      vars:
        keys:
          - foo
          - var
          - bar
        values:
          - a
          - b
          - c

В результате получится {"foo": "a", "var": "b", "bar": "c"}.

Можно также объединить эти простые примеры с другими фильтрами и поисковыми запросами, чтобы динамически создать словарь, сопоставляя шаблоны с именами переменных:

Использование «vars» для определения словаря из набора списков без необходимости создавать задачу
   vars:
       xyz_stuff: 1234
       xyz_morestuff: 567
       myvarnames: "{{ q('varnames', '^xyz_') }}"
       mydict: "{{ dict(myvarnames|map('regex_replace', '^xyz_', '')|list | zip(q('vars', *myvarnames))) }}"

Краткое пояснение: в этих двух строках есть что разобрать:

  • Поисковый запрос varnames возвращает список переменных, соответствующих шаблону «начинается с xyz_».
  • Затем список из предыдущего шага передаётся в поисковый запрос vars, чтобы получить список значений. * используется для «разыменования списка» (приём из Python, который работает и в Jinja); иначе список был бы воспринят как один аргумент.
  • Оба списка передаются фильтру zip, который объединяет их в единый список (ключ, значение, ключ2, значение2, …).
  • Затем функция dict использует этот «список пар» для создания словаря.

Пример использования фактов для поиска данных узла, удовлетворяющих условию X:

vars:
  uptime_of_host_most_recently_rebooted: "{{ansible_play_hosts_all | map('extract', hostvars, 'ansible_uptime_seconds') | sort | first}}"

Пример отображения времени работы узла в днях, часах, минутах и секундах (при условии, что факты были собраны).

- name: Show the uptime in days/hours/minutes/seconds
  ansible.builtin.debug:
   msg: Uptime {{ now().replace(microsecond=0) - now().fromtimestamp(now(fmt='%s') | int - ansible_uptime_seconds) }}

См. также

Использование фильтров для манипулирования данными

Фильтры Jinja2, включённые в Ansible

Тесты

Тесты Jinja2, включённые в Ansible

Документация Jinja2

Документация Jinja2 со списками основных фильтров и тестов

© 2012–2018 Michael DeHaan
© 2018–2025 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/projects/ansible/latest/playbook_guide/complex_data_manipulation.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API