Spec-Zone.ru › Ansible 2.4

Циклы

Часто вам нужно выполнить много действий в одной задаче, например, создать много пользователей, установить много пакетов или повторять шаг опроса до достижения определенного результата.

В этой главе рассказывается о том, как использовать циклы в плейбуках.

  • Стандартные циклы
  • Вложенные циклы
  • Итерация по хешам
  • Итерация по файлам
  • Итерация по шаблонам файлов
  • Итерация по дереву файлов
  • Итерация по параллельным наборам данных
  • Итерация по подэлементам
  • Итерация по последовательностям целых чисел
  • Случайный выбор
  • Циклы «до тех пор, пока»
  • Поиск первого соответствующего файла
  • Итерация по результатам выполнения программы
  • Итерация по списку с индексом
  • Использование файла 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

Spec-Zone.ru

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