Глава 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
для завершения этого плейбука:
Если мы изменим имеющееся значение feature.enabled
на
False
, наш вывод будет слегка иным:
Встроенные условные зависимости
Оператор 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 '' }}"
Исполнение данного плейбука отобразит построенный шаблон:
Заменив значение api.v2
на
false
мы получим другой результат:
Некоторый цикл позволяет вам делать некие динамически создаваемые разделы в файлах шаблонов и полезен когда
вы знаете что вам придётся работать с неопределённым числом элементов одним и тем же образом. Для старта некоторой
управляющей циклической структуры применяется оператор 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') }}"
Исполнение нашего плейбука отобразит следующий результат:
Фильтрация элементов цикла
Циклы можно объединять с условными зависимостями. Внутри некоторой структуры цикла может быть применён
некий оператор 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 %}
Данная структура не просто требует меньше набора текста, но также правильно определяет счётчик самих циклов, что будет подробно исследовано в нашем следующем разделе.
Индексация цикла
Счётчик цикла предоставляется бесплатно, давая индекс текущей итерации данного цикла. Будучи переменной, к нему можно выполнять доступ несколькими разными способами. Приводимая ниже таблица приводит все варианты доступа к индексам:
Переменная | Описание |
---|---|
|
Текущая итерация данного цикла (начиная с |
|
Текущая итерация данного цикла (начиная с |
|
Общее число итераций до окончания данного цикла (начиная с |
|
Общее число итераций до окончания данного цикла (начиная с |
|
Булево |
|
Булево |
|
Общее число элементов в данной последовательности |
Наличие информации, относящейся к значению позиции внутри данного цикла, может помочь с логикой, связанной
с тем содержимым, которое предстоит выстроить. Рассматривая наш предыдущий пример, вместо построения множества
строк 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') }}"
Теперь мы можем выполнить свой плейбук и просмотреть наше построенное содержимое:
Если в нашем предыдущем примере не допускались завершающие запятые, мы можем воспользоваться встроенным
оператором 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
с приведённым содержимым и исполним
свой плейбук:
Проницательный читатель заметит, что в нашем предыдущем примере у нас имелся некий повторяемый код.
Повторяющийся код является врагом любого разработчика и, к счастью, 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
, вывод будет следующим:
arguments
Данная переменная arguments
является неким кортежем из определённых
аргументов полученных данным макросом. Это те аргументы, которые определены явным образом, не особые
kwargs
и varargs
.
Наш предыдущий пример воспроизвёл некий пустой кортеж ()
,
поэтому давайте поменяем его чтобы получить что- то ещё:
{% macro test(var_a='a string') %}
{{ test.arguments }}
{%- endmacro -%}
{{ test() }}
Построение данного шаблона даст в результате следующее:
defaults
Эта переменная defaults
является неким кортежем всех определённых
по умолчанию значений для всех аргументов с ключевыми словами, которые получил в явном виде данный макрос.
Давайте поменяем наш макрос с тем, чтобы он отобразил установленные по умолчанию значения помимо
самих аргументов:
{% macro test(var_a='a string') %}
{{ test.arguments }}
{{ test.defaults }}
{%- endmacro -%}
{{ test() }}
Отрисовка данной версией нашего шаблона получает такой результат:
catch_kwargs
Данная переменная определена только если данный макрос сам выполняет доступ к определённой переменной
kwargs
чтобы захватить дополнительные аргументы с ключевым словом,
которые могут быть ему переданы помимо прочих. Без доступа к данной переменной kwargs
любые аргументы с ключевым словом в некотором вызове этого макроса будут иметь результатом некую ошибку
при построении своего шаблона. Точно так же, доступ к catch_kwargs
доступа к kwargs
будет иметь результатом некую ошибку отсутствия
определения. Давайте проведём изменения в нашем шаблоне примера вновь с тем, чтобы мы могли передавать
дополнительные kwargs
:
{% macro test() %}
{{ kwargs }}
{{ test.catch_kwargs }}
{%- endmacro -%}
{{ test(unexpected='surprise') }}
Построенной версией данного шаблона будет:
catch_varargs
Во многом аналогично catch_kwargs
, данная переменная существует если
наш макрос осуществляет доступ к переменной varargs
. Изменив наш
пример ещё раз, мы можем увидеть его в действии:
{% macro test() %}
{{ varargs }}
{{ test.catch_varargs }}
{%- endmacro -%}
{{ test('surprise') }}
Результатом построения шаблона будет:
caller
Переменная caller
требует некоторых дополнительных пояснений. Некий
макрос может вызывать другой макрос. Такой вызов может быть полезен если некий кусок нашего шаблона будет
применяться много раз, но часть его содержимого изменяется ещё, что может быть легко передаваться неким
параметром макроса. Данная переменная caller
не является в точности
некоторой переменной; это ничего более кроме некоторой обратной ссылки к самому вызову чтобы получить того
содержимого, которое вызвало данный макрос. Давайте обновим наш шаблон чтобы продемонстрировать вариант
использования:
{% macro test() %}
The text from the caller follows:
{{ caller() }}
{%- endmacro -%}
{% call test() %}
This is text inside the call
{% endcall %}
Результатом построения будет:
Некий вызов какого- то макроса всё- таки передаёт аргументы в этот макрос; может быть передана любая
комбинация аргументов или аргументов с ключевыми словами. Если данный макрос применяет
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 %}
После его построения результат будет таким:
Мы вызвали свой макрос 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
в реальном режиме времени:
Полный перечень всех встроенных в 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 }}
Хотя множество фильтров предоставляется в 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
Его вывод отображается в следующем снимке экрана:
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 }}"
Вывод этого плейбука отображён ниже:
Фильтры для обработки имён пути
Управление настройкой и оркестрация часто ссылаются на имена пути, причём как правило желательно иметь лишь часть всего пути. 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 }}"
Его вывод таков:
dirname
Инверсией basename
является dirname
.
Вместо того чтобы возвращать только окончательную часть некоторого пути,
dirname
возвратит всё за исключением такой окончательной части.
Давайте заменим своё предыдущее воспроизведение на применение dirname
и выполним его вновь:
expanduser
Часто пути к различным вещам поставляются с неким ярлыком пользователя, таким как
~/.stackrc
. Однако некоторые применения могут требовать наличие
полного пути к файлу. Вместо того чтобы усложнять всё вызовами command
и register
, имеющийся фильтр expanduser
предоставляет некий способ расширения полученного пути в необходимое полное определение. В данном примере
текущим именем пользователя является jkeating
:
---
- name: demo the filters
hosts: localhost
gather_facts: false
tasks:
- name: demo filter
debug:
msg: "{{ '~/.stackrc' | expanduser }}"
Вывод отображён на снимке экрана ниже:
Кодирование 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
Вывод отображается на следующем экранном снимке:
Поиск содержимого
Достаточно часто в 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
Этот пример короче, яснее и не создаёт дополнительных пропускаемых задач.
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() }}"
Его вывод отображается на приводимом далее снимке экрана:
Так как это методы объекта, нам необходимо получать к ним доступ с нотацией точки вместо применения
фильтра через |
.
Методы списков
Всего лишь пара методов изменяет сам список в его местоположении, а не возвращает некий новый список, и они таковы:
-
Index
: Возвращает самый первый индекс положения некоторого предоставленного значения -
Count
: Общее число элементов в данном списке
Методы int и float
Большая часть методов int
и
float
не представляют пользы в Ansible.
Порой наши переменные не в том точно формате, который нам желателен. Однако, вместо определения ещё и ещё переменных, которые слегка видоизменяют одно и то же содержимое, мы можем просто применять фильтры Jinja2 для выполнения манипуляций для себя в различных местах, которые требуют такого изменения. Это позволит нам оставаться эффективными при определении своих данных, предотвращает от многих дублирований переменных и задач, которые может понадобиться изменять в дальнейшем.
Сравнение применяется во многих местах в Ansible. Задача условных зависимостей состоит в сравнениях. Управляющие структуры Jinja2 часто применяют сравнения. некие фильтры также применяют сравнения. Чтобы управиться с применением Jinja2 в Ansible, важно понимать какие сравнения доступны.
Как и большинство языков, Jinja2 снабжён достаточным стандартным набором выражений сравнений, которые вы
бы могли ожидать, которые будут выдавать Булево true
или
false
.
Выражения в Jinja2 следующие:
Выражение | Описание |
---|---|
|
Сравнивает два объекта на их эквивалентность |
|
Сравнивает два объекта на их не эквивалентность |
|
|
|
|
|
|
|
|
Логика помогает группировать два или более сравнений вместе. Каждое сравнение называется здесь операндом.
-
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
Вывод приводится в следующем снимке экрана:
Jinja2 является мощным языком, который применяется в Ansible. Он применяется не только для генерации содержимого файла, но также используется для того чтобы сделать части плейбуков более динамичными. Мастерство работы с Jinja2 жизненно важно для создания и манипуляций элегантными и действенными плейбуками и ролями.
В своей следующей главе мы изучим более глубоко возможность Ansible по определению того, что составляет некое изменение или отказ для задач внутри какого- то воспроизведения.