Циклы
Часто вам потребуется выполнить много действий в одной задаче, например, создать много пользователей, установить много пакетов или повторить шаг опроса до достижения определенного результата.
В этой главе рассказывается о том, как использовать циклы в плейбуках.
- Стандартные циклы
- Сложные циклы
- Использование lookup и query с циклом
- Циклы «до тех пор, пока»
- Использование register с циклом
- Перебор инвентаризации
- Управление циклом
- Миграция с with_X на цикл
Стандартные циклы
Для экономии набора текста повторяющиеся задачи могут быть записаны в сокращенной форме следующим образом:
- name: add several users
user:
name: "{{ item }}"
state: present
groups: "wheel"
loop:
- testuser1
- testuser2
Если вы определили список YAML в файле переменных или в разделе «vars», вы также можете сделать следующее:
loop: "{{ somelist }}"
Вышеприведенное эквивалентно:
- name: add user testuser1
user:
name: "testuser1"
state: present
groups: "wheel"
- name: add user testuser2
user:
name: "testuser2"
state: present
groups: "wheel"
Примечание
До версии 2.5 Ansible в основном использовал ключевые слова with_<lookup> для создания циклов, ключевое слово loop в основном аналогично ключевому слову with_list.
Некоторые плагины, такие как модули yum и apt, могут напрямую принимать списки в свои параметры. Это более эффективно, чем перебор задач. Обратитесь к документации каждого действия для получения подробностей. Вот пример:
- name: optimal yum
yum:
name: "{{list_of_packages}}"
state: present
- name: non optimal yum, not only slower but might cause issues with interdependencies
yum:
name: "{{item}}"
state: present
loop: "{{list_of_packages}}"
Обратите внимание, что типы элементов, по которым вы итерируетесь, не обязательно должны быть простыми списками строк. Если у вас есть список хешей, вы можете ссылаться на подключаемые ключи, используя такие конструкции:
- name: add several users
user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
loop:
- { name: 'testuser1', groups: 'wheel' }
- { name: 'testuser2', groups: 'root' }
Также следует иметь в виду, что при комбинировании условных выражений с циклом оператор when: обрабатывается отдельно для каждого элемента. См. Оператор When для примера.
Для перебора словаря используйте dict2items Фильтр словаря:
- name: create a tag dictionary of non-empty tags
set_fact:
tags_dict: "{{ (tags_dict|default({}))|combine({item.key: item.value}) }}"
loop: "{{ tags|dict2items }}"
vars:
tags:
Environment: dev
Application: payment
Another: "{{ doesnotexist|default() }}"
when: item.value != ""
Здесь мы не хотим устанавливать пустые теги, поэтому мы создаём словарь, содержащий только непустые теги.
Сложные циклы
Иногда вам требуется больше, чем предоставляет простой список. Вы можете использовать выражения Jinja2 для создания сложных списков. Например, используя поиск «nested», вы можете объединить списки:
- name: give users access to multiple databases
mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
append_privs: yes
password: "foo"
loop: "{{ ['alice', 'bob'] |product(['clientdb', 'employeedb', 'providerdb'])|list }}"
Примечание
with_ циклы фактически являются комбинацией with_ + lookup(), даже items является поиском. loop можно использовать аналогичным образом, как показано выше.
Использование lookup и query с циклом
В Ansible 2.5 была введена новая функция Jinja2 под названием query, которая предлагает несколько преимуществ по сравнению с lookup при использовании нового ключевого слова loop.
Это лучше описано в документации по lookup. Однако, query предоставляет более простой интерфейс и более предсказуемый результат работы плагинов lookup, обеспечивая лучшую совместимость с loop.
В определённых ситуациях функция lookup может не вернуть список, который loop требует.
Следующие вызовы эквивалентны, используя wantlist=True с lookup для обеспечения возвращаемого типа списка:
loop: "{{ query('inventory_hostnames', 'all') }}"
loop: "{{ lookup('inventory_hostnames', 'all', wantlist=True) }}"
Циклы «до тех пор, пока»
Новое в версии 1.4.
Иногда вам нужно будет повторить задачу до тех пор, пока не будет выполнено определенное условие. Вот пример:
- shell: /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10
В приведенном выше примере модуль shell запускается рекурсивно до тех пор, пока в стандартном выводе модуля не будет строка «все системы работают», или задача не будет перепробована 5 раз с задержкой в 10 секунд. Значение по умолчанию для «retries» равно 3, а «delay» равно 5.
Задача возвращает результаты, полученные последней выполненной задачей. Результаты отдельных попыток можно увидеть с помощью опции -vv. Регистрируемая переменная также будет иметь новый ключ «attempts», который будет содержать количество попыток выполнения задачи.
Примечание
Если параметр until не определен, значение параметра retries принудительно устанавливается в 1.
Использование register с циклом
После использования register с циклом структура данных, помещённая в переменную, будет содержать атрибут results, который представляет собой список всех ответов от модуля.
Вот пример использования register с loop:
- shell: "echo {{ item }}"
loop:
- "one"
- "two"
register: echo
Это отличается от структуры данных, возвращаемой при использовании register без цикла:
{
"changed": true,
"msg": "All items completed",
"results": [
{
"changed": true,
"cmd": "echo \"one\" ",
"delta": "0:00:00.003110",
"end": "2013-12-19 12:00:05.187153",
"invocation": {
"module_args": "echo \"one\"",
"module_name": "shell"
},
"item": "one",
"rc": 0,
"start": "2013-12-19 12:00:05.184043",
"stderr": "",
"stdout": "one"
},
{
"changed": true,
"cmd": "echo \"two\" ",
"delta": "0:00:00.002920",
"end": "2013-12-19 12:00:05.245502",
"invocation": {
"module_args": "echo \"two\"",
"module_name": "shell"
},
"item": "two",
"rc": 0,
"start": "2013-12-19 12:00:05.242582",
"stderr": "",
"stdout": "two"
}
]
}
Следующие циклы по регистрируемой переменной для проверки результатов могут выглядеть так:
- name: Fail if return code is not 0
fail:
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
when: item.rc != 0
loop: "{{ echo.results }}"
Во время итерации результат текущего элемента будет помещен в переменную:
- shell: echo "{{ item }}"
loop:
- one
- two
register: echo
changed_when: echo.stdout != "one"
Перебор инвентаризации
Если вы хотите перебрать инвентаризацию или только её подмножество, есть несколько способов. Можно использовать обычный loop с переменными ansible_play_batch или groups, как в этом примере:
# show all the hosts in the inventory
- debug:
msg: "{{ item }}"
loop: "{{ groups['all'] }}"
# show all the hosts in the current play
- debug:
msg: "{{ item }}"
loop: "{{ ansible_play_batch }}"
Также есть специальный плагин lookup inventory_hostnames, который можно использовать так:
# show all the hosts in the inventory
- debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all') }}"
# show all the hosts matching the pattern, ie all but the group www
- debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all!www') }}"
Дополнительную информацию о шаблонах можно найти на странице Работа с шаблонами
Управление циклом
Новое в версии 2.1.
В версии 2.0 вы снова можете использовать циклы и включения задач (но не плейбуков). Это добавляет возможность перебирать набор задач в едином цикле. Ansible по умолчанию устанавливает переменную цикла item для каждого цикла, что приводит к перезаписи значения item из «внешних» циклов в этих вложенных циклах. Начиная с Ansible 2.1, опция loop_control может быть использована для указания имени переменной, используемой для цикла:
# main.yml
- include_tasks: inner.yml
loop:
- 1
- 2
- 3
loop_control:
loop_var: outer_item
# inner.yml
- debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
loop:
- a
- b
- c
Примечание
Если Ansible обнаружит, что текущий цикл использует переменную, которая уже определена, он выведет ошибку, чтобы прервать задачу.
Новое в версии 2.2.
При использовании сложных структур данных для циклов вывод может стать слишком «загромождённым». В этом случае поможет директива label:
- name: create servers
digital_ocean:
name: "{{ item.name }}"
state: present
loop:
- name: server1
disks: 3gb
ram: 15Gb
network:
nic01: 100Gb
nic02: 10Gb
...
loop_control:
label: "{{ item.name }}"
Теперь будет отображаться только поле label вместо всей структуры на каждый item, по умолчанию используется {{ item }} для отображения элементов обычным способом.
Новое в версии 2.2.
Другой вариант управления циклом — pause, который позволяет управлять временем (в секундах) между выполнением элементов в цикле задачи.
# main.yml
- name: create servers, pause 3s before creating next
digital_ocean:
name: "{{ item }}"
state: present
loop:
- server1
- server2
loop_control:
pause: 3
Новое в версии 2.5.
Если вам нужно отслеживать, где вы находитесь в цикле, вы можете использовать опцию index_var для управления циклом, чтобы указать имя переменной, которая будет содержать текущий индекс цикла:
- name: count our fruit
debug:
msg: "{{ item }} with index {{ my_idx }}"
loop:
- apple
- banana
- pear
loop_control:
index_var: my_idx
Миграция с with_X на цикл
С выпуском Ansible 2.5 рекомендуемым способом выполнения циклов является использование нового ключевого слова loop вместо циклов стиля with_X.
Во многих случаях синтаксис loop лучше выражается с помощью фильтров, а не с более сложным использованием query или lookup.
Следующие примеры покажут, как преобразовать многие распространённые циклы стиля with_ в циклы loop и фильтры.
with_list
with_list напрямую заменяется на loop.
- name: with_list
debug:
msg: "{{ item }}"
with_list:
- one
- two
- name: with_list -> loop
debug:
msg: "{{ item }}"
loop:
- one
- two
with_items
with_items заменяется на loop и фильтр flatten.
- name: with_items
debug:
msg: "{{ item }}"
with_items: "{{ items }}"
- name: with_items -> loop
debug:
msg: "{{ item }}"
loop: "{{ items|flatten(levels=1) }}"
with_indexed_items
with_indexed_items заменяется на loop, фильтр flatten и loop_control.index_var.
- name: with_indexed_items
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_indexed_items: "{{ items }}"
- name: with_indexed_items -> loop
debug:
msg: "{{ index }} - {{ item }}"
loop: "{{ items|flatten(levels=1) }}"
loop_control:
index_var: index
with_flattened
with_flattened заменяется на loop и фильтр flatten.
- name: with_flattened
debug:
msg: "{{ item }}"
with_flattened: "{{ items }}"
- name: with_flattened -> loop
debug:
msg: "{{ item }}"
loop: "{{ items|flatten }}"
with_together
with_together заменяется на loop и фильтр zip.
- name: with_together
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_together:
- "{{ list_one }}"
- "{{ list_two }}"
- name: with_together -> loop
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ list_one|zip(list_two)|list }}"
with_dict
with_dict может быть заменено на loop и либо фильтр dictsort, либо dict2items.
- name: with_dict
debug:
msg: "{{ item.key }} - {{ item.value }}"
with_dict: "{{ dictionary }}"
- name: with_dict -> loop (option 1)
debug:
msg: "{{ item.key }} - {{ item.value }}"
loop: "{{ dictionary|dict2items }}"
- name: with_dict -> loop (option 2)
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ dictionary|dictsort }}"
with_sequence
with_sequence заменяется на loop и функцию range, и потенциально фильтр format.
- name: with_sequence
debug:
msg: "{{ item }}"
with_sequence: start=0 end=4 stride=2 format=testuser%02x
- name: with_sequence -> loop
debug:
msg: "{{ 'testuser%02x' | format(item) }}"
# range is exclusive of the end point
loop: "{{ range(0, 4 + 1, 2)|list }}"
with_subelements
with_subelements заменяется на loop и фильтр subelements.
- name: with_subelements
debug:
msg: "{{ item.0.name }} - {{ item.1 }}"
with_subelements:
- "{{ users }}"
- mysql.hosts
- name: with_subelements -> loop
debug:
msg: "{{ item.0.name }} - {{ item.1 }}"
loop: "{{ users|subelements('mysql.hosts') }}"
with_nested/with_cartesian
with_nested и with_cartesian заменяются циклом и фильтром product.
- name: with_nested
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
with_nested:
- "{{ list_one }}"
- "{{ list_two }}"
- name: with_nested -> loop
debug:
msg: "{{ item.0 }} - {{ item.1 }}"
loop: "{{ list_one|product(list_two)|list }}"
с_случайным_выбором
with_random_choice заменяется простым использованием фильтра random, без необходимости в loop.
- name: with_random_choice
debug:
msg: "{{ item }}"
with_random_choice: "{{ my_list }}"
- name: with_random_choice -> loop (No loop is needed here)
debug:
msg: "{{ my_list|random }}"
tags: random
См. также
- Работа с Playbook
- Введение в Playbook
- Роли
- Организация Playbook по ролям
- Рекомендации
- Рекомендации по использованию Playbook
- Условные операторы
- Условные операторы в Playbook
- Использование переменных
- Все о переменных
- Список рассылки пользователей
- Есть вопрос? Заходите на форум!
- irc.freenode.net
- Канал чата IRC #ansible
© 2012–2018 Michael DeHaan
© 2018–2019 Red Hat, Inc.
Licensed under the GNU General Public License version 3.
https://docs.ansible.com/ansible/2.7/user_guide/playbooks_loops.html