Глава 3. Высвобождение всей мощи шаблонов Jinja2

{Прим. пер.: рекомендуем сразу обращаться к нашему более полному переводу 3 издания вышедшего в марте 2019 существенно переработанного и дополненного Полного руководства Ansible Джеймса Фримана и Джесса Китинга}

Построение шаблонов является источником жизненной силы Ansible. Начиная с содержимого файла настройки и вплоть до подстановки переменных в задачах, условных выражений и за их пределами, шаблоны вступают в игру практически во всех аспектах Ansible. Механизмом шаблонов в Ansible является Jinja2, некий современный и дружественный проектированию язык шаблонов для Python. Данная глава охватит ряд современных свойств построения шаблонов Jinja2:

  • Управляющие структуры

  • Манипуляция данными

  • Сравнения

Управляющие структуры

В Jinja2 некая управляющая структура ссылается на предметы в некотором шаблоне, которые управляют самим потоком имеющегося механизма синтаксического разбора данного шаблона. Такие структуры содержат, но не ограничиваются ими, условия, циклы и макросы. Внутри Jinja2 (в предположении, что применяются установки по умолчанию), некая управляющая структура появится внутри блоков {% ... %}. Такие открывающие и закрывающие блоки уведомляют синтаксический анализатор Jinja2 что предоставляется некое управляющее выражение вместо обычного текста или имени переменной.

Условные зависимости

Условия внутри некоторого шаблона создают некий путь принятия решения. Имеющийся механизм будет рассматривать это условие и выбирать из одного или более потенциальных блоков кода. Всегда имеются, как минимум, два: некий путь если данное условие выполняется (вычисляется как истинное) и некий подразумеваемый путь else некоторого пустого блока.

Самим оператором для условий выступает выражение if. Это выражение работает во многом похоже на то, как это происходит в Python. Некий оператор if может объединяться с одним или более не обязательных elif с неким не обязательным окончательным else и, в отличии от Python, требует определяемого в явном виде endif. Приводимый далее пример показывает некий кусочек шаблона файла config, соединяющего воедино замену обычной переменной и некоторой структуры if else:


setting = {{ setting }}
{% if feature.enabled %}
feature = True
{% else %}
feature = False
{% endif %}
another_setting = {{ another_setting }}
 	   

В данном примере наша переменная feature.enabled проверяется на предмет того существует ли она и не установлена ли она в значение False. Если это True, тогда применяется текст feature = True; в противном случае используется текст feature = False. За пределами данного управляющего блока имеющийся синтаксический анализатор осуществляет обычную подстановку переменной для той переменной, которая располагается внутри фигурных скобок. Множество путей может быть определено с применением некоторого выражения elif, которое представляется имеющемуся синтаксическому анализатору с другой необходимой проверкой в случае, если наша предыдущая проверка эквивалентна false.

Чтобы показать построение конкретного шаблона, мы сохраним свой пример шаблона как demo.j2 и затем сделаем некий плейбук с названием template-demo.yaml, который определяет необходимые для применения переменные и затем использует некий поиск шаблона как часть задачи pause для отображения построенного шаблона на экране:


---
- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    setting: a_val
    feature:
      enabled: true
    another_setting: b_val
  tasks:
    - name: pause with render
      pause:
      prompt: "{{ lookup('template', 'demo.j2') }}"
 	   

Исполнение данного плейбука отобразит построенный шаблон на экране при ожидании ввода. Мы можем просто нажать Enter для завершения этого плейбука:

 

Рисунок 3.1



Если мы изменим имеющееся значение feature.enabled на False, наш вывод будет слегка иным:

 

Рисунок 3.2



Встроенные условные зависимости

Оператор If может применяться внутри предложения. Это может быть полезно при некоторых вариантах, в которых не желательны дополнительные новые строки. Давайте построим некую последовательность событий при которых нам потребуется определить в качестве API либо cinder, либо cinderv2:


API = cinder{{ 'v2' if api.v2 else '' }}
 	   

Данный сценарий предполагает, что api.v2 определена как Булево True или False. Встроенное выражение if следует синтаксису <do something> if <conditional is true> else <do something else>. В некотором встроенном выражении if имеется некий неявный else; однако, такой подразумеваемый else означает вычисление некоторого неопределённого (undefined) объекта, что обычно создаёт некую ошибку. Мы защитились от такого случая, определив в явном виде else, который задаёт некую строку нулевой длины.

Давайте изменим свой плейбук для демонстрации некоей встроенной условной зависимости. На этот раз мы применим модуль debug для построения такого образца шаблона:


---
- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    api:
      v2: true
  tasks:
    - name: pause with render
      debug:
        msg: "API = cinder{{ 'v2' if api.v2 else '' }}"
 	   

Исполнение данного плейбука отобразит построенный шаблон:

 

Рисунок 3.3



Заменив значение api.v2 на false мы получим другой результат:

 

