Циклы
Часто вам нужно выполнить много действий в одной задаче, например, создать много пользователей, установить много пакетов или повторять шаг опроса до достижения определенного результата.
В этой главе рассказывается о том, как использовать циклы в плейбуках.
- Стандартные циклы
- Вложенные циклы
- Итерация по хешам
- Итерация по файлам
- Итерация по шаблонам файлов
- Итерация по дереву файлов
- Итерация по параллельным наборам данных
- Итерация по подэлементам
- Итерация по последовательностям целых чисел
- Случайный выбор
- Циклы «до тех пор, пока»
- Поиск первого соответствующего файла
- Итерация по результатам выполнения программы
- Итерация по списку с индексом
- Использование файла ini с циклом
- Развёртывание списка
- Использование реестра с циклом
- Итерация по инвентаризации
- Управление циклом
- Циклы и включения в версии 2.0
- Создание собственных итераторов
Стандартные циклы
Для экономии набора текста повторяющиеся задачи можно записать в сокращенном виде, как показано ниже:
- name: add several users
user:
name: "{{ item }}"
state: present
groups: "wheel"
with_items:
- testuser1
- testuser2
Если вы определили список YAML в файле переменных или в разделе «переменные», вы также можете сделать следующее:
with_items: "{{ somelist }}"
Вышеприведенное эквивалентно следующему:
- name: add user testuser1
user:
name: "testuser1"
state: present
groups: "wheel"
- name: add user testuser2
user:
name: "testuser2"
state: present
groups: "wheel"
Модули yum и apt используют with_items для выполнения меньшего количества операций с менеджерами пакетов.
Обратите внимание, что типы элементов, по которым вы итерируетесь с помощью «with_items», не обязательно должны быть простыми списками строк. Если у вас есть список хешей, вы можете ссылаться на подключаемые ключи, используя такие конструкции, как:
- name: add several users
user:
name: "{{ item.name }}"
state: present
groups: "{{ item.groups }}"
with_items:
- { name: 'testuser1', groups: 'wheel' }
- { name: 'testuser2', groups: 'root' }
Также следует учитывать, что при объединении when с with_items (или любым другим оператором цикла), оператор when обрабатывается отдельно для каждого элемента. См. Оператор When для примера.
Циклы фактически представляют собой комбинацию with_ + lookup(), поэтому любой плагин поиска можно использовать в качестве источника цикла, «items» — это плагин поиска.
Обратите внимание, что with_items разворачивает первый уровень вложенности списка, что может привести к непредсказуемым результатам, если вы передадите список, состоящий из списков. Вы можете обойти эту проблему, обернув свой вложенный список в список:
# This will run debug three times since the list is flattened
- debug:
msg: "{{ item }}"
vars:
nested_list:
- - one
- two
- three
with_items: "{{ nested_list }}"
# This will run debug once with the three items
- debug:
msg: "{{ item }}"
vars:
nested_list:
- - one
- two
- three
with_items:
- "{{ nested_list }}"
Вложенные циклы
Циклы также могут быть вложенными:
- name: give users access to multiple databases
mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
append_privs: yes
password: "foo"
with_nested:
- [ 'alice', 'bob' ]
- [ 'clientdb', 'employeedb', 'providerdb' ]
Как и в случае с «with_items» выше, вы можете использовать ранее определенные переменные:
- name: here, 'users' contains the above list of employees
mysql_user:
name: "{{ item[0] }}"
priv: "{{ item[1] }}.*:ALL"
append_privs: yes
password: "foo"
with_nested:
- "{{ users }}"
- [ 'clientdb', 'employeedb', 'providerdb' ]
Итерация по хешам
Новое в версии 1.5.
Предположим, у вас есть следующая переменная:
---
users:
alice:
name: Alice Appleworth
telephone: 123-456-7890
bob:
name: Bob Bananarama
telephone: 987-654-3210
И вы хотите вывести имя и номер телефона каждого пользователя. Вы можете перебирать элементы хеша, используя with_dict, как показано ниже:
tasks:
- name: Print phone records
debug:
msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
with_dict: "{{ users }}"
Итерация по файлам
with_file итерируется по содержимому списка файлов, item будет содержать содержимое каждого файла последовательно. Он может использоваться следующим образом:
---
- hosts: all
tasks:
# emit a debug message containing the content of each file.
- debug:
msg: "{{ item }}"
with_file:
- first_example_file
- second_example_file
Предполагая, что first_example_file содержало текст «hello», а second_example_file — текст «world», это приведет к:
TASK [debug msg={{ item }}] ******************************************************
ok: [localhost] => (item=hello) => {
"item": "hello",
"msg": "hello"
}
ok: [localhost] => (item=world) => {
"item": "world",
"msg": "world"
}
Итерация по шаблонам файлов
with_fileglob ищет все файлы в одном каталоге, не рекурсивно, которые соответствуют шаблону. Он вызывает библиотеку Python glob и может использоваться следующим образом:
---
- hosts: all
tasks:
# first ensure our target directory exists
- name: Ensure target directory exists
file:
dest: "/etc/fooapp"
state: directory
# copy each file over that matches the given pattern
- name: Copy each file over that matches the given pattern
copy:
src: "{{ item }}"
dest: "/etc/fooapp/"
owner: "root"
mode: 0600
with_fileglob:
- "/playbooks/files/fooapp/*"
Примечание
При использовании относительного пути с with_fileglob в роли Ansible разрешает путь относительно каталога roles/<rolename>/files.
Итерация по дереву файлов
with_filetree рекурсивно ищет все файлы в дереве каталогов, позволяя вам шаблонизировать полное дерево файлов на целевой системе, сохраняя при этом разрешения и права собственности.
Плагин поиска filetree поддерживает каталоги, файлы и символьные ссылки, включая SELinux и другие свойства файлов. Вот полный список того, из чего состоит каждый объект файла:
- src
- root
- path
- mode
- state
- owner
- group
- seuser
- serole
- setype
- selevel
- uid
- gid
- size
- mtime
- ctime
Если вы укажете более одного пути, он реализует логику with_first_found и не будет обрабатывать записи, которые уже были обработаны на предыдущих путях. Это позволяет пользователю объединять различные деревья в порядке важности или добавлять роли_vars, специфичные пути, чтобы повлиять на различные экземпляры одной и той же роли.
Вот пример того, как мы используем with_filetree в роли. Путь web/ является относительным к либо roles/<role>/files/, либо files/:
---
- name: Create directories
file:
path: /web/{{ item.path }}
state: directory
mode: '{{ item.mode }}'
with_filetree: web/
when: item.state == 'directory'
- name: Template files
template:
src: '{{ item.src }}'
dest: /web/{{ item.path }}
mode: '{{ item.mode }}'
with_filetree: web/
when: item.state == 'file'
- name: Recreate symlinks
file:
src: '{{ item.src }}'
dest: /web/{{ item.path }}
state: link
force: yes
mode: '{{ item.mode }}'
with_filetree: web/
when: item.state == 'link'
Также доступны следующие свойства:
-
root: позволяет фильтровать по исходному расположению -
path: содержит относительный путь к корню -
uidi,gid: принудительное создание по точному идентификатору, а не по имени -
size,mtime,ctime: фильтрация файлов по размеру, mtime или ctime
Итерация по параллельным наборам данных
Предположим, у вас есть следующие данные переменных:
--- alpha: [ 'a', 'b', 'c', 'd' ] numbers: [ 1, 2, 3, 4 ]
...и вам нужно получить набор «(a, 1)» и «(b, 2)». Используйте «with_together», чтобы получить это:
tasks:
- debug:
msg: "{{ item.0 }} and {{ item.1 }}"
with_together:
- "{{ alpha }}"
- "{{ numbers }}"
Итерация по подэлементам
Предположим, вы хотите выполнить действие, например, перебрать список пользователей, создать их и разрешить им входить по определенному набору ключей SSH.
В этом примере мы предположим, что у вас есть следующие данные, загруженные через «vars_files» или, возможно, «group_vars/all»:
---
users:
- name: alice
authorized:
- /tmp/alice/onekey.pub
- /tmp/alice/twokey.pub
mysql:
password: mysql-password
hosts:
- "%"
- "127.0.0.1"
- "::1"
- "localhost"
privs:
- "*.*:SELECT"
- "DB1.*:ALL"
- name: bob
authorized:
- /tmp/bob/id_rsa.pub
mysql:
password: other-mysql-password
hosts:
- "db1"
privs:
- "*.*:SELECT"
- "DB2.*:ALL"
Вы можете перебирать эти подэлементы следующим образом:
- name: Create User
user:
name: "{{ item.name }}"
state: present
generate_ssh_key: yes
with_items:
- "{{ users }}"
- name: Set authorized ssh key
authorized_key:
user: "{{ item.0.name }}"
key: "{{ lookup('file', item.1) }}"
with_subelements:
- "{{ users }}"
- authorized
Учитывая списки mysql hosts и privs, вы также можете перебирать список по вложенному подключаемому ключу:
- name: Setup MySQL users
mysql_user:
name: "{{ item.0.name }}"
password: "{{ item.0.mysql.password }}"
host: "{{ item.1 }}"
priv: "{{ item.0.mysql.privs | join('/') }}"
with_subelements:
- "{{ users }}"
- mysql.hosts
Subelements перебирает список хешей (т.е. словарей) и затем просматривает список с заданным (вложенным под-)ключом внутри этих записей.
Необязательно, вы можете добавить третий элемент в список subelements, который содержит словарь флагов. В настоящее время вы можете добавить флаг «skip_missing». Если он установлен в True, плагин поиска пропустит элементы списка, которые не содержат заданный подключаемый ключ. Без этого флага или если этот флаг установлен в False, плагин выдаст ошибку и пожалуется на отсутствие подключаемого ключа.
Шаблон authorized_key — это именно тот случай, когда это нужно.
Итерация по последовательностям целых чисел
with_sequence генерирует последовательность элементов. Вы можете указать начальное значение, конечное значение, необязательное значение «шага», которое определяет количество шагов для инкремента последовательности, и необязательную строку форматирования в стиле printf.
Аргументы должны быть указаны в виде строк ключ=значение.
Также принимается сокращенная форма строки аргументов: [start-]end[/stride][:format].
Числовые значения могут быть указаны в десятичной, шестнадцатеричной (0x3f8) или восьмеричной (0600) форме. Отрицательные числа не поддерживаются. Это работает следующим образом:
---
- hosts: all
tasks:
# create groups
- group:
name: "evens"
state: present
- group:
name: "odds"
state: present
# create some test users
- user:
name: "{{ item }}"
state: present
groups: "evens"
with_sequence: start=0 end=32 format=testuser%02x
# create a series of directories with even numbers for some reason
- file:
dest: "/var/stuff/{{ item }}"
state: directory
with_sequence: start=4 end=16 stride=2
# a simpler way to use the sequence plugin
# create 4 groups
- group:
name: "group{{ item }}"
state: present
with_sequence: count=4
Случайный выбор
Функция «random_choice» может быть использована для случайного выбора. Хотя она не является балансировщиком нагрузки (есть модули для этого), она может использоваться как бедный балансировщик нагрузки в ситуации «Мастер МакГайвера»:
- debug:
msg: "{{ item }}"
with_random_choice:
- "go through the door"
- "drink from the goblet"
- "press the red button"
- "do nothing"
Одна из предоставленных строк будет выбрана случайным образом.
На более базовом уровне они могут быть использованы для добавления хаоса и волнения в иные предсказуемые автоматизированные среды.
Циклы «до тех пор, пока»
Новое в версии 1.4.
Иногда вы хотите повторить задачу до тех пор, пока не будет выполнено определенное условие. Вот пример:
- shell: /usr/bin/foo
register: result
until: result.stdout.find("all systems go") != -1
retries: 5
delay: 10
В приведенном выше примере модуль shell выполняется рекурсивно до тех пор, пока результат модуля не содержит «all systems go» в своем стандартном выводе или задача не была повторена 5 раз с задержкой в 10 секунд. Значение по умолчанию для «retries» — 3, а «delay» — 5.
Задача возвращает результаты, возвращенные последней запущенной задачей. Результаты отдельных попыток можно увидеть с помощью параметра -vv. В зарегистрированной переменной также будет новый ключ «attempts», который будет содержать количество повторений для задачи.
Примечание
Если параметр «until» не определен, значение параметра «retries» принудительно устанавливается в 1.
Поиск первого соответствующего файла
Примечание
Это не очень часто используемая функция, но мы документируем её для полноты. Вероятно, вы не будете часто её использовать.
Это не совсем цикл, но близко. Что, если вы хотите использовать ссылку на файл, основанную на первом найденном файле, который соответствует заданным критериям, и некоторые имена файлов определяются именами переменных? Да, вы можете сделать это следующим образом:
- name: INTERFACES | Create Ansible header for /etc/network/interfaces
template:
src: "{{ item }}"
dest: "/etc/foo.conf"
with_first_found:
- "{{ ansible_virtualization_type }}_foo.conf"
- "default_foo.conf"
Этот инструмент также имеет длинную форму, которая позволяет настраивать пути поиска. Вот пример:
- name: some configuration template
template:
src: "{{ item }}"
dest: "/etc/file.cfg"
mode: 0444
owner: "root"
group: "root"
with_first_found:
- files:
- "{{ inventory_hostname }}/etc/file.cfg"
paths:
- ../../../templates.overwrites
- ../../../templates
- files:
- etc/file.cfg
paths:
- templates
Итерация по результатам выполнения программы
Примечание
Это не очень часто используется, но мы документируем это для полноты. Скорее всего, вы не будете часто обращаться к этому.
Иногда вам может понадобиться выполнить программу, и на основе вывода этой программы, выполнить цикл по результатам построчно. Ansible предоставляет удобный способ сделать это, хотя вы должны помнить, что это всегда выполняется на управляющей машине, а не на удаленной машине:
- name: Example of looping over a command result
shell: "/usr/bin/frobnicate {{ item }}"
with_lines:
- "/usr/bin/frobnications_per_host --param {{ inventory_hostname }}"
Хорошо, это было немного произвольно. На самом деле, если вы делаете что-то, связанное с инвентаризацией, вы можете просто написать динамический источник инвентаризации (см. Динамический инвентарь), но это может быть иногда полезно в быстрых и грязных реализациях.
Если вам нужно выполнить команду удаленно, вы не должны использовать вышеуказанный метод. Вместо этого сделайте следующее:
- name: Example of looping over a REMOTE command result
shell: "/usr/bin/something"
register: command_result
- name: Do something with each result
shell: "/usr/bin/something_else --param {{ item }}"
with_items:
- "{{ command_result.stdout_lines }}"
Перебор списка с индексом
Примечание
Это не очень часто используется, но мы документируем это для полноты. Скорее всего, вы не будете часто обращаться к этому.
Новая функция в версии 1.3.
Если вы хотите перебрать массив и также получить числовой индекс, где вы находитесь в массиве по мере продвижения, вы также можете это сделать. Это используется редко:
- name: indexed loop demo
debug:
msg: "at array position {{ item.0 }} there is a value {{ item.1 }}"
with_indexed_items:
- "{{ some_list }}"
Использование файла ini в цикле
Новая функция в версии 2.0.
Плагин ini может использовать регулярные выражения для получения набора ключей. В результате мы можем перебрать этот набор. Вот файл ini, который мы будем использовать:
[section1] value1=section1/value1 value2=section1/value2 [section2] value1=section2/value1 value2=section2/value2
Вот пример использования with_ini:
- debug:
msg: "{{ item }}"
with_ini:
- value[1-2]
- section: section1
- file: "lookup.ini"
- re: true
И вот возвращаемое значение:
{
"changed": false,
"msg": "All items completed",
"results": [
{
"invocation": {
"module_args": "msg=\"section1/value1\"",
"module_name": "debug"
},
"item": "section1/value1",
"msg": "section1/value1",
"verbose_always": true
},
{
"invocation": {
"module_args": "msg=\"section1/value2\"",
"module_name": "debug"
},
"item": "section1/value2",
"msg": "section1/value2",
"verbose_always": true
}
]
}
Разворачивание списка
Примечание
Это не очень часто используется, но мы документируем это для полноты. Скорее всего, вы не будете часто обращаться к этому.
В редких случаях у вас может быть несколько списков списков, и вы просто хотите перебрать каждый элемент во всех этих списках. Предположим, что у вас есть очень странная гипотетическая структура данных:
---- # file: roles/foo/vars/main.yml packages_base: - [ 'foo-package', 'bar-package' ] packages_apps: - [ ['one-package', 'two-package' ]] - [ ['red-package'], ['blue-package']]
Как вы можете видеть, форматирование пакетов в этих списках различное. Как мы можем установить все пакеты в обоих списках?:
- name: flattened loop demo
yum:
name: "{{ item }}"
state: present
with_flattened:
- "{{ packages_base }}"
- "{{ packages_apps }}"
Вот как!
Использование register с циклом
После использования register с циклом, структура данных, помещенная в переменную, будет содержать атрибут results, который представляет собой список всех ответов от модуля.
Вот пример использования register с with_items:
- shell: "echo {{ item }}"
with_items:
- "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
with_items: "{{ echo.results }}"
Во время итерации результат текущего элемента будет помещен в переменную:
- shell: echo "{{ item }}"
with_items:
- one
- two
register: echo
changed_when: echo.stdout != "one"
Перебор инвентаризации
Если вы хотите перебрать инвентаризацию или только подмножество ее, существуют различные способы. Можно использовать обычный with_items с переменными ansible_play_batch или groups, например так:
# show all the hosts in the inventory
- debug:
msg: "{{ item }}"
with_items:
- "{{ groups['all'] }}"
# show all the hosts in the current play
- debug:
msg: "{{ item }}"
with_items:
- "{{ ansible_play_batch }}"
Также существует специальный плагин для поиска inventory_hostnames, который можно использовать следующим образом:
# show all the hosts in the inventory
- debug:
msg: "{{ item }}"
with_inventory_hostnames:
- all
# show all the hosts matching the pattern, ie all but the group www
- debug:
msg: "{{ item }}"
with_inventory_hostnames:
- all:!www
Дополнительную информацию о шаблонах можно найти на странице Шаблоны
Управление циклом
Новая функция в версии 2.1.
В версии 2.0 вы снова можете использовать with_ циклы и задачи включения (но не playbook включения). Это добавляет возможность перебора набора задач за один раз. Ansible по умолчанию устанавливает переменную цикла item для каждого цикла, что приводит к перезаписи значения item из «внешних» циклов в этих вложенных циклах. начиная с Ansible 2.1, опция loop_control может быть использована для указания имени переменной, которая должна использоваться для цикла:
# main.yml
- include_tasks: inner.yml
with_items:
- 1
- 2
- 3
loop_control:
loop_var: outer_item
# inner.yml
- debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
with_items:
- a
- b
- c
Примечание
Если Ansible обнаружит, что текущий цикл использует переменную, которая уже определена, он выведет ошибку, чтобы завершить задачу.
Новая функция в версии 2.2.
При использовании сложных структур данных для циклов отображение может стать слишком «загроможденным». В этом случае на помощь приходит директива C(label):
- name: create servers
digital_ocean:
name: "{{ item.name }}"
state: present
with_items:
- name: server1
disks: 3gb
ram: 15Gb
network:
nic01: 100Gb
nic02: 10Gb
...
loop_control:
label: "{{item.name}}"
Теперь будет отображаться только поле ‘label’ вместо всей структуры на каждый элемент, по умолчанию отображается «”{{item}}”» для обычного отображения.
Новая функция в версии 2.2.
Другой вариант управления циклом — C(pause), который позволяет управлять временем (в секундах) между выполнением элементов в цикле задачи:
# main.yml
- name: create servers, pause 3s before creating next
digital_ocean:
name: "{{ item }}"
state: present
with_items:
- server1
- server2
loop_control:
pause: 3
Циклы и включения в 2.0
Поскольку loop_control недоступно в Ansible 2.0, при использовании включения с циклом вы должны использовать set_fact для сохранения значения «внешних» циклов для item:
# main.yml
- include_tasks: inner.yml
with_items:
- 1
- 2
- 3
# inner.yml
- set_fact:
outer_item: "{{ item }}"
- debug:
msg: "outer item={{ outer_item }} inner item={{ item }}"
with_items:
- a
- b
- c
Создание собственных итераторов
Хотя обычно это не требуется, если вы хотите создать свои собственные способы перебора произвольных структур данных, вы можете прочитать Разработка плагинов для получения начальной информации. Каждая из вышеперечисленных функций реализована как плагин в ansible, поэтому есть много реализаций, на которые можно сослаться.
См. также
- Планы
- Введение в планы
- Роли
- Организация планов по ролям
- Рекомендации по лучшим практикам
- Рекомендации по лучшим практикам в планах
- Условные операторы
- Условные операторы в планах
- Переменные
- Все о переменных
- Список рассылки пользователей
- У вас есть вопросы? Загляните в группу google!
- 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.4/playbooks_loops.html