Написать данный опус навеяла статья на Хабре Распутывая 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']

И напоследок хотелось сказать что все вышесказанное является мнением автора и может не совпадать с мнением редакции.

Комментарии (1)


  1. MartStap
    12.10.2022 20:40
    -1

    Спасибо за статью, интересный опыт!