Рисунок 3.4



Циклы

Некоторый цикл позволяет вам делать некие динамически создаваемые разделы в файлах шаблонов и полезен когда вы знаете что вам придётся работать с неопределённым числом элементов одним и тем же образом. Для старта некоторой управляющей циклической структуры применяется оператор for. Давайте рассмотрим некоторый простой способ прохода в цикле какого- то списка каталогов в которых некая вымышленная служба может осуществлять поиск данных:


# data dirs
{% for dir in data_dirs %}
data_dir = {{ dir }}
{% endfor %}
 	   

В данном примере мы получим по одной строке data_dir = на элемент внутри определённой переменной data_dirs, в предположении что data_dirs является неким списком с по крайней мере одним элементом в нём. Если такая переменная не является неким списком (или другим итерационным типом) или не определён, будет выработана некая ошибка. Если данная переменная имеет какой- то итерационный тип, но является пустой, тогда не будет создано никаких строк. Jinja2 допускает реакцию на такой вариант, а также позволяет подставку в некую строку, когда не найдено никаких элементов в данной переменной через некое выражение else. В следующем примере допустим, что data_dirs является неким пустым списком:


# data dirs
{% for dir in data_dirs %}
data_dir = {{ dir }}
{% else %}
# no data dirs found
{% endfor %}
 	   

Мы можем проверить это без изменения своего плейбука и файла шаблона снова. Мы обновим demo.j2 приведённым выше содержимым и вновь воспользуемся prompt в нашем плейбуке:


---
- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    data_dirs: []
  tasks:
    - name: pause with render
      pause:
        prompt: "{{ lookup('template', 'demo.j2') }}"
 	   

Исполнение нашего плейбука отобразит следующий результат:

 

Рисунок 3.5



Фильтрация элементов цикла

Циклы можно объединять с условными зависимостями. Внутри некоторой структуры цикла может быть применён некий оператор if для проверки какого- то условия при помощи элемента данного цикла как части данного условия. Давайте расширим наш пример и защитимся от применения /) в качестве data_dir:


# data dirs
{% for dir in data_dirs %}
{% if dir != "/" %}
data_dir = {{ dir }}
{% endif %}
{% else %}
# no data dirs found
{% endfor %}
 	   

Наш предыдущий пример успешно фильтрует все элементы data_dirs, которые являются (/), однако требует больше набора текста, чем это необходимо. Jinja2 предоставляет некий механизм, который позволяет вам легко фильтровать элементы цикла как часть вашего выражения for. Давайте повторим предыдущий пример, воспользовавшись таким удобством:


# data dirs
{% for dir in data_dirs if dir != "/" %}
data_dir = {{ dir }}
{% else %}
# no data dirs found
{% endfor %}
 	   

Данная структура не просто требует меньше набора текста, но также правильно определяет счётчик самих циклов, что будет подробно исследовано в нашем следующем разделе.

Индексация цикла

Счётчик цикла предоставляется бесплатно, давая индекс текущей итерации данного цикла. Будучи переменной, к нему можно выполнять доступ несколькими разными способами. Приводимая ниже таблица приводит все варианты доступа к индексам:

Таблица 3-1. Способы доступа к индексу цикла
Переменная Описание

loop.index

Текущая итерация данного цикла (начиная с 1)

loop.index0

Текущая итерация данного цикла (начиная с 0)

loop.revindex

Общее число итераций до окончания данного цикла (начиная с 1)

loop.revindex0

Общее число итераций до окончания данного цикла (начиная с 0)

loop.first

Булево True если это первая итерация

loop.last

Булево True если это последняя итерация

loop.first

Общее число элементов в данной последовательности

Наличие информации, относящейся к значению позиции внутри данного цикла, может помочь с логикой, связанной с тем содержимым, которое предстоит выстроить. Рассматривая наш предыдущий пример, вместо построения множества строк data_dir для выражения каждого каталога данных, мы можем вместо этого предоставить некую отдельную строку с разделёнными запятой значениями. Без наличия доступа к итерационным данным цикла это было бы затруднительно, однако, применяя эти данные действия могут быть достаточно простыми. Ради простоты данный пример предполагает допуск некоей завершающей запятой после самого последнего элемента, а также что пробельный символы (разделители строк) также разрешены между элементами:


# data dirs
{% for dir in data_dirs if dir != "/" %}
{% if loop.first %}
data_dir = {{ dir }},
{% else %}
           {{ dir }},
{% endif %}
{% else %}
# no data dirs found
{% endfor %}
 	   

Наш предыдущий пример применил переменную loop.first чтобы определить нужно ли строить часть data_dir = или ему всего лишь требуется построить соответствующим образом дополненный пробелами каталог. применяя некий филтр в имеющемся выражении for, мы получим верное значение для loop.first, даже если самый первый элемент в data_dirs является нежелательным (/). Чтобы проверить это, мы вновь видоизменим demo.j2 обновлённым шаблоном и поменяем template-demo.yaml чтобы он определял некий data_dirs, содержащий один /, который следует отфильтровать:


---
- name: demo the template
  hosts: localhost
  gather_facts: false
  vars:
    data_dirs: ['/', '/foo', '/bar']
  tasks:
    - name: pause with render
      pause:
        prompt: "{{ lookup('template', 'demo.j2') }}"
 	   

Теперь мы можем выполнить свой плейбук и просмотреть наше построенное содержимое:

 

Рисунок 3.6



Если в нашем предыдущем примере не допускались завершающие запятые, мы можем воспользоваться встроенным оператором if для определения того что мы выполняем данный цикл и верно расставили запятые, что отображает следующий пример:


# data dirs.
{% for dir in data_dirs if dir != "/" %}
{% if loop.first %}
data_dir = {{ dir }}{{ ',' if not loop.last else '' }}
{% else %}
           {{ dir }}{{ ',' if not loop.last else '' }}
{% endif %}
{% else %}
# no data dirs found
{% endfor %}
 	   

Применение операторов if позволяет нам строить некий шаблон, который будет рисовать некую запятую если в нашем цикле имеется больше элементов, чем передал наш начальный фильтр. И снова, давайте обновим demo.j2 с приведённым содержимым и исполним свой плейбук:

 

Рисунок 3.7



Макросы

Проницательный читатель заметит, что в нашем предыдущем примере у нас имелся некий повторяемый код. Повторяющийся код является врагом любого разработчика и, к счастью, Jinja2 имеет некий способ помочь ему! Макрос подобен некоторой функции в обычном языке программирования; это вариант определения некоторой повторяющейся идиомы (шаблона низкого уровня). Некий макрос определяется внутри какого- то блока {% macro ... %} ... {% endmacro %} и имеет имя, а также принимает ноль или более аргументов. Код внутри некоторого макроса не наследует определённого пространства имён того блока, который вызывает этот макрос, поэтому все аргументы должны передаваться явным образом. Макрос вызывается внутри заключённого в фигурные скобки блока по имени, а ноли или более аргументов передаются через круглые скобки. Давайте создадим некий простой макрос с названием comma для его включения в наш повторяющийся код:


{% macro comma(loop) %}
{{ ',' if not loop.last else '' }}
{%- endmacro -%}
# data dirs.
{% for dir in data_dirs if dir != "/" %}
{% if loop.first %}
data_dir = {{ dir }}{{ comma(loop) }}
{% else %}
           {{ dir }}{{ comma(loop) }}
{% endif %}
{% else %}
# no data dirs found
{% endfor %}
 	   

Вызов comma и его передача в наш объект loop позволяет данному макросу иучить этот цикл и решить должна ли быть установлена запятая или нет. Вы могли заметить некие специальные пометки в строке endmacro. Эти маркеры, - вслед за %, указывают Jinja2 зачищать имеющийся пробельный символ до и сразу после данного блока. Это делает для нас возможным иметь некий символ разделителя строки между данным макросом и начинать свой шаблон для удобства чтения без реального построения которое начнётся с перехода на новую строку при выработке данного шаблона.

Макро переменные

Макрос имеет внутри себя к любому передаваемому позиционно или по ключевому слову аргументу при вызове такого макроса. Позиционными являются аргументы, которые назначаются переменным на основе того порядка, в котором они предоставляются, в то время как аргументы с ключевым словом не упорядочены и в явном виде назначают данные по имени переменной. Аргументы с ключевым словом могут также иметь некое значение по умолчанию, если они не определены при вызове данного макроса. Также имеются доступными дополнительные специальные переменные:

  • varargs

  • kwargs

  • caller

Переменная varargs является держательницей места для дополнительных не ожидаемых позиционных аргументов, передаваемых в данный макрос. Такие значения позиционного аргумента будут построены получаемым списком varargs.

Имеющаяся переменная kwargs то же самое что и varargs; однако, вместо содержания дополнительных значений позиционных аргументов, она будет хранить некий хэш дополнительных снабжённых ключом элементов и связанных с ними значений.

Определённая переменная caller может использоваться для обратного вызова к более высокому уровню макроса, который мог вызвать данный макрос (да, макрос может вызывать другой макрос).

Дополнительно к этим трём специальным переменным имеется ряд переменных, которые выставляют внутренние подробности, относящиеся к самому макросу. Это слегка не просто, но мы пройдём их применение одну за другой. Для начала давайте взглянем на описание каждой переменной:

  • name: название самого данного макроса

  • arguments: Некий кортеж из соответствующих имён и аргументов, принимаемых данным макросом

  • catch_kwargs: Булево значение, которое определяется как true если данный макрос обращается (и таким образом допускает) к определённой переменной kwargs

  • catch_varargs: Булево значение, которое определяется как true если данный макрос обращается (и таким образом допускает) к определённой переменной varargs

  • catch_caller: Булево значение, которое определяется как true если данный макрос обращается к определённой переменной caller (и таким образом может вызываться из другого макроса)

