Часть 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 по определению того, что составляет некое изменение или отказ в задач в рамках какого- то воспроизведения.




![[Совет]](/common/images/admon/tip.png)


















