Фильтры Jinja2#
Ansible позволяет использовать фильтры Jinja2 не только в шаблонах, но и в playbook.
С помощью фильтров можно преобразовывать значения переменных, переводить их в другой формат и др.
Ansible поддерживает не только встроенные фильтры Jinja, но и множество собственных фильтров. Мы не будем рассматривать все фильтры, поэтому, если вы не найдете нужный вам фильтр тут, посмотрите документацию.
Мы уже использовали фильтры:
Если вас интересуют фильтр в контексте использования их в шаблонах, это рассматривалось в разделе Фильтры.
Для начала, перечислим несколько фильтров для общего понимания возможностей.
Ansible поддерживает такие фильтры (список не полный):
фильтры для форматирования данных:
{{ var | to_nice_json }}
- преобразует данные в формат JSON{{ var | to_nice_yaml }}
- преобразует данные в формат YAML
переменные
{{ var | default(9) }}
- позволяет определить значение по умолчанию для переменной{{ var | default(omit) }}
- позволяет пропустить переменную, если она не определена
списки
{{ lista | min }}
- минимальный элемент списка{{ lista | max }}
- максимальный элемент списка
фильтры, которые работают множествами
{{ list1 | unique }}
- возвращает множество уникальных элементов из списка{{ list1 | difference(list2) }}
- разница между двумя списками: каких элементов первого списка нет во втором
фильтр для работы с IP-адресами
{{ var | ipaddr }}
- проверяет является ли переменная IP-адресом
регулярные выражения
regex_replace - замена в строке
regex_search - ищет первое совпадение с регулярным выражением
regex_findall - ищет все совпадения с регулярным выражением
фильтры, которые применяют другие фильтры к последовательности объектов:
map:
{{ list3 | map('int') }}
- применяет другой фильтр к последовательности элементов (например, список). Также позволяет брать значение определенного атрибута у каждого объекта в списке.select:
{{ list4 | select('int') }}
- фильтрует последовательность применяя другой фильтр к каждому из элементов. Остаются только те объекты, для которых тест отработал.
конвертация типов
{{ var | int }}
- конвертирует значение в число, по умолчанию, в десятичное{{ var | list }}
- конвертирует значение в список
to_nice_yaml#
Фильтры to_nice_yaml (to_nice_json) можно использовать для того, чтобы записать нужную информацию в файл.
Ansible также поддерживает фильтры to_json и to_yaml, но их сложнее воспринимать визуально.
Повторим пример из раздела ios_facts. Playbook 8_playbook_filters_to_nice_yaml.yml:
---
- name: Collect IOS facts
hosts: cisco-routers
gather_facts: false
connection: local
tasks:
- name: Facts
ios_facts:
gather_subset: all
provider: "{{ cli }}"
register: ios_facts_result
- name: Copy facts to files
copy:
content: "{{ ios_facts_result | to_nice_yaml }}"
dest: "all_facts/{{inventory_hostname}}_facts.yml"
Результат выполнения playbook будет таким:
$ ansible-playbook 8_playbook_filters_to_nice_yaml.yml