Аналогично некоторому классу в Python, эти переменные необходимы для того, чтобы иметь возможность обращаться по определённому имени самого макроса. Попытка доступа к такому макросу без установленного спереди имени будет иметь результатом не определённую переменную. Теперь давайте пройдёмся по каждой из них чтобы продемонстрировать их варианты использования.

name

Данная переменная name на самом деле очень проста. Она всего лишь предоставляет некий способ доступа к самому названию данного макроса в виде некоторой переменной для последующих манипуляций или её применения. Следующий шаблон содержит некий макрос, который ссылается по определённому имени на конкретный макрос чтобы построить свой вывод:


{% macro test() %}
{{ test.name }}
{%- endmacro -%}
{{ test() }}
 	   

Если мы обновим demo.j2 этим шаблоном и выполним плейбук template-demo.yaml, вывод будет следующим:

 

Рисунок 3.8



arguments

Данная переменная arguments является неким кортежем из определённых аргументов полученных данным макросом. Это те аргументы, которые определены явным образом, не особые kwargs и varargs. Наш предыдущий пример воспроизвёл некий пустой кортеж (), поэтому давайте поменяем его чтобы получить что- то ещё:


{% macro test(var_a='a string') %}
{{ test.arguments }}
{%- endmacro -%}
{{ test() }}
 	   

Построение данного шаблона даст в результате следующее:

 

Рисунок 3.9



defaults

Эта переменная defaults является неким кортежем всех определённых по умолчанию значений для всех аргументов с ключевыми словами, которые получил в явном виде данный макрос. Давайте поменяем наш макрос с тем, чтобы он отобразил установленные по умолчанию значения помимо самих аргументов:


{% macro test(var_a='a string') %}
{{ test.arguments }}
{{ test.defaults }}
{%- endmacro -%}
{{ test() }}
 	   

Отрисовка данной версией нашего шаблона получает такой результат:

 

Рисунок 3.10



catch_kwargs

Данная переменная определена только если данный макрос сам выполняет доступ к определённой переменной kwargs чтобы захватить дополнительные аргументы с ключевым словом, которые могут быть ему переданы помимо прочих. Без доступа к данной переменной kwargs любые аргументы с ключевым словом в некотором вызове этого макроса будут иметь результатом некую ошибку при построении своего шаблона. Точно так же, доступ к catch_kwargs доступа к kwargs будет иметь результатом некую ошибку отсутствия определения. Давайте проведём изменения в нашем шаблоне примера вновь с тем, чтобы мы могли передавать дополнительные kwargs:


{% macro test() %}
{{ kwargs }}
{{ test.catch_kwargs }}
{%- endmacro -%}
{{ test(unexpected='surprise') }}
 	   

Построенной версией данного шаблона будет:

 

Рисунок 3.11



catch_varargs

Во многом аналогично catch_kwargs, данная переменная существует если наш макрос осуществляет доступ к переменной varargs. Изменив наш пример ещё раз, мы можем увидеть его в действии:


{% macro test() %}
{{ varargs }}
{{ test.catch_varargs }}
{%- endmacro -%}
{{ test('surprise') }}
 	   

Результатом построения шаблона будет:

 

Рисунок 3.12



caller

Переменная caller требует некоторых дополнительных пояснений. Некий макрос может вызывать другой макрос. Такой вызов может быть полезен если некий кусок нашего шаблона будет применяться много раз, но часть его содержимого изменяется ещё, что может быть легко передаваться неким параметром макроса. Данная переменная caller не является в точности некоторой переменной; это ничего более кроме некоторой обратной ссылки к самому вызову чтобы получить того содержимого, которое вызвало данный макрос. Давайте обновим наш шаблон чтобы продемонстрировать вариант использования:


{% macro test() %}
The text from the caller follows:
{{ caller() }}
{%- endmacro -%}
{% call test() %}
This is text inside the call
{% endcall %}
 	   

Результатом построения будет:

 

Рисунок 3.13



Некий вызов какого- то макроса всё- таки передаёт аргументы в этот макрос; может быть передана любая комбинация аргументов или аргументов с ключевыми словами. Если данный макрос применяет varargs или kwargs, тогда и они также могут передаваться дополнительно вместе с остальными. Кроме того, некий макрос может передавать аргументы обратно вызывающему, да, тоже! Чтобы показать это, давайте создадим пример большего размера. На это раз наш пример создаст некий файл, применимый для какой- то инвентаризации Ansible:


{% macro test(group, hosts) %}
[{{ group }}]
{% for host in hosts %}
{{ host }} {{ caller(host) }}
{%- endfor %}
{%- endmacro -%}
{% call(host) test('web', ['host1', 'host2', 'host3']) %}
ssh_host_name={{ host }}.example.name ansible_sudo=true
{% endcall %}
{% call(host) test('db', ['db1', 'db2']) %}
ssh_host_name={{ host }}.example.name
{% endcall %}
 	   

После его построения результат будет таким:

 

Рисунок 3.14



Мы вызвали свой макрос test дважды, по разу для каждой группы, которые мы хотели определить. Каждая группа имеет тонкие наборы отличий переменных хостов для применения, причём они определены в самой стороне, выполнившей вызов. Мы уклонились от излишнего набора кода имея определённый обратный вызов самой вызывающей стороны, передавая ему название хоста из текущего прохода цикла.

Управляющие блоки предоставляют мощность программирования внутри шаблонов, что делает возможным авторам шаблона выполнять свои шаблоны более эффективно. Данная действенность не обязательно вступает в игру в первоначальном проекте такого шаблона; вместо этого реальная эффективность вступает в игру когда необходимы некие небольшие изменения в каких- то повторяющихся значениях.

Манипуляция данными

В то время как управляющие структуры воздействуют на сам поток обработки шаблона, для изменения самого содержимого некоторых переменных имеется другой инструмент. Этот инструмент называется фильтром. Фильтры это то же самое, что небольшие функции или методы, которые могут исполняться с данной переменной. некоторые фильтры работают без аргументов, другие получают необязательные параметры, а некоторые требуют параметры. Фильтры могут также соединяться воедино, причём полученный результат действия одного фильтра запитывает определённый следующий и далее. Jinja2 поставляется с большим числом встроенных фильтров, а Ansible расширяет их многими индивидуальными фильтрами, доступными для вас при применении Jinja2 с шаблонами, задачами или прочими местами Ansible, допускающими построение шаблонов.

Синтаксис

Некий фильтр применяется к какой- то переменной посредством символа конвейера (|) с последующим определённым названием данного фильтра и после него все аргументы для данного фильтра внутри круглых скобок. Может присутствовать некий пробел между самим именем переменной и символом конвейера, а также пробел между символом конвейера и именем применяемого фильтра. Например, если вы желаете применить определённый фильтр lower (который изменит все переменные на нижний регистр) к своей переменной my_word, мы можем воспользоваться следующим синтаксисом:


{{ my_word | lower }}
 	   

Так как наш фильтр lower не получает никаких аргументов, нет необходимости присоединять некий набор пустых круглых скобок к нему. Давайте применим другой фильтр replace, который позволяет нам заменять все вхождения подстроки другой подстрокой. В данном примере мы хотим заменить все вхождения имеющейся подстроки no на yes в своей переменной answers:


{{ answers | replace('no', 'yes') }}
 	   

Применение множества фильтров выполняется простым добавлением большего числа символов конвейера и дополнительных имён фильтров. Давайте объединим replace и lower чтобы продемонстрировать этот синтаксис:


{{ answers | replace('no', 'yes') | lower }}
 	   

Мы легко можем продемонстрировать это в некотором простом воспроизведении, которое применяет команду debug для построения такой строки:


---
- name: demo the template
  hosts: localhost
  gather_facts: false
  tasks:
    - name: debug the template
      debug:
        msg: "{{ answers | replace('no', 'yes') | lower }}"
	  

Теперь мы можем выполнить свой плейбук и предоставить некое значение для answers в реальном режиме времени:

 

Рисунок 3.15



Полезные встроенные фильтры

Полный перечень всех встроенных в Jinja2 фильтров можно найти в имеющейся документации по Jinja2. На момент написания данной книги имелось более 45 встроенных фильтров, слишком много чтобы описать их здесь. Вместо этого мы рассмотрим некоторые из наиболее часто применяемых фильтров.

default

Фильтр default является неким способом предоставления какого- то значения по умолчанию для некоторой в противном случае не определённой переменной, что предотвратит Ansible от выработки ошибки. Это является условным обозначением сложного оператора if проверяющего, что если некая переменная определена до попытки её применения с каким- то условием else для предоставления некоторого отличного значения. Давайте взглянем на два примера построения одной и той же вещи. Одно применит имеющуюся структуру if/else, в то время как другое воспользуется фильтром default:


{% if some_variable is defined %}
{{ some_variable }}
{% else %}
default_value
{% endif %}
{{ some_variable | default('default_value') }}
	  

Построенный результат для каждого из этих примеров один и тот же, при этом пример применяющий фильтр default намного быстрее написать и легче прочесть.

Хотя default и очень полезен, работайте с осторожностью если вы применяете одну и ту же переменную во множестве мест. Изменение некоторого значения по умолчанию может стать некоторой преградой и может оказаться более действенным определить эту переменную по умолчанию в самом воспроизведении или на уровне роли.

count

