Написать данный опус навеяла статья на Хабре Распутывая Ansible Loops. Вообще с циклами у ansible на мой взгляд не задалось. Никаких тебе конструкций вида for и while. Официальная документация довольно-таки развернутая, но немного в ней не хватает элементарных примеров. Их и постараюсь привести.
Перебираем списки
Cписок наверное самое простое и частоиспользуемое что можно перебрать в ansible. Перебирать элементы списка можно с использованием loop и Jinja фильтров:
- name: print list, reversed list and sorted list debug: msg: "List is {{ list_row }} \nReverse list is {{ list_row|reverse }} \nSorted list is {{ list_row|sort }}" name: print item of sorted and reversesd list debug: msg: "{{ item }}" loop: "{{ list_row|sort|reverse }}" TASK [print list, reversed list and sorted list] MSG: List is ['row51', 'row2', 'row13', 'row4'] Reverse list is ['row4', 'row13', 'row2', 'row51'] Sorted list is ['row13', 'row2', 'row4', 'row51'] TASK [print item of sorted and reversesd list] MSG: row51 MSG: row4 MSG: row2 MSG: row13
Перебираем таблицы
Теперь разберемся с простыми циклами по таблице. Ограничения python понятны - это не matlab, заточенный под работу с матрицами, и каждая таблица-матрица-тензор удобнее всего представляется списком из списков из списков... Пробежимся по строкам, столбцам и элементам - слева направо, сверху вниз и изменив порядок.
vars: table: - ['a', 'b', 'c'] - ['d', 'e', 'f'] - ['g', 'h', 'i']
- name: print rows debug: msg: "{{ item }}" loop: "{{ table|list }}" - name: print column debug: msg: "{{ item }}" loop: "{{ table[0]|zip(*table[1:])|list }}" - name: print elements left2rigth up2down debug: msg: "{{ item }}" loop: "{{ table|list|flatten(levels=1) }}" - name: print elements up2down left2rigth debug: msg: "{{ item }}" loop: "{{ table[0]|zip(*table[1:])|list|flatten(levels=1) }}"
Результат
TASK [print rows] * MSG: ['a', 'b', 'c'] MSG: ['d', 'e', 'f'] MSG: ['g', 'h', 'i'] TASK [print column] * MSG: ['a', 'd', 'g'] MSG: ['b', 'e', 'h'] MSG: ['c', 'f', 'i'] TASK [print elements left2rigth up2down] MSG: a MSG: b MSG: c MSG: d MSG: e MSG: f MSG: g MSG: h MSG: i TASK [print elements up2down left2rigth] MSG: a MSG: d MSG: g MSG: b MSG: e MSG: h MSG: c MSG: f MSG: i
Перебираем строки
Почему строки после списков? Да потому что строка в ansible неитерируема из коробки и надо сделать приседание чтобы получить хорошо знакомый нам список.
- debug: var=test_string - name: Iterate to symvol debug: msg: "{{ item }}" loop: "{{ test_string|list }}" - name: Iterate to word debug: msg: "{{ item }}" loop: "{{ test_string.split(' ') }}" TASK [debug] ok: => { "test_string": "hello World!" } TASK [Iterate to symvol] MSG: h MSG: e MSG: l MSG: l MSG: o MSG: MSG: W MSG: o MSG: r MSG: l MSG: d SG: ! TASK [Iterate to word] MSG: hello MSG: World!
Перебираем словари
Словари как и списки можно перебрать, сославшись на каждое значение словаря, сортировав словарь, или извлекая из словаря значение key и соответствующее value. Стоит обратить внимание что фильтр dictsort преобразует словарь в список и сортрует его по значению ключей.
vars: dict_pass: broker : ['pass_11', 'pass_12'] root : ['pass_21', 'pass_22'] client : ['pass_31', 'pass_32']
- name: print dict items debug: msg: "{{ item }}" loop: "{{ dict_pass|dict2items }}" - name: print sorted dict items debug: msg: "{{ item }}" loop: "{{ dict_pass|dictsort }}" - name: print dict items debug: msg: "{{ item.key }} and {{ item.value.0 }} and {{ item.value.1 }}" loop: "{{ dict_pass|dict2items }}"
результат
TASK [print dict items] * MSG: {'key': 'broker', 'value': ['pass_11', 'pass_12']} MSG: {'key': 'root', 'value': ['pass_21', 'pass_22']} MSG: {'key': 'client', 'value': ['pass_31', 'pass_32']} TASK [print sorted dict items] MSG: ['broker', ['pass_11', 'pass_12']] MSG: ['client', ['pass_31', 'pass_32']] MSG: ['root', ['pass_21', 'pass_22']] TASK [print dict items] * MSG: broker and pass_11 and pass_12 MSG: root and pass_21 and pass_22 MSG: client and pass_31 and pass_32
Перебираем группы
Наш инвентарь будет выглядеть так (не судите строго).
[all] [all:children] test cluster kafka [kafka] kafka-1.my.domain kafka-2.my.domain [test] nginx-1.my.domain nginx-2.my.domain kafka-5.my.domain kafka-3.my.domain [cluster] [cluster:children] postgres etcd [postgres] postgres-1.my.domain postgres-2.my.domain postgres-3.my.domain [etcd_nodes] etcd-1.my.domain etcd-2.my.domain etcd-3.my.domain [all:vars] ansible_ssh_port='7722' ansible_user='ansible-allsudo' ansible_password='Ansible_pa$$word'
Попробуем вывести значение groups.
- name: print groups debug: var=groups TASK [print groups] "all": [ "kafka-1.my.domain", "kafka-2.my.domain", "nginx-1.my.domain", "nginx-2.my.domain", "kafka-5.my.domain", "kafka-3.my.domain", "postgres-1.my.domain", "postgres-2.my.domain", "postgres-3.my.domain" ], "cluster": [ "postgres-1.my.domain", "postgres-2.my.domain", "postgres-3.my.domain" ], "etcd_nodes": [ "etcd-1.my.domain", "etcd-2.my.domain", "etcd-3.my.domain" ], "kafka": [ "kafka-1.my.domain", "kafka-2.my.domain" ], "postgres": [ "postgres-1.my.domain", "postgres-2.my.domain", "postgres-3.my.domain" ], "test": [ "nginx-1.my.domain", "nginx-2.my.domain", "kafka-5.my.domain", "kafka-3.my.domain" ], "ungrouped": [] } }
Итак, имеем:
groups - словарь, элементами которого являются списки. Проверяем:
- name: print groups as dict debug: msg: "{{ item }}" loop: "{{ groups|dict2items }}" TASK [print groups] MSG: {'key': 'all', 'value': ['kafka-1.my.domain', 'kafka-2.my.domain', 'nginx-1.my.domain', 'nginx-2.my.domain', 'kafka-5.my.domain', 'kafka-3.my.domain', 'postgres-1.my.domain', 'postgres-2.my.domain', 'postgres-3.my.domain']} MSG: {'key': 'ungrouped', 'value': []} MSG: {'key': 'kafka', 'value': ['kafka-1.my.domain', 'kafka-2.my.domain']} MSG: {'key': 'test', 'value': ['nginx-1.my.domain', 'nginx-2.my.domain', 'kafka-5.my.domain', 'kafka-3.my.domain']} MSG: {'key': 'cluster', 'value': ['postgres-1.my.domain', 'postgres-2.my.domain', 'postgres-3.my.domain']} MSG: {'key': 'postgres', 'value': ['postgres-1.my.domain', 'postgres-2.my.domain', 'postgres-3.my.domain']} MSG: {'key': 'etcd_nodes', 'value': ['etcd-1.my.domain', 'etcd-2.my.domain', 'etcd-3.my.domain']}
Значит и обращаться с groups надо соответствующим образом - также как и со словарем.
Еще один способ итерироваться по хостам - использовать магическую переменную ansible_play_hosts.
- name: Create virtual group add_host: name: "{{ item|regex_replace('[.].*','')}}" groups: - firstnodes ansible_host: "{{ item }}" when: item | regex_search('.*1') loop: "{{ ansible_play_hosts }}" register: result debug: var: groups.firstnodes when: result.results[0].changed run_once: true TASK [Create virtual group] omain) main) kipping: => (item=kafka-5.my.domain) skipping: => (item=kafka-3.my.domain) omain) kipping: => (item=postgres-3.my.domain) TASK [debug] ok: => { "groups.firstnodes": [ "kafka-1", "nginx-1", "postgres-1" ] }
Стоит обратить внимание на то что первая таска с модулем add_host обрабатывается лишь однажды, несмотря на то что нет run_once: true. А groups.firstnodes является списком.
Ну и посмотрим что внутри вновь созданной группы firstnodes:
- hosts: firstnodes gather_facts: false tasks: - debug: msg: "{{ inventory_hostname }} and {{ ansible_host }}" PLAY [firstnodes] TASK [debug] ok: [kafka-1] => {} MSG: kafka-1 and kafka-1.my.domain ok: [nginx-1] => {} MSG: nginx-1 and nginx-1.my.domain ok: [postgres-1] => {} MSG: postgres-1 and postgres-1.my.domain
Вложенные циклы
Наконец-то подошли к самому интересному - использованию циклов в циклах. Есть возможность в цикле вызывать playbook и передавать туда переменные. То есть, внешним циклом осуществляем перебор хостов, внутренним - итерируем переменные.
--- - hosts: all vars: list_path_to_cfg_rgx: .... tasks: - name: regexp_change include: regexp_change.yml loop: "{{ list_path_to_cfg_rgx|flatten(levels=1) }}" loop_control: loop_var: path_to_cfg_rgx when: list_path_to_cfg_rgx is defined
Но бывают случаи когда ходить с хоста на хост не хочется, а сформировать переменные для шаблона надо. Тут для совместного использования переменных документация предлагает использовать нам Jinja2 фильтр product. Фильтр product возвращает декартово произведение входных списков. Это примерно эквивалентно вложенным циклам for. Попробуем воспроизвести. Будем перебирать инвентарь совместно с другими переменными. Помним о том, что на вход необходимо передавать списки.
- name: Add hosts vars: group_patterns: - groups: - second2group regex: ".*2.*" - groups: - nginx regex: "^nginx.*" add_host: hostname: "{{ item.0 }}" ansible_host: "{{ item.0 }}" groups: "{{ item.1['groups'] }}" when: item.0 | regex_search(item.1['regex']) loop: "{{ ansible_play_hosts | product(group_patterns) }}" debug: msg: "{{ item.key }} contain {{ item.value }}" loop: "{{ groups|dict2items }}" when: item.key == 'second2group' or item.key == 'nginx' run_once: true TASK [debug] skipping: => (item={'key': 'all', 'value': ['kafka-1.my.domain', 'kafka-2.my.domain', 'nginx-1.my.domain', 'nginx-2.my.domain', 'kafka-5.your.domain', 'kafka-3.your.domain', 'etcd-1.my.domain', 'etcd-2.my.domain', 'etcd-3.my.domain', 'postgres-2.my.domain', 'postgres-1.my.domain', 'postgres-3.my.domain']}) skipping: => (item={'key': 'ungrouped', 'value': []}) skipping: => (item={'key': 'kafka', 'value': ['kafka-1.my.domain', 'kafka-2.my.domain']}) skipping: => (item={'key': 'test', 'value': ['nginx-1.my.domain', 'nginx-2.my.domain', 'kafka-5.your.domain', 'kafka-3.your.domain']}) skipping: => (item={'key': 'cluster', 'value': ['postgres-1.my.domain', 'postgres-2.my.domain', 'postgres-3.my.domain']}) skipping: => (item={'key': 'postgres', 'value': ['postgres-1.my.domain', 'postgres-2.my.domain', 'postgres-3.my.domain']}) skipping: => (item={'key': 'etcd_nodes', 'value': ['etcd-1.my.domain', 'etcd-2.my.domain', 'etcd-3.my.domain']}) skipping: => (item={'key': 'kafka2', 'value': ['kafka-1.my.domain', 'kafka-2.my.domain', 'kafka-5.your.domain', 'kafka-3.your.domain']}) ok: => (item={'key': 'second2group', 'value': ['kafka-2.my.domain', 'nginx-2.my.domain', 'postgres-2.my.domain']}) => {} MSG: second2group contain ['kafka-2.my.domain', 'nginx-2.my.domain', 'postgres-2.my.domain'] ok: => (item={'key': 'nginx', 'value': ['nginx-1.my.domain', 'nginx-2.my.domain']}) => {} MSG: nginx contain ['nginx-1.my.domain', 'nginx-2.my.domain']
И напоследок хотелось сказать что все вышесказанное является мнением автора и может не совпадать с мнением редакции.