Теперь в каталоге all_facts появились такие файлы:
192.168.100.1_facts.yml
192.168.100.2_facts.yml
192.168.100.3_facts.yml
Файл all_facts/192.168.100.1_facts.yml:
ansible_facts:
ansible_net_all_ipv4_addresses:
- 192.168.200.1
- 192.168.100.1
ansible_net_all_ipv6_addresses: []
ansible_net_config: "Building configuration...\n\nCurrent configuration : 7367\
\ bytes\n!\n! Last configuration change at 16:33:06 UTC Mon Jan 9 2017\nversion\
\ 15.2\nno service timestamps debug uptime\nno service timestamps log uptime\n\
service password-encryption\n!\nhostname R1\n!\nboot-start-marker\n
...
regex_findall, map, max#
Посмотрим пример использования фильтров одновременно и в шаблоне, и в playbook.
Сделаем playbook, который будет генерировать конфигурацию site-to-site VPN (GRE + IPsec) для двух сторон.
В этом случае, мы не будем отправлять команды на устройства, а воспользуемся модулем template, чтобы сгенерировать конфигурацию и записать её в локальные файлы.
Настройка GRE + IPsec выглядит таким образом:
crypto isakmp policy 10
encr aes
authentication pre-share
group 5
hash sha
crypto isakmp key cisco address 192.168.100.2
crypto ipsec transform-set AESSHA esp-aes esp-sha-hmac
mode transport
crypto ipsec profile GRE
set transform-set AESSHA
interface Tunnel0
ip address 10.0.1.2 255.255.255.252
tunnel source 192.168.100.1
tunnel destination 192.168.100.2
tunnel protection ipsec profile GRE
Playbook 8_playbook_filters_regex.yml
---
- name: Cfg VPN
hosts: 192.168.100.1,192.168.100.2
gather_facts: false
connection: local
vars:
wan_ip_1: 192.168.100.1
wan_ip_2: 192.168.100.2
tun_ip_1: 10.0.1.1 255.255.255.252
tun_ip_2: 10.0.1.2 255.255.255.252
tasks:
- name: Collect facts
ios_facts:
gather_subset:
- "!hardware"
provider: "{{ cli }}"
- name: Collect current tunnel numbers
set_fact:
tun_num: "{{ ansible_net_config | regex_findall('interface Tunnel(.*)') }}"
#- debug: var=tun_num
- name: Generate VPN R1
template:
src: templates/ios_vpn1.txt
dest: configs/result1.txt
when: wan_ip_1 in ansible_net_all_ipv4_addresses
- name: Generate VPN R2
template:
src: templates/ios_vpn2.txt
dest: configs/result2.txt
when: wan_ip_2 in ansible_net_all_ipv4_addresses
Разберемся с содержимым playbook. В этом playbook один сценарий и он применяется только к двум устройствам:
- name: Cfg VPN
hosts: 192.168.100.1,192.168.100.2
gather_facts: false
connection: local
Наша задача была в том, чтобы сделать playbook, который можно легко повторно использовать. А значит, нужно сделать так, чтобы нам не нужно было повторять несколько раз одни и те же вещи (например, адреса).
И, в данном случае не очень удобно будет, если мы будем создавать переменные в файлах host_vars. Удобней создать их в самом playbook, а когда нужно будет сгенерировать конфигурацию для другой пары устройств, достаточно будет сменить адреса в playbook.
Для этого, в сценарии создан блок с переменными:
vars:
wan_ip_1: 192.168.100.1
wan_ip_2: 192.168.100.2
tun_ip_1: 10.0.1.1 255.255.255.252
tun_ip_2: 10.0.1.2 255.255.255.252
Вместо адресов wan_ip_1, wan_ip_2, вам нужно будет подставить
белые адреса маршрутизаторов.
Адреса мы задаем вручную. Но, всё остальное, хотелось бы делать автоматически.
Например, для настройки VPN нам нужно знать номер туннеля, чтобы создать интерфейс. Но мы не можем взять какой-то произвольный номер, так как на маршрутизаторе уже может существовать туннель с таким номером. Нам нужно определять автоматически.
Для этого, мы сначала собираем факты об устройстве:
- name: Collect facts
ios_facts:
gather_subset:
- "!hardware"
provider: "{{ cli }}"
Теперь мы создадим факт, для каждого из маршрутизаторов, который будет содержать список текущих номеров туннелей. Создаем факт мы с помощью модуля set_fact.
Факт создается на основе того, что нам выдаст результат поиска в
конфигурации строки interface TunnelX
с помощью фильтра
regex_findall. Этот фильтр ищет все строки, которые совпадают с
регулярным выражением. А затем, запоминает и записывает в список то, что
попало в круглые скобки (номер туннеля).
- name: Collect current tunnel numbers
set_fact:
tun_num: "{{ ansible_net_config | regex_findall('interface Tunnel(.*)') }}"
Дальнейшая обработка списка будет выполняться в шаблоне.
Затем, мы генерируем шаблоны для устройств. Для каждого устройства есть свой шаблон. Поэтому, в каждой задаче стоит условие
when: wan_ip_1 in ansible_net_all_ipv4_addresses
Благодаря этому условию, мы выбираем для какого устройства будет сгенерирован какой конфиг.
ansible_net_all_ipv4_addresses - это список IP-адресов на устройства, вида:
ansible_net_all_ipv4_addresses:
- 192.168.200.1
- 192.168.100.1
Этот список был получен в задаче по сбору фактов.
Задача будет выполняться только в том случае, если в списке адресов на устройстве, был найден адрес wan_ip_1.
Генерация шаблонов:
- name: Generate VPN R1
template:
src: templates/ios_vpn1.txt
dest: configs/result1.txt
when: wan_ip_1 in ansible_net_all_ipv4_addresses
- name: Generate VPN R2
template:
src: templates/ios_vpn2.txt
dest: configs/result2.txt
when: wan_ip_2 in ansible_net_all_ipv4_addresses
Шаблон templates/ios_vpn1.txt выглядит таким образом:
{% if not tun_num %}
{% set tun_num = 0 %}
{% else %}
{% set tun_num = tun_num | map('int') | max %}
{% set tun_num = tun_num + 1 %}
{% endif %}
crypto isakmp policy 10
encr aes
authentication pre-share
group 5
hash sha
crypto isakmp key cisco address {{ wan_ip_2 }}
crypto ipsec transform-set AESSHA esp-aes esp-sha-hmac
mode transport
crypto ipsec profile GRE
set transform-set AESSHA
interface Tunnel {{ tun_num }}
ip address {{ tun_ip_1 }}
tunnel source {{ wan_ip_1 }}
tunnel destination {{ wan_ip_2 }}
tunnel protection ipsec profile GRE
Шаблон templates/ios_vpn2.txt выглядит точно также, меняются только переменные с адресами:
{% if not tun_num %}
{% set tun_num = 0 %}
{% else %}
{% set tun_num = tun_num | map('int') | max %}
{% set tun_num = tun_num + 1 %}
{% endif %}
crypto isakmp policy 10
encr aes
authentication pre-share
group 5
hash sha
crypto isakmp key cisco address {{ wan_ip_1 }}
crypto ipsec transform-set AESSHA esp-aes esp-sha-hmac
mode transport
crypto ipsec profile GRE
set transform-set AESSHA
interface Tunnel {{ tun_num }}
ip address {{ tun_ip_2 }}
tunnel source {{ wan_ip_2 }}
tunnel destination {{ wan_ip_1 }}
tunnel protection ipsec profile GRE
В самой конфигурации никаких сложностей нет. Обычная подстановка переменных.
Разберемся с этой частью:
{% if not tun_num %}
{% set tun_num = 0 %}
{% else %}
{% set tun_num = tun_num | map('int') | max %}
{% set tun_num = tun_num + 1 %}
{% endif %}
Переменная tun_num - это факт, который мы устанавливали в playbook. Если на маршрутизаторе созданы туннели, эта переменная содержит список номеров туннелей. Но, если на маршрутизаторе нет ни одного туннеля, мы получим пустой список.
Если мы получили пустой список, то можно создавать интерфейс Tunnel0. Если мы получили список с номерами, то мы вычисляем максимальный и используем следующий номер, для нашего туннеля.
Если переменная tun_num будет пустым списком, нам нужно установить её равной 0 (пустой список - False):
{% if not tun_num %}
{% set tun_num = 0 %}
Иначе, нам нужно сначала конвертировать строки в числа, затем выбрать из чисел максимальное и добавить 1. Это и будет значение переменной tun_num.
{% else %}
{% set tun_num = tun_num | map('int') | max %}
{% set tun_num = tun_num + 1 %}
{% endif %}
Выполнение playbook (создайте каталог configs):
$ ansible-playbook 8_playbook_filters_regex.yml

На маршрутизаторе 192.168.100.1 специально созданы несколько туннелей. А на маршрутизаторе 192.168.100.2 нет ни одного туннеля.
В результате, мы получили такие конфигурации (configs/result1.txt):
crypto isakmp policy 10
encr aes
authentication pre-share
group 5
hash sha
crypto isakmp key cisco address 192.168.100.2
crypto ipsec transform-set AESSHA esp-aes esp-sha-hmac
mode transport
crypto ipsec profile GRE
set transform-set AESSHA
interface Tunnel 16
ip address 10.0.1.1 255.255.255.252
tunnel source 192.168.100.1
tunnel destination 192.168.100.2
tunnel protection ipsec profile GRE
Файл configs/result2.txt:
crypto isakmp policy 10
encr aes
authentication pre-share
group 5
hash sha
crypto isakmp key cisco address 192.168.100.1
crypto ipsec transform-set AESSHA esp-aes esp-sha-hmac
mode transport
crypto ipsec profile GRE
set transform-set AESSHA
interface Tunnel 0
ip address 10.0.1.2 255.255.255.252
tunnel source 192.168.100.2
tunnel destination 192.168.100.1
tunnel protection ipsec profile GRE