Фильтр count возвратит общую длину некоторой последовательности или хэша. На самом деле, length является синонимом count для выполнения той же самой вещи. Данный фильтр может быть полезен для выполнения любого вида математических действий с имеющимся размером некоторого набора хостов или любого другого варианта, при котором требуется знать общее число элементов некоторого набора. Давайте создадим некий пример в котором мы устанавливаем элемент настройки max_threads для соответствия общему числу хостов в данном воспроизведении:


max_threads: {{ play_hosts | count }}
	  

random

Фильтр random применяется для совершения некоторого случайного выбора в какой- то последовательности. Давайте применим этот фильтр для представления какой- то задачи некоторому случайному выбору из имеющейся группы db_servers:


- name: backup the database
  shell: mysqldump -u root nova > /data/nova.backup.sql
  delegate_to: "{{ groups['db_servers'] | random }}"
  run_once: true
	  

round

Фильтр round присутствует для округления числа. Это может быть полезно для выполнения вычислений с плавающей запятой с последующим превращением полученного результата в округлённое целое. Фильтр round принимает необязательные параметры для определения точности (значение по умолчанию 0) и какого- то метода округления. Возможными методами округления являются common (округление вниз или вверх, выбирается по умолчанию), ceil (всегда округлять вверх), floor (всегда округлять вниз). В данном примере мы соединяем в цепочку два фильтра вместе для обычного округления некоторого математического результата с нулевой точностью и последующим превращением его в некое значение int:


{{ math_result | round | int }}
	  

Полезные создаваемые Ansible фильтры

Хотя множество фильтров предоставляется в Jinja2, Ansible содержит некоторые дополнительные фильтры, которые авторы плейбуков могут найти чрезвычайно полезными. Мы здесь опишем некоторые из них.

Связанные с состоянием задачи фильтры

Ansible отслеживает данные задачи для каждой задачи. Эти данные применяются для определения того, что если некая задача отказала, были ли выполнены некие изменения, либо они все совместно были пропущены. Авторы плейбуков могут регистрировать результаты некоторой задачи и затем применять фильтры для простой проверки состояния определённой задачи. Это наиболее часто применяется в условных зависимостях с последующими задачами. Такие фильтры соответственно именуются failed, success, changed и skipped. Каждый из них возвращает некое Булево значение. Вот некий плейбук, который демонстрирует применение пары из них:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: fail a task
      debug:
        msg: "I am not a change"
      register: derp
    - name: only do this on change
      debug:
        msg: "You had a change"
      when: derp | changed
    - name: only do this on success
      debug:
        msg: "You had a success"
      when: derp | success
 	   

Его вывод отображается в следующем снимке экрана:

 

Рисунок 3.16



shuffle

Аналогично описанному ранее фильтру random, фильтр shuffle может применяться для производства случайных результатов. В отличие от фильтра random, который выбирает один случайный экземпляр из некоторого списка, фильтр shuffle перетасовывает все элементы в некоторой последовательности и возвращает полученную последовательность обратно:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: shuffle the cards
      debug:
        msg: "{{ ['Ace', 'Queen', 'King', 'Deuce'] | shuffle }}"
 	   

Вывод этого плейбука отображён ниже:

 

Рисунок 3.17



Фильтры для обработки имён пути

Управление настройкой и оркестрация часто ссылаются на имена пути, причём как правило желательно иметь лишь часть всего пути. Ansible предоставляет несколько фильтров на подмогу.

basename

Для получения самой последней части некоторого файла пути воспользуйтесь фильтром basename. Например:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: demo basename
      debug:
        msg: "{{ '/var/log/nova/nova-api.log' | basename }}"
 	   

Его вывод таков:

 

Рисунок 3.18



dirname

Инверсией basename является dirname. Вместо того чтобы возвращать только окончательную часть некоторого пути, dirname возвратит всё за исключением такой окончательной части. Давайте заменим своё предыдущее воспроизведение на применение dirname и выполним его вновь:

 

Рисунок 3.19



expanduser

Часто пути к различным вещам поставляются с неким ярлыком пользователя, таким как ~/.stackrc. Однако некоторые применения могут требовать наличие полного пути к файлу. Вместо того чтобы усложнять всё вызовами command и register, имеющийся фильтр expanduser предоставляет некий способ расширения полученного пути в необходимое полное определение. В данном примере текущим именем пользователя является jkeating:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: demo filter
      debug:
        msg: "{{ '~/.stackrc' | expanduser }}"
 	   

Вывод отображён на снимке экрана ниже:

 

Рисунок 3.20



Кодирование Base64

При чтении содержимого с удалённых хостов, аналогичного применению модулем slurp (используемого для чтения содержимого файла с удалённых хостов в некую переменную), полученное содержимое будет кодировано в Base64. Чтобы декодировать такое содержимое, Ansible предоставляет некий фильтр b64decode. Аналогично, если выполняется какая- то задача, которая требует кодированного в Base64 ввода, обычные строки могут быть закодированы с помощью фильтра b64encode.

