Часть 2. Написание плейбуков Ansible и поиск в них неисправностей
В этой части мы получим основательное понимание того как писать надёжные, разноплановые плейбуки, подходящие для использования а широком разнообразии вариантов применения и сред.
В эту часть включены такие главы:
Глава 5, Высвобождение всей мощи шаблонов Jinja2
Глава 6, Условия управления задачами
Глава 7, Компоновка повторно используемого содержания и ролей Ansible
Глава 8, Поиск неисправностей Ansible
Глава 9, Расширение Ansible
Глава 5. Высвобождение всей мощи шаблонов Jinja2
Содержание
Управление файлами настройки вручную является утомительной и подверженной ошибкам задачей, причём в равной степени рискованно выполнять сопоставление с шаблоном для внесения изменений в имеющиеся файлы, а гарантирование надёжности и точности также представляет собой временеёмкую задачу. Будете ли вы применять Ansible для определения содержимого файла настроек, осуществлять подстановку переменных в задачах, оценивать условные операторы или что-то ещё, шаблоны вступают в игру практически во всех плейбуках Ansible. По существу, принимая во внимание важность этой задачи, можно сказать что шаблоны являются источником жизненной силы Ansible.
Механизмом шаблонов в Ansible является Jinja2, некий современный и дружественный проектированию язык шаблонов для Python. Jinja2 сам по себе заслуживает отдельной книги, тем не менее в данной главе мы рассмотрим наиболее общеупортебимые в Ansible образцы шаблонов Jinja2 чтобы предоставить вам возможность для старта, а также дать вам почувствовать ту мощность, которую они способны привнести в ваши плейбуки. В этой главе мы рассмотрим следующие темы:
-
Управляющие структуры
-
Манипуляция данными
-
Сравнения
Ознакомьтесь с видеоматериалами Code in Action.
В Jinja2 некая управляющая структура ссылается на предметы в некотором шаблоне, которые управляют
самим потоком имеющегося механизма синтаксического разбора данного шаблона. Такие структуры содержат,
но не ограничиваются ими, условия, циклы и макросы. Внутри Jinja2 (в предположении, что применяются
установки по умолчанию), некая управляющая структура появится внутри блоков
{% ... %}
. Такие открывающие и закрывающие блоки уведомляют
синтаксический анализатор Jinja2 что предоставляется некое управляющее выражение вместо обычного
текста или имени переменной.
Условия внутри некоторого шаблона создают некий путь принятия решения. Имеющийся механизм будет рассматривать это
условие и выбирать из одного или более потенциальных блоков кода. Всегда имеются, как минимум, два: некий путь если
данное условие выполняется (вычисляется как true
) и либо определяемый в явном виде
путь else
если нет соответствия данному условию (вычисляется как
false
), либо же, как альтернатива какой- то подразумеваемый
путь 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
, наш вывод будет слегка иным, как это показано на следующем снимке экрана:
Как мы можем увидеть из этих простых тестов, Jinja2 предоставляет очень простой, но при этом всё ещё мощный способ определения данных через условия в неком шаблоне.
Встроенные условные зависимости
Оператор 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
мы получим другой результат, как это показано на снимке экрана ниже:
Как вы можете видеть, мы способны создавать очень лаконичный, но мощный код, который задаёт значения на основе некой переменной Ansible, как мы это наблюдали здесь.
Некий цикл позволяет вам делать некие динамически создаваемые разделы в файлах шаблонов и полезен когда
вы знаете что вам придётся работать с неопределённым числом элементов одним и тем же манером. Для старта какой- то
управляющей циклической структуры применяется оператор for
. Давайте
рассмотрим простой способ прохода в цикле какого- то списка каталогов в которых некая вымышленная
служба может осуществлять поиск данных:
# data dirs
{% for dir in data_dirs -%}
data_dir = {{ dir }}
{% endfor -%}
Совет | |
---|---|
По умолчанию, в Ansible 2.7.5 при построении конкретного шаблона блок со значением
|
В этом примере мы получим по одной строке 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') }}"
Исполнение нашего плейбука отобразит следующий результат:
Мы можем видеть, что наш оператор else
в соответствующем цикле
for
аккуратно обрабатывает пустой список
data_dirs
, в точности как мы бы и желали того для исполнения плейбука.
Фильтрация элементов цикла
Циклы также можно объединять с условными зависимостями. Внутри некоторой структуры цикла может быть применён
какой- то оператор 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
с приведённым содержимым и исполним
свой плейбук:
Полученный вывод во многом походит на тот что был ранее, но на этот раз за исключением того, что наш шаблон
вычисляет будет ли помещена запятая после каждого dir
в данном цикле с
применением встроенного if
, удаляя запятые, если бы они имелись в конце
для получаемого в итоге значения.
Проницательный читатель заметит, что в нашем предыдущем примере у нас имелся некий повторяемый код.
Повторяющийся код является врагом любого разработчика и, к счастью, 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
позволяет данному макросу изучить этот цикл и решить должна ли быть
опущена запятая или нет.
Макро переменные
Макрос имеет внутри себя к любому передаваемому позиционно или по ключевому слову аргументу при вызове такого макроса. Позиционными являются аргументы, которые назначаются переменным на основе того порядка, в котором они предоставляются, в то время как аргументы с ключевым словом не упорядочены и в явном виде назначают данные по имени переменной. Аргументы с ключевым словом могут также иметь некое значение по умолчанию, если они не определены при вызове данного макроса. Также имеются доступными дополнительные специальные переменные:
-
varargs
-
kwargs
-
caller
Переменная varargs
является держательницей места для дополнительных
не ожидаемых позиционных аргументов, передаваемых в данный макрос. Такие значения позиционного аргумента будут
построены получаемым списком varargs
.
Имеющаяся переменная kwargs
то же самое что и
varargs
; однако, вместо содержания дополнительных значений позиционных
аргументов, она будет хранить некий хэш дополнительных снабжённых ключом элементов и связанных с ними
значений.
Заданная переменная caller
может использоваться для обратного
вызова к более высокому уровню макроса, который мог вызвать данный макрос (да, макрос может вызывать другой
макрос).
Дополнительно к этим трём специальным переменным имеется ряд переменных, которые выставляют внутренние подробности, относящиеся к самому макросу. Это слегка не просто, но мы пройдём их применение одну за другой. Для начала давайте взглянем на описание каждой переменной:
-
name
: название самого данного макроса -
arguments
: Некий кортеж из соответствующих имён и аргументов, принимаемых данным макросом -
defaults
: Некий кортеж определяемых по умолчанию значений -
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
:
---
- name: demo the template
hosts: localhost
gather_facts: false
vars:
data_dirs: ['/', '/foo', '/bar']
tasks:
- name: pause with render
pause:
prompt: "{{ lookup('template', 'demo-macro.j2') }}"
Мы тогда бы получили следующий вывод:
Как мы видим из исполнения данной проверки, наш шаблон просто строится с применением соответствующего названия макроса и ничего более, в точности как мы и ожидали.
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') }}
Результатом построения шаблона будет:
И снова, мы можем видеть, что мы имели возможность перехватить и построить то неожиданное значение, которое было
передано в наш макрос, вместо того чтобы возвращать при построении некую ошибку, как это произошло, если бы мы не
воспользовались catch_varargs
.
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
в реальном режиме времени, как это показано в следующем коде:
Как мы можем здесь наблюдать, в нашей переменной значение слова no
заменено на
yes
и все буквы теперь представлены в нижнем регистре.
Полный перечень всех встроенных в Jinja2 фильтров можно найти в имеющейся документации по Jinja2. На момент написания данной книги имелось более 45 встроенных фильтров, слишком много чтобы описать их здесь. Вместо этого мы рассмотрим некоторые из наиболее часто применяемых фильтров.
Совет | |
---|---|
Если вы желаете просмотреть полный список всех доступных фильтров, для текущей версии (на момент написания книги) документация Jinja2 доступна здесь. |
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 }}
Это снабжает нас отличным лаконичным способом получения значения числа хостов, содержащихся внутри нашей переменной
play_hosts
с назначением полученного ответа в значении переменной
max_threads
.
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
Здесь мы запросто можем делегировать данную задачу отдельному участнику группы db_servers
,
руководствуясь случайным порядком при помощи нашего фильтра.
round
Фильтр round
присутствует для округления числа. Это может быть
полезно для выполнения вычислений с плавающей запятой с последующим превращением полученного результата
в округлённое целое. Фильтр round
принимает необязательные
параметры для определения точности (значение по умолчанию 0
) и
какого- то метода округления. Возможными методами округления являются
common
(округление вниз или вверх, выбирается по умолчанию),
ceil
(всегда округлять вверх),
floor
(всегда округлять вниз). В данном примере мы соединяем в цепочку
два фильтра вместе для обычного округления некоторого математического результата с нулевой точностью и
последующим превращением его в некое значение int
:
{{ math_result | round | int }}
Таким, образом, если значение переменной math_result
было установлено равным
3.4
, получаемый предыдущей цепочкой фильров вывод равнялся бы
3
.
Хотя множество фильтров предоставляется в Jinja2, Ansible содержит некоторые дополнительные фильтры, которые авторы плейбуков могут найти чрезвычайно полезными. И снова их имеется слишком много, чтобы мы могли поместить их в книгу, здесь мы опишем некоторые из них.
Совет | |
---|---|
Данные индивидуальные фильтры часто изменяются между выпускамии было бы неплохо просматривать их, в особенности когда вы их интенсивно применяете. Полный список имеющихся индивидуальных фильтров Ansible доступен здесь. |
Связанные с состоянием задачи фильтры
Ansible отслеживает данные задачи для каждой задачи. Эти данные применяются для определения того, что если некая задача отказала, были ли выполнены некие изменения, либо они все совместно были пропущены. Авторы плейбуков могут регистрировать результаты некоторой задачи и затем применять фильтры для простой проверки состояния определённой задачи. Он объявлен устаревшим; однако он заслуживает своего упоминания здесь, поскольку несмотря на то, что предыдущий метод применения таких фильтров и будет пока работать в Ansible 2.7, его поддержка полностью будет удалена в 2.9, а потому важно выполнять переход уже сейчас.
Ранее вы бы применяли некое условие с каким- то, подобным следующему фильтром:
when: derp | success
Теперь это следует записывать так:
when: derp is success
Давайте рассмотрим это в действии с неким совместимым с Ansible 2.9 плейбуком в следующем коде:
---
- 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 is changed
- name: only do this on success
debug:
msg: "You had a success"
when: derp is success
Его вывод отображается в следующем снимке экрана:
Как мы можем наблюдать, наш оператор debug
имеет результатом
success
и поэтому мы пропускаем исполнение задачи в
change
и выполняем ту, которая будет запущена в
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
Давайте допустим, что у нас есть требование работать только с самим названием файла из некого полного пути.
Очевидно, мы бы могли могли выполнить для этого сложное соответствие шаблону, но зачастую это приводит в результате к
тому, что такой код не просто читать и могут иметься сложности с его сопровождением. К счастью Ansible предоставляет
некий фильтр специально для выделения необходимого названия фильтра из полного пути, как мы это продемонстрируем. В данном
примере мы воспользуемся фильтром 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
предоставляет некий способ расширения полученного пути в необходимое полное определение. В данном примере
текущим именем пользователя является jfreeman
:
---
- 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
Вывод отображается на следующем экранном снимке:
Здесь мы можем наблюдать что мы успешно считали тот небольшой файл, который мы создали в какой- то переменной и что мы
можем видеть значение содержимой переменной, закодированной в виде Base64 (помните что это кодирование было выполнено самим
модулем slurp
). Затем мы можем применить декодирование неким фильтром чтобы
обнаружить содержимое первоначального файла.
Поиск содержимого
Достаточно часто в 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
.
Как мы уже постулировали это ранее относительно состояния задач, применение поиска по некой строке в Ansible
рассматривается как проверочное и объявлено устаревшим. Хотя это и звучит слегка странным, для совместимости с Ansible 2.9
и последующими версиями мы обязаны применять ключевое слово is
вместо
конвейера при применении 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 is search('juno')
То что мы здесь сказали, это исполнить саму задачу с названием upgrade db
при условии
что neutron_db_ver.stdout
не содержит в значении строки
juno
. Когда вы начнёте применять концепцию с
when: not ... is
, вы обнаружите что такая версия намного яснее, чтобы выбрать её,
и при этом не маскирует ошибки своей первой задачи.
Такой фильтр 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() }}"
Его вывод отображается на приводимом далее снимке экрана:
Так как это методы объекта, нам необходимо получать к ним доступ с нотацией точки вместо применения
фильтра через |
.
Методы списков
Большинство предоставляемых Ansible методов, которые относятся к спискам, выполняют изменение самого списка.
Тем не менее, есть два метода списков, которые полезны при работе со списками, в особенности при работе в циклах. Этими д
двумя функциями являются index
и count
, а
их функциональность описывается так:
-
index
: Возвращает самый первый индекс положения некоторого предоставленного значения -
count
: Общее число элементов в данном списке
Они могут быть чрезвычайно полезными при выполнении итераций по некому списку в цикле, поскольку они позволяют осуществлять логику позиционирования и предпринимать соответствующие действия, предоставляя наше положение в списке, как если бы мы работали через него. Это является общим местом в прочих изыках программирования и, к счастью, Ansible также предоставляет это.
Методы int и float
Большая часть методов int
и
float
не представляют пользы в Ansible.
Порой наши переменные не в том точно формате, который нам желателен. Однако, вместо определения ещё и ещё переменных, которые слегка видоизменяют одно и то же содержимое, мы можем просто применять фильтры Jinja2 для выполнения манипуляций для себя в различных местах, которые требуют такого изменения. Это позволит нам оставаться эффективными при определении своих данных, предотвращает от многих дублирований переменных и задач, которые может понадобиться изменять в дальнейшем.
Сравнение применяется во многих местах в Ansible. Задача условных зависимостей состоит в сравнениях.
Управляющие структуры Jinja2, такие как блоки if/elif/else
,
циклы for
и macros
часто применяют сравнения. некие фильтры также применяют сравнения. Чтобы
управиться с применением Jinja2 в Ansible, важно понимать какие сравнения доступны.
Как и большинство языков, Jinja2 снабжён достаточным стандартным набором выражений сравнений, которые вы
бы могли ожидать, которые будут выдавать Булево true
или
false
.
Выражения в Jinja2 следующие:
Выражение | Описание |
---|---|
|
Сравнивает два объекта на их эквивалентность |
|
Сравнивает два объекта на отсутствие их эквивалентности |
|
|
|
|
|
|
|
|
Если вы пользовались операциями сравнения в каком- то другом языке программирования (обычно в виде некого оператора
if
), это должно показаться вам знакомым. Jinja2 поддерживает эту функциональность
в шаблонах, что делает возможными некие мощные операции сравнения, которые можно было бы ожидать в логике сравнения
от любого приличного языка программирования.
Порой оказывается недостаточным отдельной операции самой по себе - возможно мы можем пожелать некое действие если
два сравнения вычисляют true
одновременно. В качестве альтернативы вы можете
пожелать чтобы выполнить операцию только если если сравнение не true
.
Логика в Jinja2 помогает группировать два или более сравнения воедино. Каждое сравнение называется операндом,
а сама логика, которая применяется для связывания этого воедино в сложные условия представлена в следующем списке:
-
and
: Возвращаетtrue
если и левый и правый операнды имеют значениеtrue
-
or
: Возвращаетtrue
, если значение левого или правого операнда имеет значениеtrue
-
not
: Выполняет отрицание значения операнда -
()
: Обёртывает операнды вместе для формирования операнда большего размера
Некая проверка в Jinja2 применяется чтобы посмотреть будет ли некая переменная соответствовать определённым
чётко определённым критериям и мы уже сталкивались с ними в данной главе в определённых специфических ситуациях.
Для инициации некоторого теста применяется оператор is
.
Тесты применяются в любом месте, где нужен некий Булев результат, например в выражении
if
и задачах условных зависимостей. Имеется множество встроенных
проверок, однако мы выделим некоторые из наиболее полезных:
-
defined
: Возвращаетtrue
если данная переменная определена -
dndefined
: ПротивоположностьDefined
-
none
: Возвращаетtrue
если данная переменная определена, но не имеет значения -
even
: Возвращаетtrue
если данное число делится на два без остатка -
odd
: Возвращаетtrue
если данное число не делится на два без остатка
Для проверки того что значение не является чем- то просто воспользуйтесь is not
Мы можем создать некий плейбук, которые продемонстрирует некоторые из таких сравнений значений:
---
- 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
Вывод приводится в следующем снимке экрана:
Здесь мы можем видеть что наше сложное условие вычислило true
, а потому
была выполнена отладка задачи.
Jinja2 является мощным языком, который применяется в Ansible. Он применяется не только для генерации содержимого файла, но также используется для того чтобы сделать части плейбуков более динамичными. Мастерство работы с Jinja2 жизненно важно для создания и манипуляций элегантными и действенными плейбуками и ролями.
В этой главе мы изучили как при помощи Jinja2 собирать простые шаблоны и представлять их из некого плейбука Ansible. Мы рассмотрели как сделать действенным применение структур управления для манипулирования данными и даже осуществили сравнения и проверки переменных как для управления потоком плейбуков Ansible (сохраняя сам код обладающим лёгким весом и эффективным), а также создавали и манипулировали данными без необходимости в дублировании определений и чрезмерного числа переменных.
В своей следующей главе мы изучим более глубоко возможность Ansible по определению того, что составляет некое изменение или отказ в задач в рамках какого- то воспроизведения.