Давайте считаем содержимое из своего файла derp:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: read file
      slurp:
        src: derp
      register: derp
    - name: display file content (undecoded)
      debug:
        var: derp.content
    - name: display file content (decoded)
      debug:
        var: derp.content | b64decode
 	   

Вывод отображается на следующем экранном снимке:

 

Рисунок 3.21



Поиск содержимого

Достаточно часто в Ansible требуется отыскать некую строку с подстрокой. В частности, распространённая задача администратора при запуске команды и выполнения grep вывода для определённой ключевой части данных является повторное построение во многих плейбуках. Хотя это можно реплицировать и некоторой задачей shell для исполнения некоторой команды и конвейера для вывода в grep, а также аккуратной обработки failed_when для перехвата кодов выхода из grep, намного лучшая стратегия состоит в применении некоторой задачи command, register полученного вывода и последующего применения Ansible, осуществляющего фильтрацию regex в последующих условных зависимостях. Давайте рассмотрим два примера, один с применением методов shell, pipe, grep и другого, использующего фильтр search:


- name: check database version
  shell: neutron-manage current |grep juno
  register: neutron_db_ver
  failed_when: false
- name: upgrade db
  command: neutron-manage db_sync
  when: neutron_db_ver|failed
 	   

Наш предыдущий пример работает заставляя Ansible всегда видеть данную задачу как успешную, но предполагает, что если код выхода из оболочки не нулевой, то такая строка juno не обнаружена в полученном выводе исполненной команды neutron-manage. Данная конструкция рабочая, но слегка тяжеловесна и может маскировать реальные ошибки данной команды. Давайте попробуем снова с применением имеющегося фильтра search:


- name: check database version
  command: neutron-manage current
  register: neutron_db_ver
- name: upgrade db
  command: neutron-manage db_sync
  when: not neutron_db_ver.stdout | search('juno')
 	   

Эта версия намного яснее, чтобы выбрать её, и при этом не маскирует ошибки своей первой задачи.

Такой фильтр search осуществляет поиск некоторой строки и возвращает True если такая подстрока обнаружена где- то во входной строке. Если вместо этого требуется точное совпадение, может быть применён фильтр match. Внутри строки search/match можно применять полный синтаксис regex Python.

Пропуск неопределённых аргументов

Переменная omit требует неких пояснений. Иногда, когда выполняются итерации по хэшу данных для построения аргументов задачи, может понадобиться не только предоставить некие аргументы для каких- то из имеющихся в данном хэше элементов. Даже несмотря на то, что Jinja2 поддерживает встроенные операторы if для условного построения частей какой- то строки, это не работает как нужно в некоторых задачах Ansible. Как правило, авторы плейбука создают множество задач, по одной для каждого набора потенциально передаваемых аргументов и применяет условные зависимости для сортировки членов цикла между каждым набором задачи. Только что добавленная магическая переменная omit решает данную проблему при применении совместно с имеющимся фильтром default. Данная переменная omit удалит такой аргумент той переменной, которая была использована совместно.

Чтобы проиллюстрировать как это работает, давайте рассмотрим некий вариант при котором нам необходимо установить некий набор пакетов Python с помощью pip. Некоторые из этих пакетов находятся в каком- то списке хэшей с названием pips. Каждый хэш имеет ключ name и потенциально ключ ver. Наш первый пример использует две различные задачи для выполнения такой установки:


- name: install pips with versions
  pip: name={{ item.name }} version={{ item.ver }}
  with_items: pips
  when: item.ver is defined
- name: install pips without versions
  pip: name={{ item.name }}
  with_items: pips
  when: item.ver is undefined
 	   

Данная конструкция работает, однако наш цикл выполняет итерации дважды, причём некоторые итерации будут пропущены в каждой задаче. Наш следующий пример сливает обе задачи в одну и использует переменную omit:


- name: install pips
  pip: name={{ item.name }} version={{ item.ver | default(omit) }}
  with_items: pips
 	   

Этот пример короче, яснее и не создаёт дополнительных пропускаемых задач.

Объектные методы Python

Jinja2 является основанным на Python механизме шаблона. По этой причине, внутри шаблонов доступны методы объекта Python. Методами объекта являются методы или функции, которые непосредственно доступны по самой переменной объекта (обычно string, list, int или float). Хороший повод вспомнить это когда вы пишите код Python и можете написать саму переменную, затем точку, после этого некий вызов метода и потом вы получите доступ сделать то же самое в Jinja2. Внутри Ansible обычно используются только методы, которые возвращают изменённое содержимое или некое Булево значение. Давайте исследуем некоторые общие методы объекта, которые могут оказаться полезными в Ansible.

Строковые методы

Строковые методы могут применяться для возврата новой строки или какого- то списка строк, изменённых неким образом или для проверки такой строки на различные условные зависимости и возврата Булева значения. Некоторые полезные методы:

  • endswith: Определяет заканчивается ли данная строка искомой подстрокой

  • startswith: То же что и endswith, но для начала строки

  • split: Расщепляет определённую строку по символу (значением по умолчанию является пробел) на некий список подстрок

  • rsplit: То же, что и split, но начинает работу с конца данной строки и работает в обратном порядке

  • splitlines: Разделяет строку по символам новой строки на некий список подстрок

  • upper: Возвращает некую копию данной строки, переводя всю её в верхний регистр

  • lower: Возвращает некую копию данной строки, переводя всю её в нижнний регистр

  • capitalize: Возвращает некую копию данной строки, причём только самый первый символ в верхнем регистре

Мы можем создать некое простое воспроизведение, которое применит некоторые из этих методов в какой- то отдельной задаче:


---
- name: demo the filters
  hosts: localhost
  gather_facts: false
  tasks:
    - name: string methods
      debug:
        msg: "{{ 'foo bar baz'.upper().split() }}"
 	   

Его вывод отображается на приводимом далее снимке экрана:

 

Рисунок 3.22



Так как это методы объекта, нам необходимо получать к ним доступ с нотацией точки вместо применения фильтра через |.

Методы списков

Всего лишь пара методов изменяет сам список в его местоположении, а не возвращает некий новый список, и они таковы:

  • Index: Возвращает самый первый индекс положения некоторого предоставленного значения

  • Count: Общее число элементов в данном списке

Методы int и float

Большая часть методов int и float не представляют пользы в Ansible.

Порой наши переменные не в том точно формате, который нам желателен. Однако, вместо определения ещё и ещё переменных, которые слегка видоизменяют одно и то же содержимое, мы можем просто применять фильтры Jinja2 для выполнения манипуляций для себя в различных местах, которые требуют такого изменения. Это позволит нам оставаться эффективными при определении своих данных, предотвращает от многих дублирований переменных и задач, которые может понадобиться изменять в дальнейшем.

Сравнение значений

Сравнение применяется во многих местах в Ansible. Задача условных зависимостей состоит в сравнениях. Управляющие структуры Jinja2 часто применяют сравнения. некие фильтры также применяют сравнения. Чтобы управиться с применением Jinja2 в Ansible, важно понимать какие сравнения доступны.

Сравнения

Как и большинство языков, Jinja2 снабжён достаточным стандартным набором выражений сравнений, которые вы бы могли ожидать, которые будут выдавать Булево true или false.

Выражения в Jinja2 следующие:

Таблица 3-2. Выражения сравнения в Jinja2
Выражение Описание

==

Сравнивает два объекта на их эквивалентность

!=

Сравнивает два объекта на их не эквивалентность

>

true если значение левой стороны больше чем значение справа

<

true если значение левой стороны меньше чем значение справа

>=

true если значение левой стороны больше чем значение справа или эквивалентно ему

<=

true если значение левой стороны меньше чем значение справа или эквивалентно ему

Логика

Логика помогает группировать два или более сравнений вместе. Каждое сравнение называется здесь операндом.

  • and: Возвращает true если и левый и правый операнды имеют значение true

  • or: Возвращает true, если значение левого или правого операнда имеет значение true

  • not: Выполняет отрицание значения операнда

  • (): Обёртывает операнды вместе для формирования операнда большего размера

Тесты

Некая проверка в Jinja2 применяется чтобы посмотреть имеется ли некое значение чем- нибудь. На самом деле для инициации некоторого теста применяется оператор is. Тесты применяются в любом месте, где нужен некий Булев результат, например в выражении if и задачах условных зависимостей. Имеется множество встроенных проверок, однако мы выделим некоторые из наиболее полезных:

  • Defined: Возвращает true если данная переменная определена

  • Undefined: Противоположность Defined

  • None: Возвращает true если данная переменная определена, но не имеет значения

  • Even: Возвращает true если данное число делится на два без остатка

  • odd: Возвращает true если данное число не делится на два без остатка

Мы можем создать некий плейбук, которые продемонстрирует некоторые из таких сравнений значений:


---
- name: demo the logic
  hosts: localhost
  gather_facts: false
  vars:
    num1: 10
    num3: 10
  tasks:
    - name: logic and comparison
      debug:
        msg: "Can you read me?"
      when: num1 >= num3 and num1 is even and num2 is not defined
 	   

Вывод приводится в следующем снимке экрана:

 

Рисунок 3.23



Выводы

Jinja2 является мощным языком, который применяется в Ansible. Он применяется не только для генерации содержимого файла, но также используется для того чтобы сделать части плейбуков более динамичными. Мастерство работы с Jinja2 жизненно важно для создания и манипуляций элегантными и действенными плейбуками и ролями.

В своей следующей главе мы изучим более глубоко возможность Ansible по определению того, что составляет некое изменение или отказ для задач внутри какого- то воспроизведения.